为什么64位比32位更快?

23

我一直在进行性能测试,主要是为了理解迭代器和简单for循环之间的区别。作为其中的一部分,我创建了一组简单的测试,并对结果感到非常惊讶。对于某些方法,64位几乎比32位快10倍。

我正在寻找的是为什么会出现这种情况的解释。

[下面的答案指出这是由于32位应用中的64位算术所致。将长整型更改为整型可在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循环一样高,并且代码更加清晰易读!


我觉得有趣的是显然JIT编译器没有优化掉对array.LongLength的访问。 - newgre
5个回答

53

x64处理器包含64位通用寄存器,可以在一条指令中计算64位整数的操作。32位处理器则没有这个功能。对于使用大量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位性能将会进一步降低。


4
x64处理器的寄存器数量也增加了,但当运行32位代码时不使用这些寄存器,仅用于64位代码。 - Powerlord
关于C#编译器和foreach的评论非常棒,尤其是对于数组访问的边界检查! - Nick Randell

5

长整型数据类型是64位的,在64位进程中,它被处理为一个单一的本地长度单位。在32位进程中,它被视为2个32位单位。对于这些“分离”类型的数学运算,特别是在32位进程中,将会是处理器密集型操作。


2

不确定为什么,但我建议至少在计时器循环之外调用你的方法,这样你就不会计算第一次jitting。(因为这对我来说看起来像C#)。


1

这很容易。 我猜你正在使用x86技术。想要用汇编做循环需要什么?

  1. 一个索引变量 i
  2. 一个结果变量 result
  3. 一个长整型的结果数组。

所以你需要三个变量。如果你能将它们存储在寄存器中,那么变量访问速度是最快的;如果你需要将它们移进或移出内存,那么你就会失去速度。 对于64位的长整数,你需要 32位上的两个寄存器,而我们只有四个寄存器,因此所有变量都无法存储在寄存器中,但必须存储在像栈这样的中间存储器中。这本身会严重降低访问速度。

加法运算: 必须进行两次加法运算; 第一次没有进位标志位,第二次带进位标志位。64位可以在一次周期内完成。

移动/加载: 对于每个1周期64位变量,你需要两个周期来将长整型载入/卸载到内存中。

每种组件数据类型(比寄存器/地址位多的数据类型) 都会损失相当大的速度。数量级的速度增益正是GPU仍然更喜欢浮点数(32位)而不是双精度浮点数(64位)的原因。


0

就像其他人所说的那样,在32位计算机上进行64位算术运算将需要一些额外的操作,尤其是在进行乘法或除法时。

回到您关于迭代器与简单for循环的担忧,迭代器可能具有相当复杂的定义,并且只有在内联和编译器优化能够用等效的简单形式替换它们时才会变得更快。这确实取决于迭代器的类型和底层容器的实现。检查生成的汇编代码是判断是否已经合理优化的最简单方法之一。另一种方法是将其放入一个长时间运行的循环中,暂停它,并查看堆栈以查看它正在做什么。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接