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


80

我试图衡量访问值类型和引用类型列表时使用afor和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);
    }
}

我使用double我的值类型。我创建了这个“假类”来测试引用类型:

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

    public DoubleWrapper(double value)
    {
        Value = value;
    }
}

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

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();
}

我选择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!

然后,我选择了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!

为什么x64位版本这么快?我期望有所不同,但不会那么大。

我无权访问其他计算机。您能在您的机器上运行此程序并告诉我结果吗?我正在使用Visual Studio 2015,并且具有Intel Core i7 930。

这是SafeExit()方法,因此您可以自己编译/运行:

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

根据要求,使用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!

x64

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!

最后但并非最不重要的一点:创建x86配置文件可为我提供与使用几乎相同的结果Any CPU


14
“任何CPU”!=“ 32Bits”!如果编译为“ Any CPU”,则您的应用程序应在64位系统上作为64位进程运行。另外,我将删除与GC混淆的代码。它实际上没有帮助。
Thorsten Dittmar

9
@ThorstenDittmar GC调用在测量之前,而不是在测量的代码中。这是足够合理的事情,可以减少运气GC计时会影响这种测量的程度。同样,在构建之间有一个因素是“赞成32位”与“赞成64位”。
乔恩·汉娜

1
@ThorstenDittmar但是我运行发行版(在Visual Studio之外),任务管理器说它是32位应用程序(编译为Any CPU时)。也。正如Jon Hanna所说,GC调用非常有用。
Trauer

2
您正在使用哪个运行时版本?在4.6的新RyuJIT是很多快,但即使早期版本中,64位编译器和JITer一些较新的,比X32版本更先进。他们能够执行比x86版本更为激进的优化。
Panagiotis Kanavos

2
我会注意到所涉及的类型似乎没有任何作用。更改doublefloatlong或者int,你会得到类似的结果。
乔恩·汉纳

Answers:


87

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

x86使用另一组浮点指令。这些指令的性能似乎可以与x64指令相媲美,但除法是

  1. 32位x87浮点指令在内部使用10字节精度。
  2. 扩展精度除法非常慢。

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

彼得·科德斯(Peter Cordes)指出,两个浮点单元的指令等待时间并没有什么不同。也许某些中间结果是非正规数或NaN。这些可能会触发其中一个单元的慢速路径。或者,由于10字节vs. 8字节的浮点精度,这两个实现之间的值可能会有所不同。

彼得·科德斯(Peter Cordes)还指出,所有中间结果均为NaN ...消除此问题(valueList.Add(i + 1)以使除数不为零)通常会使结果相等。显然,32位代码根本不喜欢NaN操作数。让我们打印一些中间值:if (i % 1000 == 0) Console.WriteLine(result);。这确认数据现在是正确的。

进行基准测试时,您需要对实际工作量进行基准测试。但是谁会想到一个无辜的分裂会破坏您的基准?

尝试简单地将数字求和以获得更好的基准。

除法和取模总是很慢。如果将BCLDictionary代码修改为仅不使用模运算符来计算存储桶索引,则可衡量的性能将得到改善。这是多么缓慢的分裂。

这是32位代码:

在此处输入图片说明

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

在此处输入图片说明

,尽管使用SSE指令量化。


11
“谁会想到一个无辜的部门会破坏您的基准?” 看到内环中的一个分割后,我马上就做到了,尤其是。作为依赖链的一部分。除法是整数除以2的幂时,才是无辜的。从agner.org/optimize insn表中:Nehalemfdiv是7-27个周期的延迟(和相同的倒数)。 divsd是7-22个周期。addsd延迟为3c,吞吐量为1 / c。除法是Intel / AMD CPU中唯一的非流水线执行单元。C#JIT并未向量化x86-64的循环(带有divPd)。
彼得·科德斯

1
另外,对于32b C#,不使用SSE数学是否正常?JIT要点不能使用当前计算机的功能吗?因此,在Haswell及更高版本上,它可以使用256b AVX2(而不只是SSE)自动向量化整数循环。为了获得FP循环的矢量化,我猜您必须并行编写4个累加器之类的东西,因为FP数学不是关联的。但是无论如何,在32位模式下使用SSE速度更快,因为在不必处理x87 FP堆栈的情况下执行相同的标量工作的指令较少。
彼得·科德斯

4
无论如何,div非常慢,但是10B x87 fdiv并不比8B SSE2慢很多,因此这不能解释x86和x86-64之间的区别。可以解释的是FPU异常或异常/无穷慢。x87 FPU控制字与SSE舍入/异常控制寄存器(MXCSR)分开。NaN我认为可以对异常值或s进行不同的处理来解释26 perf diff的因素。C#可以在MXCSR中将反常态设置为零。
彼得·科德斯

