编译32位和64位时,性能差异很大(快26倍)

Tra*_*uer 78 c# performance 32bit-64bit

我试图衡量在访问值类型和引用类型列表时使用a for和a 的区别foreach.

我使用以下类进行分析.

public static class Benchmarker
{
    public static void Profile(string description, int iterations, Action func)
    {
        Console.Write(description);

        // Warm up
        func();

        Stopwatch watch = new Stopwatch();

        // Clean up
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        watch.Start();
        for (int i = 0; i < iterations; i++)
        {
            func();
        }
        watch.Stop();

        Console.WriteLine(" average time: {0} ms", watch.Elapsed.TotalMilliseconds / iterations);
    }
}
Run Code Online (Sandbox Code Playgroud)

我用double我的价值类型.我创建了这个'假类'来测试引用类型:

class DoubleWrapper
{
    public double Value { get; set; }

    public DoubleWrapper(double value)
    {
        Value = value;
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,我运行了这段代码并比较了时差.

static void Main(string[] args)
{
    int size = 1000000;
    int iterationCount = 100;

    var valueList = new List<double>(size);
    for (int i = 0; i < size; i++) 
        valueList.Add(i);

    var refList = new List<DoubleWrapper>(size);
    for (int i = 0; i < size; i++) 
        refList.Add(new DoubleWrapper(i));

    double dummy;

    Benchmarker.Profile("valueList for: ", iterationCount, () =>
    {
        double result = 0;
        for (int i = 0; i < valueList.Count; i++)
        {
             unchecked
             {
                 var temp = valueList[i];
                 result *= temp;
                 result += temp;
                 result /= temp;
                 result -= temp;
             }
        }
        dummy = result;
    });

    Benchmarker.Profile("valueList foreach: ", iterationCount, () =>
    {
        double result = 0;
        foreach (var v in valueList)
        {
            var temp = v;
            result *= temp;
            result += temp;
            result /= temp;
            result -= temp;
        }
        dummy = result;
    });

    Benchmarker.Profile("refList for: ", iterationCount, () =>
    {
        double result = 0;
        for (int i = 0; i < refList.Count; i++)
        {
            unchecked
            {
                var temp = refList[i].Value;
                result *= temp;
                result += temp;
                result /= temp;
                result -= temp;
            }
        }
        dummy = result;
    });

    Benchmarker.Profile("refList foreach: ", iterationCount, () =>
    {
        double result = 0;
        foreach (var v in refList)
        {
            unchecked
            {
                var temp = v.Value;
                result *= temp;
                result += temp;
                result /= temp;
                result -= temp;
            }
        }

        dummy = result;
    });

    SafeExit();
}
Run Code Online (Sandbox Code Playgroud)

我选择了ReleaseAny CPU选项,运行程序并得到以下时间:

valueList for:  average time: 483,967938 ms
valueList foreach:  average time: 477,873079 ms
refList for:  average time: 490,524197 ms
refList foreach:  average time: 485,659557 ms
Done!
Run Code Online (Sandbox Code Playgroud)

然后我选择了Release和x64选项,运行程序并得到以下时间:

valueList for:  average time: 16,720209 ms
valueList foreach:  average time: 15,953483 ms
refList for:  average time: 19,381077 ms
refList foreach:  average time: 18,636781 ms
Done!
Run Code Online (Sandbox Code Playgroud)

为什么x64位版本更快?我期待一些差异,但不是这么大的东西.

我无法访问其他计算机.你可以在你的机器上运行它并告诉我结果吗?我正在使用Visual Studio 2015,我有一个Intel Core i7 930.

这是SafeExit()方法,所以你可以自己编译/运行:

private static void SafeExit()
{
    Console.WriteLine("Done!");
    Console.ReadLine();
    System.Environment.Exit(1);
}
Run Code Online (Sandbox Code Playgroud)

根据要求,使用double?而不是我DoubleWrapper:

任何CPU

valueList for:  average time: 482,98116 ms
valueList foreach:  average time: 478,837701 ms
refList for:  average time: 491,075915 ms
refList foreach:  average time: 483,206072 ms
Done!
Run Code Online (Sandbox Code Playgroud)

64位

valueList for:  average time: 16,393947 ms
valueList foreach:  average time: 15,87007 ms
refList for:  average time: 18,267736 ms
refList foreach:  average time: 16,496038 ms
Done!
Run Code Online (Sandbox Code Playgroud)

最后但并非最不重要:创建一个x86配置文件给我几乎相同的使用结果Any CPU.

usr*_*usr 87

我可以在4.5.2上重现这一点.这里没有RyuJIT.x86和x64拆卸都很合理.范围检查等是相同的.基本结构相同.没有循环展开.

x86使用一组不同的浮点指令.除了划分之外,这些指令的性能似乎与x64指令相当:

  1. 32位x87浮点指令在内部使用10字节精度.
  2. 扩展精度划分超级慢.

除法运算使32位版本非常慢.取消注释除法在很大程度上均衡了性能(从430ms到3.25ms的32位).

Peter Cordes指出,两个浮点单元的指令延迟并没有那么不同.也许一些中间结果是非规范化数字或NaN.这些可能会触发其中一个单元的慢速路径.或者,由于10字节与8字节浮点精度,两个实现之间的值可能不同.

Peter Cordes 还指出所有中间结果都是NaN ...删除这个问题(valueList.Add(i + 1)因此没有除数为零)大多数均衡结果.显然,32位代码根本不喜欢NaN操作数.让我们打印一些中间值:if (i % 1000 == 0) Console.WriteLine(result);.这证实了数据现在是理智的.

在进行基准测试时,您需要对实际工作负载进 但是谁会想到一个无辜的师会搞乱你的基准?!

尝试简单地将数字相加以获得更好的基准.

除法和模数总是很慢.如果您修改BCL Dictionary代码,只是不使用模运算符来计算存储区索引性能可衡量的改进.这是分裂的缓慢程度.

这是32位代码:

在此输入图像描述

64位代码(相同结构,快速划分):

在此输入图像描述

尽管使用了SSE指令,但这不是矢量化的.

  • "谁会想到一个无辜的师会搞乱你的基准?" 我一看到内循环中的一个分区就立刻做了,特别是.作为依赖链的一部分.当它的整数除以2的幂时,除法是无辜的.来自http://agner.org/optimize/insn表:Nehalem`fdiv`是7-27个周期的延迟(和相同的倒数吞吐量).`divsd`是7-22个周期.以3c延迟,1/c吞吐量添加`.Division是Intel/AMD CPU中唯一的非流水线执行单元.C#JIT没有为x86-64(使用`divPd`)的循环进行矢量化. (11认同)
  • 无论如何,div非常慢,但是10B x87 fdiv并不比8B SSE2慢很多,所以这并不能解释x86和x86-64之间的区别.可以解释的是FPU异常或具有非正规/无穷大的减速.x87 FPU控制字与SSE舍入/异常控制寄存器("MXCSR")分开.对于非正规或"NaN"的不同处理,我认为可以解释26 perf diff的因素.C#可以在MXCSR中设置denormals-are-zero. (4认同)
  • @Trauer和usr:我刚注意到`valueList [i] = i`,从`i = 0`开始,所以第一次循环迭代做'0.0/0.0`.因此,整个基准测试中的每个操作都是使用`NaN`s完成的.那个师看起来越来越无辜了!我不是NaN的性能专家,或者x87和SSE之间的区别,但我认为这解释了26x的性能差异.我敢打赌,如果你初始化`valueList [i] = i + 1`,你的结果将会在32和64位之间更接近*. (2认同)

Pet*_*des 31

valueList[i] = i,从i=0第一次循环迭代开始0.0 / 0.0. 因此,整个基准测试中的每个操作都是用NaNs 完成的.

正如@usr在反汇编输出中所示,32位版本使用x87浮点,而64位使用SSE浮点.

我不是NaNs的性能专家,或x87和SSE之间的差异,但我认为这解释了26x的性能差异.我敢打赌,你的结果将是大量的 32位和64位之间更密切的,如果你初始化valueList[i] = i+1.(更新:usr证实这使得32和64位性能相当接近.)

与其他业务相比,分部非常缓慢.请参阅我对@ usr的回答的评论.另请参阅http://agner.org/optimize/以获取大量关于硬件的优秀内容,并优化asm和C/C++,其中一些与C#相关.他为所有最新的x86 CPU提供了大多数指令的延迟和吞吐量指令表.

但是,对于正常值,10B x87 fdiv并不比SSE2的8B双精度慢得多divsd.IDK关于与NaN,无穷大或非正规的性能差异.

但是,他们对NaN和其他FPU异常的情况有不同的控制.所述的x87 FPU控制字是从SSE舍入/异常控制寄存器(MXCSR)分开.如果x87为每个分区获得CPU异常,但SSE不是,则可以很容易地解释因子26.或者在处理NaN时可能只有很大的性能差异.硬件是通过优化搅动NaN之后NaN.

IDK如果SSE控制避免使用非正规减速将在这里发挥作用,因为我相信result将会NaN一直这样.IDK,如果C#在MXCSR中设置denormals-are-zero标志,或者flush-to-zero-flag(首先写入零,而不是在读回时将非正规数设置为零).

我发现了一篇关于SSE浮点控制的英特尔文章,将其与x87 FPU控制字进行了对比.但是,它没有太多可说的NaN.它以此结束:

结论

为避免由于非正规数和下溢数导致的序列化和性能问题,请使用SSE和SSE2指令在硬件中设置Flush-to-Zero和Nonormals-Are-Zero模式,以便为浮点应用程序提供最高性能.

IDK,如果这有助于任何除零.

对于foreach

测试一个吞吐量有限的循环体可能很有意思,而不仅仅是一个循环携带的依赖链.事实上,所有的工作都取决于以前的结果; CPU并行执行任何操作(除了边界 - 在mul/div链运行时检查下一个数组加载).

如果"实际工作"占用了更多的CPU执行资源,您可能会发现方法之间存在更多差异.此外,在Sandybridge之前的Intel上,28uop循环缓冲区中的循环拟合与否之间存在很大差异.如果没有,你会得到指令解码瓶颈,尤其是 当平均指令长度较长时(SSE发生).解码到多个uop的指令也会限制解码器的吞吐量,除非它们的格式对解码器来说很好(例如2-1-1).因此,循环开销更多指令的循环可以区分28条uop缓存中的循环,这对Nehalem来说是一个大问题,有时候对Sandybridge和后来有用.