【问题标题】:Why is this faster on 64 bit than 32 bit?为什么 64 位比 32 位更快?
【发布时间】:2010-12-28 20:22:00
【问题描述】:

我一直在做一些性能测试,主要是为了了解迭代器和简单 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位:

对于:00:00:00.0006080
For2: 00:00:00.0005694
迭代器:00:00:00.0001717

64 位

对于:00:00:00.0007421
For2: 00:00:00.0000814
迭代器:00:00:00.0000818

我从中读到的是,使用 LongLength 很慢。如果我使用 array.Length,第一个 for 循环的性能在 64 位中非常好,但不是 32 位。

我从中读到的另一件事是,对数组进行迭代与 for 循环一样高效,而且代码更简洁易读!

【问题讨论】:

  • 我还觉得有趣的是,JIT 编译器显然不会优化 array.LongLength 访问。

标签: c# performance


【解决方案1】:

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 &lt; a.Length; ++i) 并省略循环内数组访问的绑定检查。这不会在LongLength 情况下发生,并且会导致性能下降(对于 32 位和 64 位情况);并且由于您将使用long 变量和LongLength,因此32 位性能将进一步下降。

【讨论】:

  • x64 处理器中寄存器的数量也有所增加,但它们在运行 32 位代码时不使用这些寄存器,仅使用 64 位代码。
  • 关于 C# 编译器和 foreach 的精彩评论,尤其是数组访问的边界检查!
【解决方案2】:

long 数据类型是 64 位,在 64 位进程中,它作为单个原生长度单元处理。在 32 位进程中,它被视为 2 个 32 位单元。数学,尤其是在这些“拆分”类型上将是处理器密集型的。

【讨论】:

    【解决方案3】:

    不确定“为什么”,但我会确保在您的计时器循环之外至少调用一次您的“方法”,这样您就不会计算第一次抖动。 (因为这在我看来像 C#)。

    【讨论】:

      【解决方案4】:

      哦,这很容易。 我假设您使用的是 x86 技术。在汇编程序中执行循环需要什么?

      1. 一个索引变量 i
      2. 一个结果变量结果
      3. 一长串结果。

      所以你需要三个变量。如果您可以将它们存储在寄存器中,则变量访问是最快的;如果您需要将它们移入和移出内存,那么您将失去速度。 对于 64 位 long,您需要在 32 位上使用 两个 寄存器,而我们只有四个寄存器,因此所有变量很可能无法存储在寄存器中,而必须存储在像堆栈这样的中间存储中。仅此一项就会大大降低访问速度。

      数字相加: 加法必须是两次;第一次没有进位,第二次有进位。 64bit一个周期就可以搞定。

      移动/装载: 对于每 1 个周期的 64 位 var,您需要两个 32 位周期才能将长整数加载/卸载到内存中。

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

      【讨论】:

        【解决方案5】:

        正如其他人所说,在 32 位机器上执行 64 位算术将需要一些额外的操作,如果进行乘法或除法则更是如此。

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-11-23
          • 2018-03-23
          • 2017-06-05
          • 1970-01-01
          • 2012-10-26
          • 2014-07-31
          相关资源
          最近更新 更多