我一直在做一些性能测试,主要是因为我可以理解迭代器和简单for循环之间的区别。作为其中的一部分,我创建了一组简单的测试,然后对结果感到惊讶。对于某些方法,64位比32位快近10倍。
我正在寻找的是为什么会发生这种情况的一些解释。
[下面的答案说明这是由于32位应用程序中的64位算术。将long更改为int会在32位和64位系统上产生良好的性能。]
以下是有问题的3种方法。
private static long ForSumArray(long[] array)
{
var result = 0L;
for (var i = 0L; i < array.LongLength; i++)
{
result += array[i];
}
return result;
}
private static long ForSumArray2(long[] array)
{
var length = array.LongLength;
var result = 0L;
for (var i = 0L; i < length; i++)
{
result += array[i];
}
return result;
}
private static long IterSumArray(long[] array)
{
var result = 0L;
foreach (var entry in array)
{
result += entry;
}
return result;
}
我有一个简单的测试工具来测试这个
var repeat = 10000;
var arrayLength = 100000;
var array = new long[arrayLength];
for (var i = 0; i < arrayLength; i++)
{
array[i] = i;
}
Console.WriteLine("For: {0}", AverageRunTime(repeat, () => ForSumArray(array)));
repeat = 100000;
Console.WriteLine("For2: {0}", AverageRunTime(repeat, () => ForSumArray2(array)));
Console.WriteLine("Iter: {0}", AverageRunTime(repeat, () => IterSumArray(array)));
private static TimeSpan AverageRunTime(int count, Action method)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < count; i++)
{
method();
}
stopwatch.Stop();
var average = stopwatch.Elapsed.Ticks / count;
return new TimeSpan(average);
}
当我运行这些时,我得到以下结果:
32位:
For: 00:00:00.0006080 For2: 00:00:00.0005694 Iter: 00:00:00.0001717
64位
For: 00:00:00.0007421 For2: 00:00:00.0000814 Iter: 00:00:00.0000818
我从中读到的内容是使用LongLength很慢。如果我使用array.Length,第一个for循环的性能在64位中相当不错,但不是32位。
我从中读到的另一件事是迭代数组和for循环一样高效,代码更清晰,更容易阅读!
答案 0 :(得分:51)
long
(64位整数)变量。
例如,在x64汇编中,要添加存储在寄存器中的几个64位整数,您可以简单地执行:
; adds rbx to rax
add rax, rbx
要在32位x86处理器上执行相同的操作,您必须使用两个寄存器并在第二个操作中手动使用第一个操作的进位:
; adds ecx:ebx to edx:eax
add eax, ebx
adc edx, ecx
更多指令和更少的寄存器意味着更多的时钟周期,内存提取,......这最终会导致性能降低。数字运算应用程序的差异非常明显。
对于.NET应用程序,似乎64位JIT编译器执行更积极的优化,以提高整体性能。
关于你关于数组迭代的观点,C#编译器足够聪明,可以识别数组foreach
并特别处理它们。生成的代码与使用for
循环相同,如果您不需要更改循环中的数组元素,建议您使用foreach
。除此之外,运行时识别模式for (int i = 0; i < a.Length; ++i)
并省略循环内数组访问的绑定检查。在LongLength
情况下不会发生这种情况,并且会导致性能下降(对于32位和64位情况都是如此);由于您将long
变量用于LongLength
,因此32位性能会降低甚至更多。
答案 1 :(得分:5)
long数据类型为64位,在64位进程中,它作为单个本机长度单元处理。在32位进程中,它被视为2个32位单元。数学,尤其是这些“分裂”类型将是处理器密集型的。
答案 2 :(得分:1)
不确定“为什么”,但我会确保在定时器循环之外至少调用一次“方法”,这样你就不会计算第一次jitting。 (因为这看起来像C#)。
答案 3 :(得分:1)
哦,这很容易。 我假设您使用的是x86技术。在汇编程序中执行循环需要什么?
所以你需要三个变量。如果可以将它们存储在寄存器中,则变量访问最快;如果你需要将它们移入和移出内存,你就会失去速度。 对于64位长,你需要在32位上有两个寄存器,而我们只有四个寄存器,所以很可能所有变量都不能存储在寄存器中,但必须存储在像堆栈这样的中间存储器中。仅这一点就会大大减慢访问速度。
增加数字: 增加必须是两次;第一次没有进位,第二次带进位。它可以在一个周期内完成64位。
动/装载: 对于每个1个周期的64位var,你需要两个32bit的周期才能将一个长整数加载/卸载到内存中。
每个组件数据类型(数据类型包含比寄存器/地址位更多的位) 会失去相当快的速度。一个数量级的速度增益是GPU更喜欢浮点数(32位)而不是双打(64位)的原因。
答案 4 :(得分:0)
正如其他人所说的那样,在32位机器上进行64位运算会采取一些额外的操作,如果进行乘法或除法则会更多。
回到你对迭代器与简单for循环的关注,迭代器可以有相当复杂的定义,只有在内联和编译器优化能够用等效的简单形式替换它们时它们才会很快。它实际上取决于迭代器的类型和底层容器实现。判断它是否已被合理优化的最简单方法是检查生成的汇编代码。另一种方法是将它放在一个长时间运行的循环中,暂停它,然后查看堆栈以查看它正在做什么。