2
@Trauer和usr:我刚刚注意到valueList[i] = i,从开始i=0,所以第一次循环迭代就可以了0.0 / 0.0。因此,整个基准测试中的每个操作都使用NaNs完成。那个分裂看起来越来越无辜了!我不是NaNs的性能专家,也不是x87和SSE之间的区别,但是我认为这可以解释26倍的性能差异。我敢打赌,你的结果将是大量的接近32位和64位之间,如果你初始化valueList[i] = i+1
彼得·科德斯

1
至于刷新为零,我不太喜欢64位双精度,但是当同时使用80位扩展和64位双精度时,可能会出现80位值下溢然后被放大的情况产生一个可以表示为64位的double值非常罕见。80位类型的主要用法之一是允许将多个数字求和,而不必紧紧舍入结果直到最后。在这种模式下,溢出不是问题。
2015年

31

valueList[i] = i,从头开始i=0,所以第一次循环迭代就可以了0.0 / 0.0因此,整个基准测试中的每个操作都使用NaNs完成。

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

我不是NaNs的性能专家,也不是x87和SSE之间的区别,但是我认为这可以解释26倍的性能差异。我敢打赌,你的结果将是大量的接近32位和64位之间,如果你初始化valueList[i] = i+1。(更新:usr确认这使32位和64位性能相当接近。)

与其他作业相比,分割速度非常慢。请参阅我对@usr答案的评论。另请参阅http://agner.org/optimize/,以获取有关硬件,优化asm和C / C ++的大量书籍,其中一些与C#相关。他具有所有最新x86 CPU的大多数指令的延迟和吞吐量的指令表。

但是,对于正常值,10B x87fdiv并不比SSE2的8B双精度慢很多divsd。IDK关于与NaN,无穷大或异常有关的性能差异。

但是,他们对NaN和其他FPU异常的处理方式有不同的控制。所述的x87 FPU控制字是从SSE舍入/异常控制寄存器(MXCSR)分开。如果x87的每个分区都出现CPU异常,而SSE却不是,那很容易解释了26的原因。或者在处理NaN时,性能差异可能很大。硬件是通过优化搅动NaN之后NaN

IDK认为,如果SSE采取措施避免因异常而导致的放缓,那么IDK​​将会在这里发挥作用,因为我相信这resultNaN一直存在。如果C#在MXCSR中将“反常态为零”标志或“对零归零”标志(首先写入零,而不是在回读时将反常态视为零)设置为IDK。

我找到了一篇有关SSE浮点控件的Intel文章,将其与x87 FPU控制字进行了对比。不过,它没有太多要说的NaN。到此为止:

结论

为避免由于异常和下溢数导致的序列化和性能问题,请使用SSE和SSE2指令在硬件中设置“齐平为零”和“归零为零”模式,以使浮点应用程序具有最高性能。

IDK(如果这有助于除零)。

对于vs.foreach

测试受吞吐量限制的循环主体,而不只是测试一个循环承载的依赖链,可能会很有趣。实际上,所有工作都取决于先前的结果。CPU无需并行执行任何操作(除了在mul / div链运行时检查边界,检查下一个数组负载)。

如果“实际工作”占用了更多的CPU执行资源,您可能会发现方法之间的差异更大。另外,在Sandybridge之前的Intel版本中,是否在28uop循环缓冲区中进行循环拟合之间存在很大差异。如果没有,您将获得指令解码瓶颈,尤其是。当平均指令长度更长时(这在SSE中发生)。解码到多个uop的指令也将限制解码器的吞吐量,除非它们采用对解码器有利的模式(例如2-1-1)。因此,具有更多循环开销指令的循环可以使是否适合28个条目的uop缓存中的循环有所不同,这对Nehalem来说意义重大,有时对Sandybridge及以后版本有所帮助。


我从未遇到过基于NaN是否存在于我的数据流中而观察到任何性能差异的情况,但是非规范化数字的存在会对性能产生巨大的影响。在此示例中,情况似乎并非如此,但要记住这一点。
杰森R

@JasonR:那是因为NaN在实践中s确实很少见吗?我留下了所有有关异常的内容,以及与Intel的内容的链接,主要是为了读者的利益,而不是因为我认为这确实会对这种特定情况产生很大的影响。
彼得·科德斯

在大多数应用中,它们很少见。但是,在开发使用浮点的新软件时,实现错误会产生NaN流而不是期望的结果的情况并不罕见!这对我来说已经发生过很多次了,我不记得NaN弹出时任何明显的性能下降。如果我做会导致异常现象出现的事情,我会发现相反的情况。通常会导致性能立即下降。请注意,这些只是基于我的轶事。NaN可能会导致一些性能下降,而我只是没有注意到。
杰森R

@JasonR:IDK,也许NaN的速度与SSE相比并不慢。显然,对于x87,它们是一个大问题。SSE FP语义是由Intel在PII / PIII时代设计的。这些CPU在引擎盖下的混乱机器与当前设计相同,因此在设计SSE时,考虑到它们对P6的高性能。(是的,Skylake是基于P6微体系结构的。有些事情已经发生了变化,但是它仍然解码为微指令,并使用重排序缓冲区将它们调度到执行端口。)x87语义是为用于以下操作的可选外部协处理器芯片设计的:有序标量CPU。
彼得·科德斯

