Performance 英特尔SnB系列CPU上涉及微代码指令的循环的分支对齐
这与这个问题相关,但不相同:与我之前的问题略有关联: 以下是一个非真实的测试用例。这种素性测试算法是不合理的。我怀疑任何现实世界的算法都不会多次执行如此小的内部循环(Performance 英特尔SnB系列CPU上涉及微代码指令的循环的分支对齐,performance,assembly,x86,intel,micro-optimization,Performance,Assembly,X86,Intel,Micro Optimization,这与这个问题相关,但不相同:与我之前的问题略有关联: 以下是一个非真实的测试用例。这种素性测试算法是不合理的。我怀疑任何现实世界的算法都不会多次执行如此小的内部循环(num是一个大约2**50大小的素数)。在C++11中: using nt = unsigned long long; bool is_prime_float(nt num) { for (nt n=2; n<=sqrt(num); ++n) { if ( (num%n)==0 ) { return fals
num
是一个大约2**50大小的素数)。在C++11中:
using nt = unsigned long long;
bool is_prime_float(nt num)
{
for (nt n=2; n<=sqrt(num); ++n) {
if ( (num%n)==0 ) { return false; }
}
return true;
}
我使用std::chrono::staid\u clock
计时。我不断得到奇怪的性能变化:仅仅添加或删除其他代码。我最终找到了一个对齐问题。命令.p2align 4,,10
试图对齐到2**4=16字节的边界,但最多只使用10字节的填充,我想这是为了平衡对齐和代码大小
我编写了一个Python脚本,用手动控制的nop
指令数替换.p2align 4,10
。以下散点图显示了20次运行中最快的15次,时间以秒为单位,x轴填充的字节数:
从无填充的objdump
中,pxor指令将在偏移量0x402f5f处出现。在笔记本电脑上运行,Sandybridge i5-3210m,turboboost禁用,我发现
- 对于0字节填充,性能较慢(0.42秒)
- 对于1-4字节的填充(偏移量0x402f60到0x402f63)稍微好一些(0.41s,在绘图上可见)
- 对于5-20字节的填充(偏移量0x402f64到0x402f73),可获得快速性能(0.37s)
- 对于21-32字节的填充(偏移量0x402f74到0x402f7f),性能较慢(0.42秒)
- 然后在32字节的样本上循环
通过将C++算法改为Cache <代码> SqRT(num)< /C> > 64位整数,然后使循环纯整数,我就解决了问题——对齐现在根本没什么区别。 您遇到的问题可能不是分支到对齐位置,尽管这仍然有帮助,但您当前的问题更可能是管道机制
当您一个接一个地编写两个说明时,例如: mov %eax, %ebx
add 1, %ebx
为了执行第二条指令,必须完成第一条指令。因此,编译器倾向于混合指令。假设您需要将%ecx
设置为零,您可以执行以下操作:
mov %eax, %ebx
xor %ecx, %ecx
add 1, %ebx
在这种情况下,mov
和xor
都可以并行执行。这让事情进展得更快。。。处理器之间可以并行处理的指令数量差别很大(Xeon通常更擅长于此)
分支添加了另一个参数,在该参数中,最好的处理器可以同时开始执行分支的两侧(true和false…)。但实际上,大多数处理器都会猜测,并希望他们是正确的
最后,很明显,将sqrt()
结果转换为整数将大大加快速度,因为您将避免SSE2代码的所有不合理之处。当这两条指令可以使用整数执行时,如果仅用于转换+比较,SSE2代码的速度肯定会减慢
现在。。。您可能还在想为什么对齐与整数无关。事实上,如果代码适合一级指令缓存,那么对齐就不重要了。如果您丢失了一级缓存,那么它必须重新加载代码,这就是对齐变得非常重要的地方,因为在每个循环上,它可能会加载无用的代码(可能是15字节的无用代码…)而且内存访问仍然非常慢。性能差异可以通过指令编码机制“看到”指令的不同方式来解释。CPU以块的形式读取指令(我相信是在core2 16字节上),并尝试给出不同的超标量单位微操作。如果指令在边界上或排序不太可能,那么一个核心中的单元很容易饿死。我没有具体的答案,只是一些我无法测试的不同假设(缺乏硬件)。我想我已经找到了一些结论性的东西,但我的对齐方式偏离了1(因为问题是从0x5F开始计算填充,而不是从对齐的边界)。无论如何,希望这篇文章能对描述可能在这里起作用的因素有所帮助 问题也没有指定分支的编码(短(2B)或近(6B))。这就留下了太多的可能性来研究和理论化到底是哪条指令跨越了32B边界导致了问题
我认为这要么是uop缓存中是否适合循环的问题,要么是与传统解码器的解码速度是否一致的问题。
显然,asm循环可以改进很多(例如,通过提升浮点值,更不用说使用完全不同的算法),但这不是问题所在。我们只是想知道为什么对齐对这个精确的循环很重要 您可能会认为,在除法上出现瓶颈的循环不会在前端出现瓶颈,也不会受到对齐的影响,因为除法很慢,并且循环每个时钟只运行很少的指令。的确如此,但在IvyBridge上,64位DIV被微编码为35-57微操作(UOP),因此可能存在前端问题。
mov %eax, %ebx
xor %ecx, %ecx
add 1, %ebx
# let's consider the case where this is 32B-aligned, so it runs in 0.41s
# i.e. this is at 0x402f60, instead of 0 like this objdump -Mintel -d output on a .o
# branch displacements are all 00, and I forgot to put in dummy labels, so they're using the rel32 encoding not rel8.
0000000000000000 <.text>:
0: 66 0f ef c0 pxor xmm0,xmm0 # 1 uop
4: f2 48 0f 2a c1 cvtsi2sd xmm0,rcx # 2 uops
9: 66 0f 2e f0 ucomisd xmm6,xmm0 # 2 uops
d: 0f 82 00 00 00 00 jb 0x13 # 1 uop (end of one uop cache line of 6 uops)
13: 31 d2 xor edx,edx # 1 uop
15: 48 89 d8 mov rax,rbx # 1 uop (end of a uop cache line: next insn doesn't fit)
18: 48 f7 f1 div rcx # microcoded: fills a whole uop cache line. (And generates 35-57 uops)
1b: 48 85 d2 test rdx,rdx ### PROBLEM!! only 3 uop cache lines can map to the same 32-byte block of x86 instructions.
# So the whole block has to be re-decoded by the legacy decoders every time, because it doesn't fit in the uop-cache
1e: 0f 84 00 00 00 00 je 0x24 ## spans a 32B boundary, so I think it goes with TEST in the line that includes the first byte. Should actually macro-fuse.
24: 48 83 c1 01 add rcx,0x1 # 1 uop
28: 79 d6 jns 0x0 # 1 uop
# branch displacements are still 32-bit, except the loop branch.
# This may not be accurate, since the question didn't give raw instruction dumps.
# the version with short jumps looks even more unlikely
0000000000000000 <loop_start-0x64>:
...
5c: 00 00 add BYTE PTR [rax],al
5e: 90 nop
5f: 90 nop
60: 90 nop # 4NOPs of padding is just enough to bust the uop cache before (instead of after) div, if they have to go in the uop cache.
# But that makes little sense, because looking backward should be impossible (insn start ambiguity), and we jump into the loop so the NOPs don't even run once.
61: 90 nop
62: 90 nop
63: 90 nop
0000000000000064 <loop_start>: #uops #decode in cycle A..E
64: 66 0f ef c0 pxor xmm0,xmm0 #1 A
68: f2 48 0f 2a c1 cvtsi2sd xmm0,rcx #2 B
6d: 66 0f 2e f0 ucomisd xmm6,xmm0 #2 C (crosses 16B boundary)
71: 0f 82 db 00 00 00 jb 152 #1 C
77: 31 d2 xor edx,edx #1 C
79: 48 89 d8 mov rax,rbx #1 C
7c: 48 f7 f1 div rcx #line D
# 64B boundary after the REX in next insn
7f: 48 85 d2 test rdx,rdx #1 E
82: 74 06 je 8a <loop_start+0x26>#1 E
84: 48 83 c1 01 add rcx,0x1 #1 E
88: 79 da jns 64 <loop_start>#1 E
.skip 0x5e
nop
# this is 0x5F
#nop # OP needed 1B of padding to reach a 32B boundary
.skip 5, 0x90
.globl loop_start
loop_start:
.L37:
pxor %xmm0, %xmm0
cvtsi2sdq %rcx, %xmm0
ucomisd %xmm0, %xmm6
jb .Loop_exit // Exit the loop
.L20:
xorl %edx, %edx
movq %rbx, %rax
divq %rcx
testq %rdx, %rdx
je .Lnot_prime // Failed divisibility test
addq $1, %rcx
jns .L37
.skip 200 # comment this to make the jumps rel8 instead of rel32
.Lnot_prime:
.Loop_exit:
LSD? <_start.L37>:
ab 1 4000a8: 66 0f ef c0 pxor xmm0,xmm0
ab 1 4000ac: f2 48 0f 2a c1 cvtsi2sd xmm0,rcx
ab 1 4000b1: 66 0f 2e f0 ucomisd xmm6,xmm0
ab 1 4000b5: 72 21 jb 4000d8 <_start.L36>
ab 2 4000b7: 31 d2 xor edx,edx
ab 2 4000b9: 48 89 d8 mov rax,rbx
ab 3 4000bc: 48 f7 f1 div rcx
!!!! 4000bf: 48 85 d2 test rdx,rdx
4000c2: 74 0d je 4000d1 <_start.L30>
4000c4: 48 83 c1 01 add rcx,0x1
4000c8: 79 de jns 4000a8 <_start.L37>
00000000004000a9 <_start.L37>:
ab 1 4000a9: 66 0f ef c0 pxor xmm0,xmm0
ab 1 4000ad: f2 48 0f 2a c1 cvtsi2sd xmm0,rcx
ab 1 4000b2: 66 0f 2e f0 ucomisd xmm6,xmm0
ab 1 4000b6: 72 21 jb 4000d9 <_start.L36>
ab 2 4000b8: 31 d2 xor edx,edx
ab 2 4000ba: 48 89 d8 mov rax,rbx
ab 3 4000bd: 48 f7 f1 div rcx
cd 1 4000c0: 48 85 d2 test rdx,rdx
cd 1 4000c3: 74 0d je 4000d2 <_start.L30>
cd 1 4000c5: 48 83 c1 01 add rcx,0x1
cd 1 4000c9: 79 de jns 4000a9 <_start.L37>
ALIGN 32
<add some nops here to swtich between DSB and MITE>
.top:
add r8, r9
xor eax, eax
div rbx
xor edx, edx
times 5 add eax, eax
dec rcx
jnz .top
00000000004000b2 <_start.L37>:
ab 1 4000b2: 66 0f ef c0 pxor xmm0,xmm0
ab 1 4000b6: f2 48 0f 2a c1 cvtsi2sd xmm0,rcx
ab 1 4000bb: 66 0f 2e f0 ucomisd xmm6,xmm0
ab 1 4000bf: 72 21 jb 4000e2 <_start.L36>
cd 1 4000c1: 31 d2 xor edx,edx
cd 1 4000c3: 48 89 d8 mov rax,rbx
cd 2 4000c6: 48 f7 f1 div rcx
cd 3 4000c9: 48 85 d2 test rdx,rdx
cd 3 4000cc: 74 0d je 4000db <_start.L30>
cd 3 4000ce: 48 83 c1 01 add rcx,0x1
cd 3 4000d2: 79 de jns 4000b2 <_start.L37>
00000000004000b3 <_start.L37>:
ab 1 4000b3: 66 0f ef c0 pxor xmm0,xmm0
ab 1 4000b7: f2 48 0f 2a c1 cvtsi2sd xmm0,rcx
ab 1 4000bc: 66 0f 2e f0 ucomisd xmm6,xmm0
cd 1 4000c0: 72 21 jb 4000e3 <_start.L36>
cd 1 4000c2: 31 d2 xor edx,edx
cd 1 4000c4: 48 89 d8 mov rax,rbx
cd 2 4000c7: 48 f7 f1 div rcx
cd 3 4000ca: 48 85 d2 test rdx,rdx
cd 3 4000cd: 74 0d je 4000dc <_start.L30>
cd 3 4000cf: 48 83 c1 01 add rcx,0x1
cd 3 4000d3: 79 de jns 4000b3 <_start.L37>
+----------------------------+----------+----------+----------+
| | Region 1 | Region 2 | Region 3 |
+----------------------------+----------+----------+----------+
| cycles: | 7.7e8 | 8.0e8 | 8.3e8 |
| uops_executed_stall_cycles | 18% | 24% | 23% |
| exe_activity_1_ports_util | 31% | 22% | 27% |
| exe_activity_2_ports_util | 29% | 31% | 28% |
| exe_activity_3_ports_util | 12% | 19% | 19% |
| exe_activity_4_ports_util | 10% | 4% | 3% |
+----------------------------+----------+----------+----------+
FE Frontend_Bound: 57.59 % [100.00%]
BAD Bad_Speculation: 0.01 %below [100.00%]
BE Backend_Bound: 0.11 %below [100.00%]
RET Retiring: 42.28 %below [100.00%]