在Windows上,试用版代码的运行速度比32位快32倍,而在Linux上则高于64位

时间:2015-05-01 07:56:26

标签: c++ performance x86 benchmarking 32bit-64bit

我有一段代码在Windows上运行速度比在Linux上快2倍。 以下是我测量的时间:

g++ -Ofast -march=native -m64
    29.1123
g++ -Ofast -march=native
    29.0497
clang++ -Ofast -march=native
    28.9192
visual studio 2013 Debug 32b
    13.8802
visual studio 2013 Release 32b
    12.5569

这似乎是一个太大的差异。

以下是代码:

#include <iostream>
#include <map>
#include <chrono>
static std::size_t Count = 1000;

static std::size_t MaxNum = 50000000;

bool IsPrime(std::size_t num)
{
    for (std::size_t i = 2; i < num; i++)
    {
        if (num % i == 0)
            return false;
    }
    return true;
}

int main()
{
    auto start = std::chrono::steady_clock::now();
    std::map<std::size_t, bool> value;
    for (std::size_t i = 0; i < Count; i++)
    {
        value[i] = IsPrime(i);
        value[MaxNum - i] = IsPrime(MaxNum - i);
    }
    std::chrono::duration<double> serialTime = std::chrono::steady_clock::now() - start;
    std::cout << "Serial time = " << serialTime.count() << std::endl;

    system("pause");
    return 0;
}

所有这些都是在Windows 8 vs linux 3.19.5(gcc 4.9.2,clang 3.5.0)的同一台机器上测得的。 linux和windows都是64位。

这可能是什么原因?一些调度问题?

3 个答案:

答案 0 :(得分:6)

您不会说Windows / Linux操作系统是32位还是64位。

在64位Linux机器上,如果你将size_t更改为int,你会发现linux上的执行时间下降到与windows相同的值。

size_t是win32上的int32,win64上的int64。

编辑:刚刚看到你的窗户拆卸了。

你的Windows操作系统是32位的(至少你已编译为32位)。

答案 1 :(得分:4)

