首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么32字节上的循环对齐会使代码更快?

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

Stack Overflow用户
提问于 2017-07-25 09:14:16
回答 1查看 2.2K关注 0票数 16

看看这段代码:

one.cpp:

代码语言:javascript
复制
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:

代码语言:javascript
复制
bool test(int a, int b, int c, int d) {
        // return a == d || b == d || c == d;
        return false;
}

nop在one.cpp中有16种。您可以注释/分解它们以更改循环入口点在16到32之间的对齐方式。我用g++ one.cpp two.cpp -O3 -mtune=native编译了它们。

以下是我的问题:

  1. 32对齐的版本比16对齐的版本快.桑迪桥的差别是20%,哈斯韦尔的差距是8%。为什么会有区别?
  2. 在32对齐的版本中,代码在Sandy上运行的速度相同,哪条返回语句在two.cpp中并不重要。我认为return false版本应该更快,至少要快一点。但是不,完全一样的速度!
  3. 如果我从volatile中删除one.cpp s,代码就会变慢(Haswell:之前:~2.17秒,之后:~2.38秒)。为什么会这样呢?但是当循环对齐到32时,这个只会发生

32对齐版本速度更快,这对我来说很奇怪,因为Intel 64和IA-32架构优化参考手册说(第3-9页):

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

另一个小问题是:有什么诀窍可以让只使用这个循环32对齐(所以其他代码可以继续使用16字节对齐)?

注:我试过编译gcc 6,gcc 7和clang 3.9,结果是一样的。

下面是易失性的代码( 16/32对齐的代码相同,只是地址不同):

代码语言:javascript
复制
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]

不波动的:

代码语言:javascript
复制
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]
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2017-07-29 16:55:17

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

32对齐的版本更快,这对我来说很奇怪,因为英特尔的手册上写着要对齐到16。

这个优化指南建议是一个非常普遍的指导方针,而且肯定并不意味着更大的指南永远不会起作用。通常情况下不会,填充到32会更容易伤害而不是帮助。(i-缓存丢失,ITLB未命中,以及从磁盘加载更多的代码字节)。

事实上,16B对齐很少是必要的,特别是在带有uop缓存的CPU上。对于可以从循环缓冲区运行的小循环,它的对齐通常是完全不相关的。

(Skylake微码更新禁用循环缓冲区

另一个SKL错误解决方案创建了另一个更糟糕的代码对齐坑洞:我怎样才能减轻英特尔的误读对gcc的影响呢?

作为一个广泛的推荐,16B仍然不错,但它并没有告诉您您需要了解的所有知识,以了解几个特定CPU上的一个特定情况。

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

代码对齐有一些直接和间接的影响。其直接影响包括英特尔SnB家族的uop缓存。例如,请参见Intel SnB系列CPU上涉及微编码指令的循环的分支对齐

英特尔优化手册的另一部分详细介绍了uop缓存的工作方式:

2.3.2.2解码的ICache

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

另见阿格纳雾微拱导轨。他补充说:

  • 无条件跳转或调用总是以μop缓存行结尾。
  • 很多其他可能与此无关的东西。

另外,如果您的代码不适合uop缓存,它就不能从循环缓冲区中运行。

调整的间接影响包括:

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

如果我从volatile中删除one.cpp,代码就会变慢。为什么会这样呢?

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

代码语言:javascript
复制
 59e:   83 eb 01                sub    ebx,0x1
 5a1:   75 dd                   jne    580 <main+0x20>

因此,如果您不是从循环缓冲区(LSD)运行,那么如果没有volatile,一个uop缓存获取周期只能获得1个uop。

如果子/jne宏熔断器,这可能不适用。我认为只有跨越64B边界才能打破宏观融合。

而且那些不是真正的地址。你查过链接后的地址了吗?如果文本部分的对齐度小于64B,则链接后可能存在64B边界。

同样与32字节边界有关的是,JCC差错特值会禁用对分支(包括宏融合ALU+JCC)包含行的最后一个字节( Skylake )的块的uop缓存。我怎样才能减轻英特尔的误读对gcc的影响呢?

很抱歉,我还没有对这个具体案例做更多的测试。关键是,当您在前端瓶颈时,比如在一个紧密的循环中有一个call/ret,那么对齐就变得非常重要,并且可以得到非常复杂的。过境或非所有未来指示都会受到影响。不要指望它是简单的。如果你读过我的其他答案,你就会知道我通常不是那种会说“太复杂而无法充分解释”的人,但对齐可能是这样的。

另见一个对象文件中的代码对齐会影响另一个对象文件中函数的性能。

在您的情况下,确保微小的函数内联。如果您的代码库在单独的.c 文件中有任何重要的小函数,而不是在 .h 中,它们可以内联。或更改代码以将它们放入.h中,则使用链接时间优化。

票数 5
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/45298870

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档