看看这两个循环
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
给出总循环。现在我完全放弃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++;
}
}
答案 0 :(得分:6)
<强>更新强>
在我看来,这是由于 jitter 而不是编译器尝试优化的结果。简而言之,如果 jitter 可以确定下限是一个常量,它会做一些不同的事情,结果实际上更慢。我的结论的基础需要一些证明,所以请耐心等待。如果你不感兴趣,或者去看别的东西!
我在测试了设置循环下限的四种不同方法后得出结论:
循环部分的所有四个版本的已编译中间语言几乎相同。唯一的区别是在版本1中,下限加载了命令ldc.i4.#
,其中#
是0,1,2或3.这代表加载常量。 (见ldc.i4 opcode)。在所有其他版本中,下限加载ldloc
。即使在情况3中也是如此,编译器可以推断出lowerBound
实际上是一个常数。
结果表现不稳定。版本1(显式常量)比版本2(运行时参数)沿着与OP找到的类似的行慢。非常有趣的是版本3 也更慢,与版本1相当。因此即使IL将下限视为变量,抖动似乎已经发现值永远不会更改,并替换版本1中的常量,并相应地降低性能。在版本4中,抖动无法推断出我所知道的内容 - Confuser
实际上是一个身份函数 - 因此它将变量保留为变量。结果性能与命令行参数version(2)相同。
我对性能差异原因的理论:抖动能够识别并利用实际处理器架构的精细细节。当它决定使用0
以外的常量时,它必须实际从某个不在L2缓存中的存储中获取该字面值。当它获取一个经常使用的局部变量时,它会从L2缓存中读取它的值,这是非常快的。通常情况下,使用像已知的文字整数值一样愚蠢的东西占用宝贵的缓存是没有意义的。在这种情况下,我们更关心的是读取时间而不是存储,因此它会对性能产生不良影响。
以下是版本2(命令行arg)的完整代码:
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
或{{ 1}}(单独编译和执行)。
对于版本3:与上面相同,除了用
替换lowerBound声明3
对于版本4:与上面相同,除了用
替换lowerBound声明 int lowerBound = 0; // or 1, 2, or 3
int lowerBound = Ext.Confuser<int>(0); // or 1, 2, or 3
的位置:
Confuser
结果(每次测试50次迭代,5次10次):
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));
}
}
这是一个充满活力的阵列。出于所有实际目的,您要测试操作系统从内存中获取每个元素的值所需的时间,而不是比较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
,增加计数器,增加总和。获取这些值的延迟与运行时或抖动本身没什么关系,与整个系统上正在运行的其他任何事情以及堆的当前压缩和组织有很大关系。
此外,由于您的阵列占用了大量空间并且经常被访问,因此很可能在测试迭代的某些期间运行垃圾收集,这将完全膨胀明显的CPU时间。
尝试在没有数组查找的情况下进行测试 - 只需添加1(arrayLength
),然后看看会发生什么。为了更加彻底,请在每次测试之前调用sum++
以避免在循环期间收集。
答案 1 :(得分:1)
我认为版本0更快,因为在这种情况下编译器会生成一个没有范围检查的特殊代码。请参阅http://msdn.microsoft.com/library/ms973858.aspx(范围检查消除)
答案 2 :(得分:0)
只是一个想法:
可能在循环中存在位移优化,因此在不均匀计数开始时需要更长的时间。
我也不知道您的处理器是否可能是不同结果的指标
答案 3 :(得分:0)
在我的头顶,也许有一个0的编译器优化 - >长度。检查构建设置(发布与调试)。
除此之外,除非你的计算机正在做其他影响基准测试的工作,否则我不确定。也许你应该改变你的基准测试以多次运行每个测试并平均结果。