为什么32字节的循环对齐使代码更快?

时间:2017-07-25 09:14:16

标签: performance gcc clang x86-64 benchmarking

看看这段代码:

one.cpp:

bool test(int a, int b, int c, int d);

int main() {
        volatile int va = 1;
        volatile int vb = 2;
        volatile int vc = 3;
        volatile int vd = 4;

        int a = va;
        int b = vb;
        int c = vc;
        int d = vd;

        int s = 0;
        __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop");
        __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop");
        __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop");
        __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop");
        for (int i=0; i<2000000000; i++) {
                s += test(a, b, c, d);
        }

        return s;
}

two.cpp:

bool test(int a, int b, int c, int d) {
        // return a == d || b == d || c == d;
        return false;
}

one.cpp中有16 nop个。您可以对它们进行注释/解除注释,以更改循环的16到32之间的入口点的对齐。我已使用g++ one.cpp two.cpp -O3 -mtune=native编译它们。

以下是我的问题:

  1. 32对齐版本比16对齐版本更快。在Sandy Bridge,差异是20%;在Haswell,8%。为什么会有区别?
  2. 使用32对齐版本,代码在Sandy Bridge上运行速度相同,但返回语句在two.cpp中并不重要。我认为return false版本至少应该更快一点。但不,速度完全相同!
  3. 如果我从one.cpp中删除volatile,则代码变慢(Haswell:之前:~2.17秒,之后:~2.38秒)。这是为什么?但是,当循环对齐到32时,会发生。
  4. 32对齐版本更快的事实对我来说很奇怪,因为Intel® 64 and IA-32 Architectures Optimization Reference Manual说(第3-9页):

      

    汇编/编译器编码规则12.(M影响,H一般性)所有分支   目标应该是16字节对齐。

    另一个小问题:是否有任何技巧使此循环32对齐(因此其余代码可以继续使用16字节对齐)?

    注意:我已经尝试过编译器gcc 6,gcc 7和clang 3.9,结果相同。

    这里的代码是volatile(16/32对齐的代码相同,只是地址不同):

    0000000000000560 <main>:
     560:   41 57                   push   r15
     562:   41 56                   push   r14
     564:   41 55                   push   r13
     566:   41 54                   push   r12
     568:   55                      push   rbp
     569:   31 ed                   xor    ebp,ebp
     56b:   53                      push   rbx
     56c:   bb 00 94 35 77          mov    ebx,0x77359400
     571:   48 83 ec 18             sub    rsp,0x18
     575:   c7 04 24 01 00 00 00    mov    DWORD PTR [rsp],0x1
     57c:   c7 44 24 04 02 00 00    mov    DWORD PTR [rsp+0x4],0x2
     583:   00 
     584:   c7 44 24 08 03 00 00    mov    DWORD PTR [rsp+0x8],0x3
     58b:   00 
     58c:   c7 44 24 0c 04 00 00    mov    DWORD PTR [rsp+0xc],0x4
     593:   00 
     594:   44 8b 3c 24             mov    r15d,DWORD PTR [rsp]
     598:   44 8b 74 24 04          mov    r14d,DWORD PTR [rsp+0x4]
     59d:   44 8b 6c 24 08          mov    r13d,DWORD PTR [rsp+0x8]
     5a2:   44 8b 64 24 0c          mov    r12d,DWORD PTR [rsp+0xc]
     5a7:   0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]
     5ac:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
     5b3:   00 00 00 
     5b6:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
     5bd:   00 00 00 
     5c0:   44 89 e1                mov    ecx,r12d
     5c3:   44 89 ea                mov    edx,r13d
     5c6:   44 89 f6                mov    esi,r14d
     5c9:   44 89 ff                mov    edi,r15d
     5cc:   e8 4f 01 00 00          call   720 <test(int, int, int, int)>
     5d1:   0f b6 c0                movzx  eax,al
     5d4:   01 c5                   add    ebp,eax
     5d6:   83 eb 01                sub    ebx,0x1
     5d9:   75 e5                   jne    5c0 <main+0x60>
     5db:   48 83 c4 18             add    rsp,0x18
     5df:   89 e8                   mov    eax,ebp
     5e1:   5b                      pop    rbx
     5e2:   5d                      pop    rbp
     5e3:   41 5c                   pop    r12
     5e5:   41 5d                   pop    r13
     5e7:   41 5e                   pop    r14
     5e9:   41 5f                   pop    r15
     5eb:   c3                      ret    
     5ec:   0f 1f 40 00             nop    DWORD PTR [rax+0x0]
    

    没有不稳定:

    0000000000000560 <main>:
     560:   55                      push   rbp
     561:   31 ed                   xor    ebp,ebp
     563:   53                      push   rbx
     564:   bb 00 94 35 77          mov    ebx,0x77359400
     569:   48 83 ec 08             sub    rsp,0x8
     56d:   66 0f 1f 84 00 00 00    nop    WORD PTR [rax+rax*1+0x0]
     574:   00 00 
     576:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
     57d:   00 00 00 
     580:   b9 04 00 00 00          mov    ecx,0x4
     585:   ba 03 00 00 00          mov    edx,0x3
     58a:   be 02 00 00 00          mov    esi,0x2
     58f:   bf 01 00 00 00          mov    edi,0x1
     594:   e8 47 01 00 00          call   6e0 <test(int, int, int, int)>
     599:   0f b6 c0                movzx  eax,al
     59c:   01 c5                   add    ebp,eax
     59e:   83 eb 01                sub    ebx,0x1
     5a1:   75 dd                   jne    580 <main+0x20>
     5a3:   48 83 c4 08             add    rsp,0x8
     5a7:   89 e8                   mov    eax,ebp
     5a9:   5b                      pop    rbx
     5aa:   5d                      pop    rbp
     5ab:   c3                      ret    
     5ac:   0f 1f 40 00             nop    DWORD PTR [rax+0x0]
    

