看看这段代码:
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
编译它们。
以下是我的问题:
return false
版本至少应该更快一点。但不,速度完全相同!volatile
,则代码变慢(Haswell:之前:~2.17秒,之后:~2.38秒)。这是为什么?但是,当循环对齐到32时,仅会发生。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]
答案 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缓存,则无法从循环缓冲区运行。
对齐的间接影响包括:
如果我从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
。