看看这两个循环
const int arrayLength = ...
版本 0
public void RunTestFrom0()
{
int sum = 0;
for (int i = 0; i < arrayLength; i++)
for (int j = 0; j < arrayLength; j++)
for (int k = 0; k < arrayLength; k++)
for (int l = 0; l < arrayLength; l++)
for (int m = 0; m < arrayLength; m++)
{
sum += myArray[i][j][k][l][m];
}
}
版本 1
public void RunTestFrom1()
{
int sum = 0;
for (int i = 1; i < arrayLength; i++)
for (int j = 1; j < arrayLength; j++)
for (int k = 1; k < arrayLength; k++)
for (int l = 1; l < arrayLength; l++)
for (int m = 1; m < arrayLength; m++)
{
sum += myArray[i][j][k][l][m];
}
}
版本 2
public void RunTestFrom2()
{
int sum = 0;
for (int i = 2; i < arrayLength; i++)
for (int j = 2; j < arrayLength; j++)
for (int k = 2; k < arrayLength; k++)
for (int l = 2; l < arrayLength; l++)
for (int m = 2; m < arrayLength; m++)
{
sum += myArray[i][j][k][l][m];
}
}
arrayLength=50
的结果是(来自多次采样编译的 X64 的平均值):如果我们制作
arrayLength=45
然后为什么:
更新:
myArray
的长度每个维度的 = arrayLength
,我一开始就初始化了,所用的时间被排除在外。值为 1。所以 sum
给出总循环数。 更新2:
现在我丢弃
myArray
完全,sum++
相反,并添加了 GC.Collect()
public void RunTestConstStartConstEnd()
{
int sum = 0;
for (int i = constStart; i < constEnd; i++)
for (int j = constStart; j < constEnd; j++)
for (int k = constStart; k < constEnd; k++)
for (int l = constStart; l < constEnd; l++)
for (int m = constStart; m < constEnd; m++)
{
sum++;
}
}
最佳答案
更新
在我看来,这似乎是 尝试优化失败的结果。抖动 ,不是编译器。总之,如果抖动可以确定下限是一个常数,它会做一些不同的事情,结果实际上更慢。我的结论的基础需要一些证明,所以请耐心等待。或者,如果您不感兴趣,请阅读其他内容!
在测试了四种不同的设置循环下界的方法后,我得出了这个结论:
循环部分的所有四个版本的编译中间语言几乎相同。唯一的区别是在版本 1 中,下限是用命令
ldc.i4.#
加载的。 ,其中 #
是 0、1、2 或 3。这代表负载常数。 (见 ldc.i4 opcode)。在所有其他版本中,下限加载 ldloc
.即使在情况 3 中也是如此,编译器可以推断出 lowerBound
真的是一个常数。由此产生的性能不是恒定的。与 OP 发现的类似,版本 1(显式常量)比版本 2(运行时参数)慢。非常有趣的是,版本 3 是 还有较慢,时间与版本 1 相当。因此,即使 IL 将下限视为变量,抖动似乎已经发现该值永远不会改变,并像版本 1 一样替换为常数,相应的性能降低。在版本 4 中,抖动无法推断出我所知道的——
Confuser
实际上是一个恒等函数——所以它把变量作为一个变量。结果性能与命令行参数版本 (2) 相同。我关于性能差异原因的理论:抖动是意识到并利用了实际处理器架构的细节。当它决定使用
0
以外的常量时,它实际上必须从不在 L2 缓存中的某个存储中获取该文字值。当它获取一个经常使用的局部变量时,它会从 L2 缓存中读取它的值,这非常快。通常,使用像已知文字整数值这样愚蠢的东西占用宝贵缓存中的空间是没有意义的。在这种情况下,我们更关心读取时间而不是存储,因此它对性能产生了不希望的影响。这是版本 2 的完整代码(命令行参数):
class Program {
static void Main(string[] args) {
List<double> testResults = new List<double>();
Stopwatch sw = new Stopwatch();
int upperBound = int.Parse(args[0]) + 1;
int tests = int.Parse(args[1]);
int lowerBound = int.Parse(args[2]); // THIS LINE CHANGES
int sum = 0;
for (int iTest = 0; iTest < tests; iTest++) {
sum = 0;
GC.Collect();
sw.Reset();
sw.Start();
for (int lvl1 = lowerBound; lvl1 < upperBound; lvl1++)
for (int lvl2 = lowerBound; lvl2 < upperBound; lvl2++)
for (int lvl3 = lowerBound; lvl3 < upperBound; lvl3++)
for (int lvl4 = lowerBound; lvl4 < upperBound; lvl4++)
for (int lvl5 = lowerBound; lvl5 < upperBound; lvl5++)
sum++;
sw.Stop();
testResults.Add(sw.Elapsed.TotalMilliseconds);
}
double avg = testResults.Average();
double stdev = testResults.StdDev();
string fmt = "{0,13} {1,13} {2,13} {3,13}"; string bar = new string('-', 13);
Console.WriteLine();
Console.WriteLine(fmt, "Iterations", "Average (ms)", "Std Dev (ms)", "Per It. (ns)");
Console.WriteLine(fmt, bar, bar, bar, bar);
Console.WriteLine(fmt, sum, avg.ToString("F3"), stdev.ToString("F3"),
((avg * 1000000) / (double)sum).ToString("F3"));
}
}
public static class Ext {
public static double StdDev(this IEnumerable<double> vals) {
double result = 0;
int cnt = vals.Count();
if (cnt > 1) {
double avg = vals.Average();
double sum = vals.Sum(d => Math.Pow(d - avg, 2));
result = Math.Sqrt((sum) / (cnt - 1));
}
return result;
}
}
对于版本 1:除删除
lowerBound
外,同上声明并替换所有 lowerBound
文字实例 0
, 1
, 2
, 或 3
(分别编译和执行)。对于版本 3:除了用
int lowerBound = 0; // or 1, 2, or 3
对于版本 4:除了用
int lowerBound = Ext.Confuser<int>(0); // or 1, 2, or 3
哪里
Confuser
是:public static T Confuser<T>(T d) {
decimal d1 = (decimal)Convert.ChangeType(d, typeof(decimal));
List<decimal> L = new List<decimal>() { d1, d1 };
decimal d2 = L.Average();
if (d1 - d2 < 0.1m) {
return (T)Convert.ChangeType(d2, typeof(T));
} else {
// This will never actually happen :)
return (T)Convert.ChangeType(0, typeof(T));
}
}
结果(每个测试 50 次迭代,分 5 个批次,每批次 10 个):
1: Lower bound hard-coded in all loops:
Program Iterations Average (ms) Std Dev (ms) Per It. (ns)
-------- ------------- ------------- ------------- -------------
Looper0 345025251 267.813 1.776 0.776
Looper1 312500000 344.596 0.597 1.103
Looper2 282475249 311.951 0.803 1.104
Looper3 254803968 282.710 2.042 1.109
2: Lower bound supplied at command line:
Program Iterations Average (ms) Std Dev (ms) Per It. (ns)
-------- ------------- ------------- ------------- -------------
Looper 345025251 269.317 0.853 0.781
Looper 312500000 244.946 1.434 0.784
Looper 282475249 222.029 0.919 0.786
Looper 254803968 201.238 1.158 0.790
3: Lower bound hard-coded but copied to local variable:
Program Iterations Average (ms) Std Dev (ms) Per It. (ns)
-------- ------------- ------------- ------------- -------------
LooperX0 345025251 267.496 1.055 0.775
LooperX1 312500000 345.614 1.633 1.106
LooperX2 282475249 311.868 0.441 1.104
LooperX3 254803968 281.983 0.681 1.107
4: Lower bound hard-coded but ground through Confuser:
Program Iterations Average (ms) Std Dev (ms) Per It. (ns)
-------- ------------- ------------- ------------- -------------
LooperZ0 345025251 266.203 0.489 0.772
LooperZ1 312500000 241.689 0.571 0.774
LooperZ2 282475249 219.533 1.205 0.777
LooperZ3 254803968 198.308 0.416 0.778
这是一个庞大的数组。出于所有实际目的,您正在测试操作系统从内存中获取每个元素的值需要多长时间,而不是比较
j
, k
等小于 arrayLength
, 以增加计数器,并增加您的总和。获取这些值的延迟与运行时或抖动本身几乎没有关系,而与整个系统上运行的其他任何事情以及堆的当前压缩和组织有很大关系。此外,由于您的阵列占用了如此多的空间并且被频繁访问,因此在您的某些测试迭代期间很可能正在运行垃圾收集,这会完全增加明显的 CPU 时间。
尝试在没有数组查找的情况下进行测试——只需添加 1 (
sum++
),然后看看会发生什么。要更彻底,请调用 GC.Collect()
就在每次测试之前,以避免在循环期间进行收集。
关于c# - 循环从 0 开始比循环从 1 开始快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9739578/