1 个答案:

答案 0 :(得分:3)

这不回答第2点(return a == d || b == d || c == d;return false的速度相同)。这仍然是一个可能有趣的问题,因为它必须编译多个uop-cache指令行。

  

32对齐版本更快的事实对我来说很奇怪,因为[英特尔的手册说要对齐32]

优化指南建议是一个非常一般的指导方针,而且 肯定意味着更大的从不帮助。通常它没有,填充到32比受到帮助更容易受伤。 (I-cache未命中,ITLB未命中,以及从磁盘加载更多代码字节)。

实际上,很少需要16B对齐,尤其是在具有uop缓存的CPU上。对于可以从循环缓冲区运行的小循环,它的对齐通常完全不相关。

16B仍然不错,作为一个广泛的建议,但它没有告诉你需要知道的一切,以了解几个特定的​​CPU上的一个特定情况。

编译器通常默认为对齐循环分支和函数入口点,但通常不对齐其他分支目标。执行NOP(和代码膨胀)的成本通常大于未对齐的非循环分支目标的可能成本。

代码对齐有一些直接影响和一些间接影响。直接影响包括Intel SnB系列上的uop缓存。例如,请参阅Branch alignment for loops involving micro-coded instructions on Intel SnB-family CPUs

Intel's optimization manual的另一部分详细介绍了uop缓存的工作原理:

  

2.3.2.2解码的ICache

     
      
  • 一种方式中的所有微操作(uop缓存行)代表在代码中静态连续的指令并且在其中具有其EIP   相同的32字节区域。 (我认为这意味着一条指令   延伸越过边界进入块的uop缓存   包含它的开始,而不是结束。跨越指令必须   去某个地方,并运行分支目标地址   指令是insn的开始,所以把它放进去是最有用的   该块的一行)。
  •   
  • 不能跨方式分割多微操作指令。
  •   
  • 打开MSROM的指令消耗整个方式。
  •   
  • 每条路最多允许两个分支。
  •   
  • 将一对宏融合指令保存为一个微操作。
  •   

另见Agner Fog's microarch guide。他补充说:

  
      
  • 无条件跳转或调用始终结束μop缓存行
  •   
  • 许多其他可能与此无关的东西。
  •   

另外,如果您的代码不适合uop缓存,则无法从循环缓冲区运行。

对齐的间接影响包括:

  • 更大/更小的代码大小(L1I缓存未命中,TLB)。与您的测试无关
  • 哪个分支在BTB(分支目标缓冲区)中相互别名。

  

如果我从one.cpp中删除volatile,代码会变慢。那是为什么?

较大的指令将最后一条指令推入32B边界的循环中:

 59e:   83 eb 01                sub    ebx,0x1
 5a1:   75 dd                   jne    580 <main+0x20>

因此,如果您没有从循环缓冲区(LSD)运行,那么在没有volatile的情况下,其中一个uop-cache获取周期只能获得1个uop。

如果sub / jne宏保险丝,这可能不适用。我认为只有穿越64B边界才能打破宏观融合。

另外,那些不是真正的地址。你有没有检查链接后的地址是什么?如果文本部分的对齐小于64B,则链接后可能存在64B边界。

对不起,我实际上没有对此进行测试,以便详细说明这个具体案例。关键是,当您在前端遇到像call / ret这样的东西瓶颈时,对齐变得很重要并且可能会非常复杂。未来所有指令的边界交叉是否受到影响。不要指望它很简单。如果你已经阅读了我的其他答案,你就会知道我通常不会说“它太复杂而无法完全解释”,但是对齐可能就是这样。

另见Code alignment in one object file is affecting the performance of a function in another object file

在您的情况下,请确保内联的小功能。 使用链接时优化,如果您的代码库在单独的.c文件中有任何重要的微小功能,而不是.h中可以内联的文件。或者将您的代码更改为他们在.h