c++ Trial-Division代码在Windows上以32位运行时比在Linux上以64位运行时快2倍

d7v8vwbk  于 2023-01-10  发布在  Windows
关注(0)|答案(3)|浏览(140)

我有一段代码在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;
}

所有这些测试都是在同一台机器上进行的,使用的是windows8和linux3.19.5(gcc4.9.2,clang3.5.0),linux和windows都是64位的。
这可能是什么原因?一些调度问题?

u5rb5r59

u5rb5r591#

你不会说windows/linux操作系统是32位还是64位。
在一台64位linux机器上,如果你把size_t改为int,你会发现linux上的执行时间下降到与windows相似的值。
size_t在win32上是一个int 32,在win 64上是一个int 64。
编辑:刚刚看到你的Windows拆卸。
您的Windows操作系统是32位的(或者至少您已经为32位编译过)。

r1wp621o

r1wp621o2#

size_t是Linux上x86-64 System V ABI中的一个64位无符号类型,您在其中编译64位二进制文件。(就像你在Windows上做的一样),它只有32位,因此试除循环只做32位除法。(size_t是针对C++对象的大小,而不是文件的大小,因此它只需要是指针宽度。)
在x86-64 Linux上,-m64是默认值,因为32位基本上被认为是过时的。要创建32位可执行文件,请使用g++ -m32

与大多数整数运算不同,Ice Lake之前英特尔CPU上的除法吞吐量(和延迟)取决于操作数大小:64位除法比32位除法慢。https://agner.org/optimize/https://uops.info/用于查看哪些端口的指令吞吐量/延迟/微指令表)。相关:代码评审问答:AMD没有这个问题(或者64位div/idiv不合理地慢),Intel Ice Lake使64位div/idiv的微指令数与32位相似。

与其他运算(如乘法,尤其是加法)相比,它的速度非常慢:你的程序完全在整数除法吞吐量上瓶颈,而不是在map操作上。(使用Skylake上32位二进制的性能计数器,arith.divider_active可统计24.03十亿个周期,其中除法执行单元处于活动状态,而总内核时钟周期数为24.84十亿。是的,没错,除法运算非常慢,所以只有一个性能计数器专门针对那个执行单元。它也是一个特殊情况,因为它不是完全流水线的,所以即使在这样的情况下,你有独立的除法运算,它不能像其它多周期运算(如FP或整数乘法)那样,在每个时钟周期启动一个新运算。)
不幸的是,g++无法基于数字是编译时常量的事实进行优化,因此范围有限法律的g++ -m64优化到div ecx而不是div rcx(并且有巨大的加速),这一改变使得64位二进制文件的运行速度与32位二进制文件一样快。(它的计算过程完全相同,只是没有那么多高0位,结果被隐式地零扩展以填充64位寄存器,而不是被除法器显式地计算为零,在这种情况下,这样做要快得多。)

我在Skylake上验证了这一点,方法是编辑二进制文件,将0x48雷克斯.W前缀替换为0x40,将div rcx更改为div ecx,并使用不执行任何操作的REX前缀。所用的总周期在g++ -O3 -m32 -march=native的32位二进制文件的1%以内。(还有时间,因为CPU在两次运行中碰巧以相同的时钟速度运行)(Godbolt编译器资源管理器上的g++7.3 asm输出)。
运行Linux的3.9GHz Skylake i7- 6700 k上的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

64位,雷克斯.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

原始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,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的吞吐量为div r32 = 1/13-39周期,div r64 = 13-70。我猜想,对于相同的实际数字,即使将它们分配给更宽寄存器中的除法器,也可能获得相同的性能。不像英特尔。(最坏情况上升是因为输入和结果的可能大小更大。)AMD整数除法仅为2uop,不像英特尔的在Skylake上被微编码为10或36 uops。(对于57 uops的带符号idiv r64甚至更高。)这可能与AMD在宽寄存器中对小数目的高效性有关。
顺便说一句,FP除法始终是单微指令,因为它在普通代码中对性能要求更高。(提示:在真实的生活中,如果他们关心性能的话,没有人会使用完全幼稚的试除来检查多个素数。

有序map的键是size_t,64位代码中的指针更大,使得每个红黑树节点明显更大,但这不是瓶颈

顺便说一句,与两个bool prime_low[Count], prime_high[Count]阵列相比,map<>是一个 * 糟糕 * 的选择:一个用于低Count元素,一个用于高Count元素。您有2个连续的范围,键可以通过位置隐式。或者至少使用std::unordered_map哈希表。我觉得有序版本应该称为ordered_map,和map = unordered_map,因为您经常看到使用map的代码没有利用排序。
您甚至可以使用std::vector<bool>来获取位图,使用1/8该高速缓存空间。
有一个“x32”ABI(长模式下的32位指针),它具有两个方面的优点,适用于不需要超过4G虚拟地址空间的进程:小指针可以提高数据密度/减少指针密集型数据结构中的缓存占用空间,但它具有现代调用约定、更多寄存器、基线SSE 2和64位整数寄存器(用于需要64位数学运算时)的优势。但遗憾的是,它并不是很受欢迎。它只是稍微快一点,所以大多数人不希望每个库都有第三个版本。

在这种情况下,您可以将源代码修复为使用unsigned int(或者uint32_t,如果你想移植到int只有16位的系统上)。或者uint_least32_t,以避免需要固定宽度的类型。你可以只对IsPrime的arg这样做,或者也可以对数据结构这样做。(但如果您正在优化,则键是根据数组中的位置隐式设置的,而不是显式设置的。)

您甚至可以创建一个具有64位循环和32位循环的IsPrime版本,后者根据输入的大小进行选择。

omjgkv6w

omjgkv6w3#

从编辑的问题中提取答案:
这是由于在Windows上构建32b二进制文件而不是在Linux上构建64b二进制文件造成的,以下是Windows的64b数字:

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

相关问题