gez*_*eza 12 performance benchmarking gcc x86-64 clang
看看这段代码:
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;
}
Run Code Online (Sandbox Code Playgroud)
two.cpp:
bool test(int a, int b, int c, int d) {
// return a == d || b == d || c == d;
return false;
}
Run Code Online (Sandbox Code Playgroud)
nopone.cpp中有16 秒.您可以对它们进行注释/取消注释,以更改循环在16到32之间的入口点的对齐.我已经使用它们编译它们g++ one.cpp two.cpp -O3 -mtune=native.
这是我的问题:
return false版本至少应该更快一点.但不,速度完全相同!volatile从one.cpp中删除s,代码变慢(Haswell:之前:~2.17秒,之后:~2.38秒).这是为什么?但这只会发生,当循环对齐到32时.32对齐版本更快的事实对我来说很奇怪,因为英特尔®64和IA-32架构优化参考手册说明(第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]
Run Code Online (Sandbox Code Playgroud)
没有不稳定:
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]
Run Code Online (Sandbox Code Playgroud)
这并不能回答第 2 点(return a == d || b == d || c == d;与return false)。这仍然是一个可能有趣的问题,因为它必须将多个指令行编译为微指令缓存。
\n\n32 对齐版本更快这一事实对我来说很奇怪,因为 [Intel 手册说要对齐到 16]
\n
优化指南建议是一个非常通用的指南,绝对并不意味着更大的值永远没有帮助。通常情况下不会,并且填充到 32 可能弊大于利。(I-cache 未命中、ITLB 未命中以及要从磁盘加载的更多代码字节)。
\n事实上,很少需要 16B 对齐,尤其是在具有 uop 缓存的 CPU 上。对于可以从循环缓冲区运行的小循环,其对齐通常完全无关。
\n(Skylake 微代码更新禁用了循环缓冲区,以解决部分寄存器 AH 合并错误 SKL150。这会给跨越 32 字节边界的微小循环带来问题,每 2 个时钟只运行一次迭代,而不是每个时钟运行一次迭代。您可能会从 Haswell 上的 6 uop 循环中获得1.5 个时钟,或者在具有较旧微代码的 SKL 上获得。直到 Ice Lake 才重新启用 LSD,在 Kaby/Coffee/Comet Lake 中损坏,它们与 SKL/SKX 的微架构相同。)
\n另一个 SKL 勘误解决方法造成了另一个更糟糕的代码对齐坑:如何减轻 Intel jcc 勘误对 gcc 的影响?
\n作为一个广泛的建议,16B 仍然不错,但它并没有告诉您了解几个特定 CPU 上的一个特定情况所需的一切。
\n编译器通常默认对齐循环分支和函数入口点,但通常不对齐其他分支目标。执行 NOP(和代码膨胀)的成本通常大于未对齐的非循环分支目标的可能成本。
\n代码对齐有一些直接和一些间接的影响。直接影响包括 Intel SnB 系列上的 uop 缓存。例如,请参阅涉及 Intel SnB 系列 CPU 上微编码指令的循环的分支对齐。
\nIntel 优化手册的另一部分详细介绍了 uop 缓存的工作原理:
\n\n\n2.3.2.2 解码后的 ICache:
\n\n
\n- Way(uop 缓存行)中的所有微操作表示在代码中静态连续的指令,并且其 EIP 在相同的对齐 32 字节区域内。(我认为这意味着\n延伸超过边界的指令进入包含其开始而不是结束的块的uop高速缓存。跨越指令必须\n到达某个地方,并且将运行\n指令的分支目标地址是insn 的开头,因此将其放入该块的行中最有用)。
\n- 多微操作指令不能跨路分割。
\n- 打开 MSROM 的指令会消耗整个 Way。
\n- 每路最多允许有两个分支。
\n- 一对宏融合指令保留为一个微操作。
\n
另请参阅Agner Fog 的微体系结构指南。他补充道:
\n\n\n\n
\n- 无条件跳转或调用总是结束 \xce\xbcop 缓存行
\n- 许多其他内容可能与这里无关。
\n
另外,如果您的代码不适合微指令缓存,它就无法从循环缓冲区运行。
\n对齐的间接影响包括:
\n\n\n如果我从 one.cpp 中删除
\nvolatiles,代码会变得更慢。这是为什么?
较大的指令将最后一条指令推入跨 32B 边界的循环中:
\n 59e: 83 eb 01 sub ebx,0x1\n 5a1: 75 dd jne 580 <main+0x20>\nRun Code Online (Sandbox Code Playgroud)\n因此,如果您不是从循环缓冲区 (LSD) 运行,则在没有volatileuop 缓存提取周期之一的情况下,仅获取 1 uop。
如果 sub/jne 宏融合,这可能不适用。我认为只有跨越 64B 边界才会破坏宏融合。
\n而且,这些都不是真实的地址。你检查过链接后的地址是什么吗?如果文本部分的对齐方式小于 64B,则链接后可能存在 64B 边界。
\n同样与 32 字节边界相关的是,JCC 勘误表会在 Skylake CPU 上禁用分支(包括宏融合 ALU+JCC)包含该行最后一个字节的块的 uop 缓存。\n 如何减轻以下影响gcc 上的 Intel jcc 勘误表?
\n抱歉,我还没有实际测试过这个具体案例。关键是,当您因诸如在紧密循环内有call/之类的东西而成为前端瓶颈时,对齐就变得非常重要并且可能变得极其复杂。是否跨越边界对于所有未来的指令都会受到影响。不要期望它很简单。如果您读过我的其他答案,您就会知道我通常不是那种会说“它太复杂而无法完全解释”的人,但对齐可以是这样。ret
另请参见一个目标文件中的代码对齐正在影响另一目标文件中函数的性能
\n在你的情况下,确保微小的函数内联。 如果您的代码库在单独的.c文件中而不是在.h可以内联的位置中具有任何重要的微小函数,请使用链接时优化。 或者更改您的代码以将它们放入.h.
| 归档时间: |
|
| 查看次数: |
735 次 |
| 最近记录: |