@PeterCordes将Skylake称为基于P6的芯片实在是太困难了。1)FPU(几乎)在桑迪布里奇时代进行了完全重新设计,因此旧的P6 FPU基本上已经消失到今天;2)x86到uop解码在Core2时代进行了重要的修改:以前的设计将计算和存储指令解码为单独的uops,而Core2 +芯片具有由计算指令内存运算符组成的uops 。这导致性能和功率效率大大提高,但设计成本更高,峰值频率可能更低。
shodanshok

1

我们观察到,所有浮点运算的99.9%将涉及NaN,这至少是非常不寻常的(首先由Peter Cordes发现)。我们通过usr进行了另一个实验,发现删除除法指令会使时差几乎完全消失。

然而事实是,NaN的生成仅是因为第一个除法运算得出的0.0 / 0.0给出了初始NaN。如果不执行除法,则结果将始终为0.0,并且我们将始终计算0.0 * temp-> 0.0,0.0 + temp-> temp,temp-temp = 0.0。因此,删除除法不仅删除除法,还删除了NaN。我希望NaN实际上是问题所在,并且一个实现处理NaN的速度非常慢,而另一个实现则没有问题。

值得在i = 1处开始循环并再次进行测量。这四个操作的结果为* temp,+ temp,/ temp,-temp有效加(1-temp),因此对于大多数操作,我们不会有任何不寻常的数字(0,无穷大,NaN)。

唯一的问题可能是除法总是给出整数结果,并且某些除法实现在正确结果不使用很多位的情况下具有捷径。例如,将310.0 / 31.0除以10.0作为前四个位,余数为0.0,某些实现可以停止评估剩余的50个左右的位,而其他实现则不能。如果存在显着差异,则以result = 1.0 / 3.0开始循环会有所不同。


-2

在计算机上以64位执行速度更快的原因可能有多种。我问您使用哪个CPU的原因是因为当64位CPU首次出现时,AMD和Intel具有不同的机制来处理64位代码。

处理器架构:

英特尔的CPU体系结构完全是64位的。为了执行32位代码,需要在执行之前将32位指令(在CPU内部)转换为64位指令。

AMD的CPU架构是在其32位架构之上构建64位。也就是说,它实际上是具有64位扩展的32位体系结构-没有代码转换过程。

显然这是几年前的事,所以我不知道技术是否/如何改变,但从本质上讲,您希望64位代码在64位计算机上性能更好,因为CPU能够以两倍的内存工作。每条指令的位数。

.NET JIT

有人争辩说,由于JIT编译器能够根据您的处理器体系结构优化代码的方式,.NET(以及Java等其他托管语言)的性能优于C ++。在这方面,您可能会发现JIT编译器正在使用64位体系结构中的某些功能,当以32位执行时,该功能可能不可用或需要解决方法。

注意:

除了考虑使用DoubleWrapperNullable<double>还是速记语法之外,您还可以考虑使用它们:double?-我很想看看这是否对您的测试有影响。

注2:似乎有人将我对IA-64的64位体系结构的评论混为一谈。为了澄清,在我的回答中,64位是指x86-64,而32位是指x86-32。这里没有引用IA-64!


4
好,那为什么要快26倍?在答案中找不到。
usr 2015年

2
我猜这是抖动差异,但仅此而已。
乔恩·汉娜

2
@seriesOne:我认为MSalters试图说您正在将IA-64与x86-64混合使用。(英特尔在其手册中还将IA-32e用于x86-64)。每个人的台式机CPU都是x86-64。Itanic几年前就沉没了,我认为它主要用于服务器,而不是工作站。实际上,Core2(第一个支持x86-64 long模式的P6系列CPU)在64bit模式下实际上有一些限制。例如,uop macro-fusion仅在32位模式下起作用。英特尔和AMD的做法相同:将其32位设计扩展到64位。
彼得·科德斯

1
@PeterCordes我在哪里提到IA-64?我知道Itanium CPU是完全不同的设计和指令集。标记为EPIC或显式并行指令计算的早期模型。我认为MSalters正在将64位和IA-64混合在一起。我的答案适用于x86-64体系结构-那里没有提及Itanium CPU系列的信息
Matthew Layton

2
@ series0ne:好的,那么关于Intel CPU为“纯64位”的段落完全是胡说八道。我以为您在考虑IA-64,因为那样您就不会完全错了。运行32位代码从来没有多余的翻译步骤。x86-> uop解码器只有两种相似的模式:x86和x86-64。英特尔在P4之上构建了64位P4。64位Core2在Core和Pentium M的基础上进行了许多其他体系结构改进,但诸如仅在32位模式下工作的宏融合之类的事情表明64位已被采用。(在设计过程中相当早,但仍然如此。)
Peter Cordes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.