我对.net 中低级算法的效率感兴趣。我希望让我们能够选择在未来使用 C# 而不是 C++ 编写更多代码,但一个绊脚石是在循环和随机访问数组时发生的 .net 中的边界检查。
一个有启发性的例子是一个函数,它计算两个数组中对应元素的乘积之和(这是两个向量的点积)。
static void SumProduct(double[] X, double[] Y)
{
double sum = 0;
int length = X.Length;
if (length != Y.Length)
throw new ArgumentException("X and Y must be same size");
for (int i = 0; i < length; i++) // Check X.Length instead? See below
sum += X[i] * Y[i];
}
据我所知,并且不知道足够的 IL 或 x86 来检查,编译器不会优化X
and Y
的边界检查。我错了和/或有没有办法编写我的代码以允许编译器帮助我?
更多详细信息
有许多支持和反对使用特定语言的效率论据,尤其是最好专注于“大 O”算法成本而不是比例常数,更高级别的语言可以帮助您做到这一点。关于 .net 中的边界检查,我发现的最好的文章是MSDN上 CLR 中的 Array Bounds Check Elimination(也在关于启用优化重要性的堆栈溢出答案中引用)。
这可以追溯到 2009 年,所以我想知道从那时起情况是否发生了显着变化。此外,这篇文章揭示了一些真正的微妙之处,这些细节会引起我的注意,因此仅出于这个原因,我就欢迎一些专家的建议。
例如,在我上面的代码中,我最好写i< X.Length
而不是i < length
. 此外,我还天真地假设对于具有单个数组的算法,编写一个foreach
循环会更好地向编译器声明您的意图,并给它优化边界检查的最佳机会。
根据SumForBAD
下面的 MSDN 文章,我认为肯定会优化,但不会。而SumFor
将被直接优化,并且SumForEach
也会被优化,但不是微不足道的(如果数组被传递给函数 as ,可能根本不会被优化IEnumerable<int>
)?
static double SumForBAD(double[] X)
{
double sum = 0;
int length = X.Length; // better to use i < X.length in loop
for (int i = 0; i < length; i++)
sum += X[i];
return sum;
}
static double SumFor(double[] X)
{
double sum = 0;
for (int i = 0; i < X.Length; i++)
sum += X[i];
return sum;
}
static double SumForEach(double[] X)
{
double sum = 0;
foreach (int element in X)
sum += element;
return sum;
}
我根据 doug65536 的回答做了一些调查。在 C++ 中,我比较了进行边界检查的 SumProduct 的时间
for(int i=0; i<n; ++i) sum += v1[i]*v2[i];
针对执行两次边界检查的另一个版本
for(int i=0; i<n1 && i <n2; ++i) sum += v1[i]*v2[i];
我发现第二个版本速度较慢,但只有 3.5% 左右(Visual Studio 2010,优化构建,默认选项)。但是我突然想到,在 C# 中,可能有三个边界检查。一个显式(在此问题开头i < length
的函数中)和两个隐式(和)。所以我测试了第三个 C++ 函数,带有三个边界检查static void SumProduct(double[] X, double[] Y)
X[i]
Y[i]
for(int i=0; i<n1 && i <n2 && i <n3; ++i) sum += v1[i]*v2[i];
这比第一个慢了 35%,值得关注。我在这个问题上做了更多调查,为什么在某些机器上添加额外的检查循环会产生很大的不同,而在其他机器上会产生很小的差异?. 有趣的是,边界检查的成本似乎在不同的机器上差异很大。