size_t是Linux上的x86-64 System V ABI中的64位无符号类型,您将在其中编译64位二进制文​​件。但是在32位二进制文​​件中(就像您在Windows上所做的那样),它只有32位,因此,试验除法循环仅进行32位除法。 ({size_t是C ++对象的大小,不是文件的大小,因此只需要指针宽度即可。)

在x86-64 Linux上,默认为-m64,因为基本上认为32位已过时。要制作32位可执行文件,请使用g++ -m32


与大多数整数运算不同,现代x86 CPU上的除法吞吐量(和延迟)取决于操作数大小:64位除法比32位除法慢。https://agner.org/optimize/有关指令吞吐量/延迟/端口的指令表。

与乘法或特加等其他操作相比,它非常慢:您的程序完全瓶颈在整数除法吞吐量上,而不是map操作上。 (在Skylake上使用32位二进制的性能计数器,arith.divider_active在除24.03个核心时钟周期总数之外的24.84个十亿周期内计数了除法执行单元处于活动状态。是的,没错,除法速度太慢,以至于该执行单元只有一个性能计数器,这也是一种特殊情况,因为它没有完全流水线化,所以即使在这种情况下,如果您有独立的除法,也无法每个时钟开始一个新的除法器。循环,例如FP或整数乘法等其他多循环操作。)

不幸的是,

g ++无法基于数字是编译时常量,因此范围有限的事实而无法进行优化。 g++ -m64而不是div ecx优化到div rcx是合法的(并且是巨大的提速)。这一更改使64位二进制文​​件的运行速度与32位二进制文​​件的运行速度相同。 (它计算的是完全相同的东西,只是没有那么多的高零位。结果隐式地零扩展以填充64位寄存器,而不是由分频器显式地计算为零,在这种情况下这要快得多。)

我在Skylake上进行了验证,方法是编辑二进制文件,将0x48的{​​{1}} REX.W前缀替换为0x40 ,将div rcx更改为div ecx不执行REX前缀。占用的总周期距离g++ -O3 -m32 -march=native的32位二进制文​​件的1%以内。 (而且时间,因为CPU碰巧两次都以相同的时钟速度运行。)(g++7.3 asm output on the Godbolt compiler explorer。)

运行Linux的3.9GHz Skylake i7-6700k上的32位代码gcc7.3 -O3

$ cat > primes.cpp     # and paste your code, then edit to remove the silly system("pause")
$ g++ -Ofast -march=native -m32 primes.cpp -o prime32

$ taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread,arith.divider_active  ./prime32 
Serial time = 6.37695


 Performance counter stats for './prime32':
       6377.915381      task-clock (msec)         #    1.000 CPUs utilized          
                66      context-switches          #    0.010 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
               111      page-faults               #    0.017 K/sec                  
    24,843,147,246      cycles                    #    3.895 GHz                    
     6,209,323,281      branches                  #  973.566 M/sec                  
    24,846,631,255      instructions              #    1.00  insn per cycle         
    49,663,976,413      uops_issued.any           # 7786.867 M/sec                  
    40,368,420,246      uops_executed.thread      # 6329.407 M/sec                  
    24,026,890,696      arith.divider_active      # 3767.201 M/sec                  

       6.378365398 seconds time elapsed

vs。 64位,REX.W = 0(手工编辑的二进制文件)

 Performance counter stats for './prime64.div32':

       6399.385863      task-clock (msec)         #    1.000 CPUs utilized          
                69      context-switches          #    0.011 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
               146      page-faults               #    0.023 K/sec                  
    24,938,804,081      cycles                    #    3.897 GHz                    
     6,209,114,782      branches                  #  970.267 M/sec                  
    24,845,723,992      instructions              #    1.00  insn per cycle         
    49,662,777,865      uops_issued.any           # 7760.554 M/sec                  
    40,366,734,518      uops_executed.thread      # 6307.908 M/sec                  
    24,045,288,378      arith.divider_active      # 3757.437 M/sec                  

       6.399836443 seconds time elapsed

vs。 原始的64位二进制文​​件

$ g++ -Ofast -march=native primes.cpp -o prime64
$ taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread,arith.divider_active  ./prime64
Serial time = 20.1916

 Performance counter stats for './prime64':

      20193.891072      task-clock (msec)         #    1.000 CPUs utilized          
                48      context-switches          #    0.002 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
               148      page-faults               #    0.007 K/sec                  
    78,733,701,858      cycles                    #    3.899 GHz                    
     6,225,969,960      branches                  #  308.310 M/sec                  
    24,930,415,081      instructions              #    0.32  insn per cycle         
   127,285,602,089      uops_issued.any           # 6303.174 M/sec                  
   111,797,662,287      uops_executed.thread      # 5536.212 M/sec                  
    27,904,367,637      arith.divider_active      # 1381.822 M/sec                  

      20.193208642 seconds time elapsed

IDK为什么arith.divider_active的性能指标没有增加更多。 div 64div r32更具优势,因此可能会损害乱序执行并减少周围代码的重叠。但是我们知道,没有其他指令的背对背div的性能差异也差不多。

无论如何,这段代码将大部分时间都花在了那个可怕的除法循环中(该循环检查每个奇数和偶数除数,即使我们已经在检查了低位之后就已经排除了所有偶数除数... 并且它一直检查直到num而不是sqrt(num),所以对于很大的素数来说,非常慢。)

根据perf record,在 2nd 试用区循环中触发了99.98%的cpu周期事件,其中一个MaxNum - i,所以div仍然整个瓶颈,而且只是性能计数器的一个怪癖,并非所有时间都记录为arith.divider_active

  3.92 │1e8:   mov    rax,rbp
  0.02 │       xor    edx,edx
 95.99 │       div    rcx
  0.05 │       test   rdx,rdx 
       │     ↓ je     238     
  ... loop counter logic to increment rcx

从Agner Fog关于Skylake的说明表中:

           uops    uops      ports          latency     recip tput
           fused   unfused
DIV r32     10     10       p0 p1 p5 p6     26           6
DIV r64     36     36       p0 p1 p5 p6     35-88        21-83

({div r64本身实际上是取决于其输入的实际大小,较小的输入会更快。真正慢的情况是很大的商IIRC。当RDX:RAX中128位除数的上半部分非零时,它也会变慢。C编译器通常只将divrdx=0一起使用。)

周期计数的比率(78733701858 / 24938804081 = ~3.15)实际上小于最佳情况吞吐量的比率(21/6 = 3.5)。这应该是纯吞吐量瓶颈,而不是延迟,因为下一个循环迭代可以开始而无需等待最后的除法结果。 (感谢分支预测+投机执行。)在该划分循环中可能会有一些分支未命中。

如果仅发现2倍的性能比,则说明您拥有不同的CPU。可能是Haswell,其中32位div的吞吐量为9-11个周期,而64位div的吞吐量为21-74。

可能不是AMD:即使对于div r64,最佳情况下的吞吐量仍然很小。例如Steamroller每13-39个周期的div r32吞吐量= 1,并且div r64 = 13-70。我猜想,与英特尔不同,即使使用相同的实际数字,即使将它们分配给更宽寄存器的分频器,您也可能会获得相同的性能。 (最坏的情况是上升,因为可能的输入大小和结果都更大。)AMD整数除法仅为2 ups,这与Intel在Skylake上被微编码为10或36 ups的微分不同。 (对于签名为idiv r64的{​​57},甚至更多。)这可能与AMD对宽寄存器中的少量数字有效有关。

顺便说一句,FP划分始终是单UOP,因为在常规代码中它对性能的要求更高。 (提示:在现实生活中,如果他们根本不在乎性能的话,没有人会使用天真的试法来检查多个素数。筛子或其他东西。)


有序map的键是size_t,并且指针在64位代码中更大,这使得每个红黑树节点都明显更大,但这不是瓶颈< / strong>。

顺便说一句,map<>是一个糟糕的选择,与bool prime_low[Count], prime_high[Count]的两个数组相比:一个数组用于低Count元素,而一个数组用于高{{ 1}}。您有2个连续范围,该键可以按位置隐式显示。或至少使用Count哈希表。我觉得订购的版本应该被称为std::unordered_mapordered_map,因为您经常在不使用订购的情况下使用map = unordered_map来查看代码。

您甚至可以使用map来获取位图,其占用的缓存空间为1/8。

有一个“ x32” ABI(长模式下的32位指针),对于不需要超过4G虚拟地址空间的进程而言,它兼具两全其美的优点:小指针可实现更高的数据密度/较小的缓存指针密集型数据结构中的占位面积,但是现代调用约定的优点是,当您确实需要64位数学运算时,更多的寄存器,基准SSE2和64位整数寄存器就可以使用。但不幸的是,它不是很流行。它只是快一点,所以大多数人不想要每个库的第三个版本。

在这种情况下,您可以将源固定为使用std::vector<bool> (如果希望将其移植到unsigned int仅16个的系统上,则可以使用uint32_t位)。或int避免要求使用固定宽度类型。您只能对uint_least32_t的arg或数据结构进行此操作。 (但是,如果要进行优化,则键是按数组中的位置隐式而不是显式的。)

您甚至可以制作一个IsPrime版本,该版本具有64位循环和32位循环,并根据输入的大小进行选择。

答案 2 :(得分:1)

从已编辑的问题中提取答案:

这是由于在Windows上构建32b二进制文件而不是在Linux上构建64b二进制文件引起的,这是Windows的64b数字:

Visual studio 2013 Debug 64b
    29.1985
Visual studio 2013 Release 64b
    29.7469