【发布时间】:2022-01-07 11:49:43
【问题描述】:
(注意:问题底部有更新)
看看这个精简的 small_vector 基准测试:
#include <cstdlib>
#define NOINLINE __attribute__((noinline))
class Array {
public:
Array() : m_data(m_buffer), m_size(0) {}
~Array() {
// if (m_data != m_buffer) free(m_data);
}
void append(int v) {
if (m_size >= 3) expand();
m_data[m_size++] = v;
}
private:
int *m_data;
int m_size;
int m_buffer[3];
NOINLINE void expand() {}
};
NOINLINE
void add(Array &array) {
array.append(11);
array.append(22);
array.append(33);
}
int main() {
for (int i = 0; i < 1000000000; i++) {
Array array;
add(array);
}
}
(使用 NOINLINE 是因为编译器决定内联原始 small_vector 代码的方式)
如果这段代码是用 clang 11 编译的,如果我取消注释 ~Array 中的注释行,它会变得更快(注意,free 调用永远不会执行)。在我的机器(i7 8750)上,差异是 18%。在quick-bench.com 上,差异更小,为 5.3%。
我知道这是一个微基准测试,可能会发生疯狂的事情,但是:add 是一个 37 条指令、120 字节的代码,所以它不是那么小。并且在两个版本中都是一样的。唯一的区别是main,也没有什么不同,只是编译的循环有点不同。然而,性能差异很大,具有更多指令/分支的版本运行速度更快。如果我运行perf stat -d -d -d,我看不到任何可疑的东西(分支/缓存未命中没有显着差异,但insn/循环差异仍然很大:2.4 vs 3.12):
慢
3939.23 msec task-clock # 1.000 CPUs utilized 10 context-switches # 0.003 K/sec 0 cpu-migrations # 0.000 K/sec 107 page-faults # 0.027 K/sec 13345669446 cycles # 3.388 GHz (38.26%) 32029499558 instructions # 2.40 insn per cycle (45.98%) 6005787151 branches # 1524.610 M/sec (45.99%) 71062 branch-misses # 0.00% of all branches (46.09%) 6000238616 L1-dcache-loads # 1523.202 M/sec (46.20%) 180237 L1-dcache-load-misses # 0.00% of all L1-dcache accesses (46.30%) 35516 LLC-loads # 0.009 M/sec (30.87%) 13655 LLC-load-misses # 38.45% of all LL-cache accesses (30.87%) not supported L1-icache-loads 545548 L1-icache-load-misses (30.87%) 6003584439 dTLB-loads # 1524.051 M/sec (30.86%) 5290 dTLB-load-misses # 0.00% of all dTLB cache accesses (30.76%) 4583 iTLB-loads # 0.001 M/sec (30.65%) 4222 iTLB-load-misses # 92.12% of all iTLB cache accesses (30.55%) not supported L1-dcache-prefetches not supported L1-dcache-prefetch-misses 3.939756460 seconds time elapsed 3.939678000 seconds user 0.000000000 seconds sys
快速
3316.00 msec task-clock # 1.000 CPUs utilized 5 context-switches # 0.002 K/sec 0 cpu-migrations # 0.000 K/sec 110 page-faults # 0.033 K/sec 11235910328 cycles # 3.388 GHz (38.24%) 35013565821 instructions # 3.12 insn per cycle (45.96%) 7002622651 branches # 2111.770 M/sec (45.96%) 59596 branch-misses # 0.00% of all branches (46.02%) 7001546754 L1-dcache-loads # 2111.446 M/sec (46.14%) 143554 L1-dcache-load-misses # 0.00% of all L1-dcache accesses (46.26%) 20608 LLC-loads # 0.006 M/sec (30.88%) 3562 LLC-load-misses # 17.28% of all LL-cache accesses (30.88%) not supported L1-icache-loads 431694 L1-icache-load-misses (30.88%) 7003243717 dTLB-loads # 2111.958 M/sec (30.88%) 3296 dTLB-load-misses # 0.00% of all dTLB cache accesses (30.82%) 2836 iTLB-loads # 0.855 K/sec (30.70%) 3436 iTLB-load-misses # 121.16% of all iTLB cache accesses (30.58%) not supported L1-dcache-prefetches not supported L1-dcache-prefetch-misses 3.316414943 seconds time elapsed 3.312479000 seconds user 0.003995000 seconds sys
注意:如果我在main 中手动展开循环到此:
for (int i = 0; i < 250000000; i++) {
{Array array; add(array);}
{Array array; add(array);}
{Array array; add(array);}
{Array array; add(array);}
}
性能差异保持不变。
您知道造成这种差异的原因是什么吗?有什么提示可以检查perf 事件吗?
作为参考,这里是 asm 列表:
慢主
401190: 55 push rbp
401191: 41 56 push r14
401193: 53 push rbx
401194: 48 83 ec 20 sub rsp,0x20
401198: bd 00 ca 9a 3b mov ebp,0x3b9aca00
40119d: 4c 8d 74 24 14 lea r14,[rsp+0x14]
4011a2: 48 8d 5c 24 08 lea rbx,[rsp+0x8]
4011a7: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0]
4011ae: 00 00
4011b0: 4c 89 74 24 08 mov QWORD PTR [rsp+0x8],r14
4011b5: c7 44 24 10 00 00 00 mov DWORD PTR [rsp+0x10],0x0
4011bc: 00
4011bd: 48 89 df mov rdi,rbx
4011c0: e8 4b ff ff ff call 401110 <add(Array&)>
4011c5: 83 c5 ff add ebp,0xffffffff
4011c8: 75 e6 jne 4011b0 <main+0x20>
4011ca: 31 c0 xor eax,eax
4011cc: 48 83 c4 20 add rsp,0x20
4011d0: 5b pop rbx
4011d1: 41 5e pop r14
4011d3: 5d pop rbp
4011d4: c3 ret
快速主
4011b0: 55 push rbp
4011b1: 41 56 push r14
4011b3: 53 push rbx
4011b4: 48 83 ec 20 sub rsp,0x20
4011b8: bd 00 ca 9a 3b mov ebp,0x3b9aca00
4011bd: 48 8d 5c 24 14 lea rbx,[rsp+0x14]
4011c2: 4c 8d 74 24 08 lea r14,[rsp+0x8]
4011c7: eb 0c jmp 4011d5 <main+0x25>
4011c9: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]
4011d0: 83 c5 ff add ebp,0xffffffff
4011d3: 74 26 je 4011fb <main+0x4b>
4011d5: 48 89 5c 24 08 mov QWORD PTR [rsp+0x8],rbx
4011da: c7 44 24 10 00 00 00 mov DWORD PTR [rsp+0x10],0x0
4011e1: 00
4011e2: 4c 89 f7 mov rdi,r14
4011e5: e8 46 ff ff ff call 401130 <add(Array&)>
4011ea: 48 8b 7c 24 08 mov rdi,QWORD PTR [rsp+0x8]
4011ef: 48 39 df cmp rdi,rbx
4011f2: 74 dc je 4011d0 <main+0x20>
4011f4: e8 37 fe ff ff call 401030 <free@plt>
4011f9: eb d5 jmp 4011d0 <main+0x20>
4011fb: 31 c0 xor eax,eax
4011fd: 48 83 c4 20 add rsp,0x20
401201: 5b pop rbx
401202: 41 5e pop r14
401204: 5d pop rbp
401205: c3 ret
添加
(慢速和快速相同)
401130: 53 push rbx
401131: 48 89 fb mov rbx,rdi
401134: 8b 4f 08 mov ecx,DWORD PTR [rdi+0x8]
401137: 83 f9 03 cmp ecx,0x3
40113a: 7c 0b jl 401147 <add(Array&)+0x17>
40113c: 48 89 df mov rdi,rbx
40113f: e8 cc 00 00 00 call 401210 <Array::expand()>
401144: 8b 4b 08 mov ecx,DWORD PTR [rbx+0x8]
401147: 48 8b 03 mov rax,QWORD PTR [rbx]
40114a: 8d 51 01 lea edx,[rcx+0x1]
40114d: 89 53 08 mov DWORD PTR [rbx+0x8],edx
401150: 48 63 c9 movsxd rcx,ecx
401153: c7 04 88 0b 00 00 00 mov DWORD PTR [rax+rcx*4],0xb
40115a: 8b 4b 08 mov ecx,DWORD PTR [rbx+0x8]
40115d: 83 f9 03 cmp ecx,0x3
401160: 7c 0e jl 401170 <add(Array&)+0x40>
401162: 48 89 df mov rdi,rbx
401165: e8 a6 00 00 00 call 401210 <Array::expand()>
40116a: 8b 4b 08 mov ecx,DWORD PTR [rbx+0x8]
40116d: 48 8b 03 mov rax,QWORD PTR [rbx]
401170: 8d 51 01 lea edx,[rcx+0x1]
401173: 89 53 08 mov DWORD PTR [rbx+0x8],edx
401176: 48 63 c9 movsxd rcx,ecx
401179: c7 04 88 16 00 00 00 mov DWORD PTR [rax+rcx*4],0x16
401180: 8b 4b 08 mov ecx,DWORD PTR [rbx+0x8]
401183: 83 f9 03 cmp ecx,0x3
401186: 7c 0e jl 401196 <add(Array&)+0x66>
401188: 48 89 df mov rdi,rbx
40118b: e8 80 00 00 00 call 401210 <Array::expand()>
401190: 8b 4b 08 mov ecx,DWORD PTR [rbx+0x8]
401193: 48 8b 03 mov rax,QWORD PTR [rbx]
401196: 8d 51 01 lea edx,[rcx+0x1]
401199: 89 53 08 mov DWORD PTR [rbx+0x8],edx
40119c: 48 63 c9 movsxd rcx,ecx
40119f: c7 04 88 21 00 00 00 mov DWORD PTR [rax+rcx*4],0x21
4011a6: 5b pop rbx
4011a7: c3 ret
更新
我设法使差异更大,将main 更改为这个(这只是添加了malloc 和free 调用):
void *d;
int main() {
d = malloc(1);
for (int i = 0; i < 1000000000; i++) {
Array array;
add(array);
}
free(d);
}
有了这个main,慢版变得更慢了,慢版和快版的差距是~30%!
【问题讨论】:
-
您的 CPU 是 Skylake 系列,因此 JCC 错误可能是前端吞吐量较低的原因。值得检查
idq.mite_uops事件以查看uops_issued.any的很大一部分是否来自旧版解码而不是uop 缓存。如果是这样,请寻找任何跨越或触及 32 字节边界底部的分支。 32-byte aligned routine does not fit the uops cache。 (和/或尝试编译器/汇编器选项来避免问题:How can I mitigate the impact of the Intel jcc erratum on gcc?) -
@EOF: 1) 我更喜欢反汇编,因为它有 intel 语法,而且代码对齐也是可见的。 2)是的,(-03)。 3)我会检查 coz,谢谢,但我认为加速与更改有关(我不确定在这种情况下因果意味着什么,但我看到 coz 也使用这个术语,所以我想我会如果我阅读文档了解含义)。
-
gcc -masm=intel -S使用与objdump -d -Mintel相同的 GAS.intel_syntax noprefix。但是,是的,在这种情况下,反汇编实际上是好的,因为一些微架构效果与代码对齐有关。回复:“因果关系”:这只是一个英语/科学词,而不是基准术语。 “因果关系”的意思是“一件事是由另一件事引起的”,而不是“这些事情一起发生,由第三件事引起”。 (您可能听说过“相关性不是因果关系”;EOF 建议排除基准方法错误以实际检查因果关系)。 -
彼得,再次感谢! @EOF我不确定我们是否可以说加速是严格随意地与更改相关的。但是,尽管如此,我们有两个几乎相同的程序,它们应该以相似的速度运行。我很好奇速度差异的原因。
-
clang 10+ 支持
-mbranches-within-32B-boundaries。所以你可以试试快速排除JCC勘误。
标签: c++ performance assembly x86 x86-64