imul
吞吐量瓶颈的影响。但是在Haswell/Skylake上,它以每11个周期运行一次,显然是因为setnz al
依赖于最后一个imul
。; synthetic micro-benchmark to test partial-register renaming
mov ecx, 1000000000
.loop: ; do{
imul eax, eax ; a dep chain with high latency but also high throughput
imul eax, eax
imul eax, eax
dec ecx ; set ZF, independent of old ZF. (Use sub ecx,1 on Silvermont/KNL or P4)
setnz al ; ****** Does this depend on RAX as well as ZF?
movzx eax, al
jnz .loop ; }while(ecx);
如果
setnz al
依赖于rax
,那么3ximul/setcc/movzx序列形成一个循环依赖链。如果不是,则每个setcc
/movzx
/3ximul
链都是独立的,从更新循环计数器的dec
中分叉出来。在HSW/SKL上每次迭代测量到的11c完全可以通过延迟瓶颈进行解释:3x3c(imul) + 1c(setcc的读取-修改-写入) + 1c(在同一寄存器内的movzx)。
离题:避免这些(有意的)瓶颈
我正在寻求可理解/可预测的行为来隔离部分寄存器,而不是追求最佳性能。
例如,xor
-零/设置标志/setcc
在任何情况下都更好(在本例中,xor eax,eax
/ dec ecx
/ setnz al
)。 这会打破所有 CPU 上对 eax 的依赖(除了早期 P6 家族如 PII 和 PIII),仍然避免了部分寄存器合并惩罚,并节省了 1c 的 movzx
延迟。 它还在处理寄存器重命名阶段的 xor 零化上比使用一个较少的 ALU uop。 请参见该链接以获取有关使用带有 setcc
的 xor 零化的更多信息。
请注意,AMD、Intel Silvermont/KNL 和 P4 根本不进行部分寄存器重命名。 它只是 Intel P6 家族 CPU 及其后代 Intel Sandybridge-family 的一个特性,但似乎正在逐步淘汰。
很不幸,gcc在(Godbolt编译器浏览器示例)中会使用cmp
/ setcc al
/ movzx eax,al
,而本可以使用xor
代替movzx
。然而,clang会使用xor-zero/cmp/setcc,除非你将多个布尔条件结合起来,例如count += (a==b) | (a==~b)
。
此外,我使用了movzx eax,al,它与mov-elimination一样无效,就像mov rax,rax一样。(IvB、HSW和SKL可以在0延迟下重命名movzx eax,bl,但Core2不能)。这使得Core2/SKL上的所有内容都相等,除了部分寄存器的行为。
Core2的行为与Agner Fog的微架构指南一致,但HSW/SKL的行为则不同。从Skylake的第11.10节开始,以前的Intel uarchs也是如此:
为了消除错误依赖关系,通用寄存器的不同部分可以存储在不同的临时寄存器中。
不幸的是,他没有时间为每个新的uarch进行详细的测试以重新测试假设,因此这种行为变化被忽略了。
Agner确实描述了一个合并的uop被插入(无需停顿)来处理Sandybridge到Skylake上的high8寄存器(AH / BH / CH / DH),并且在SnB上的low8 / low16。 (我很抱歉过去传播了错误的信息,并说Haswell可以免费合并AH。 我太快速地浏览了Agner的Haswell部分,没有注意到高8个寄存器的后面一段话。 如果您看到我在其他帖子上发表的错误评论,请告诉我,这样我就可以删除它们或添加更正。 我会尽力找到并编辑我回答过这些问题的答案。)
我的实际问题是:Skylake上的部分寄存器到底如何工作?
从IvyBridge到Skylake,包括high8额外延迟,所有东西都一样吗?
Intel的优化手册没有具体说明哪些CPU有什么假依赖关系(尽管它提到了一些CPU有这些关系),并且遗漏了像读取AH/BH/CH/DH(high8寄存器)一样的事情,即使它们没有被修改也会增加额外的延迟。
如果有任何P6家族(Core2/Nehalem)的行为不在Agner Fog的微架构指南中描述,那也很有趣,但我应该将这个问题的范围限制在Skylake或Sandybridge家族。
我的Skylake测试数据,是通过在一个运行1亿或10亿次迭代的小dec ebp/jnz
循环中放置%rep 4
短序列来获得的。我使用Linux perf
以与这里的答案相同的方式,在相同的硬件(桌面Skylake i7 6700k)上测量周期。
除非另有说明,否则每个指令都作为1个融合域uop运行,使用ALU执行端口。(使用ocperf.py stat -e ...,uops_issued.any,uops_executed.thread
进行测量)。这可以检测到(缺少)mov消除和额外合并uop。
"每周期4个"的情况是对无限展开情况的推断。循环开销占用了一些前端带宽,但任何优于每周期1个的情况都表明寄存器重命名避免了写后写输出依赖性,并且该uop在内部不被处理为读取修改写入。
仅写入AH:防止循环从回送缓冲区(也称为Loop Stream Detector(LSD))执行。在HSW上,lsd.uops的计数完全为0,在SKL上很小(约为1.8k),并且不随循环迭代次数而变化。可能这些计数来自某个内核代码。当循环从LSD运行时,lsd.uops ~= uops_issued,误差在测量噪声范围内。一些循环会在LSD或无LSD之间交替运行(例如,当它们可能无法适应uop缓存时,如果解码开始位置错误),但我在测试中没有遇到过这种情况。
- 重复的
mov ah, bh
和/或mov ah, bl
每个周期运行4次。它需要一个ALU uop,因此不能像mov eax, ebx
一样被消除。 - 重复的
mov ah, [rsi]
每个周期运行2次(加载吞吐量瓶颈)。 - 重复的
mov ah, 123
每个周期运行1次。(循环内的dep-breakingxor eax,eax
去除了瓶颈。) 重复的
setz ah
或setc ah
每个周期运行1次。(dep-breakingxor eax,eax
使其在setcc
和循环分支上瓶颈为p06吞吐量。)为什么使用通常会使用ALU执行单元的指令写入
ah
会对旧值产生虚假依赖,而mov r8,r/m8
不会(对于reg或memory src)?(那么mov r/m8,r8
呢?无论使用这两个操作码中的哪一个进行reg-reg移动都没有关系,对吗?)重复的
add ah, 123
每个周期运行1次,如预期。- 重复的
add dh,cl
每个周期运行1次。 - 重复的
add dh,dh
每个周期运行1次。 - 重复的
add dh,ch
每个周期运行0.5次。当[ABCD]H是“干净”的时候(在这种情况下,RCX根本没有被修改过),读取它们是特殊的。
inc eax
或mov eax,esi
。仅写入AL:这些循环确实从LSD运行:
uops_issue.any
〜=lsd.uops
。
- 重复的
mov al, bl
每个周期运行一次。偶尔的dep-breakingxor eax,eax
可以让OOO执行瓶颈在uop吞吐量上,而不是延迟。 - 重复的
mov al, [rsi]
以微融合ALU+load uop的形式每个周期运行1次。(uops_issued=4G + 循环开销,uops_executed=8G + 循环开销)。在一组4个之前进行dep-breakingxor eax,eax
可以让它瓶颈在每个时钟2个loads上。 - 重复的
mov al, 123
每个周期运行1次。 - 重复的
mov al, bh
每2个周期运行0.5次。(每2个周期1次)。读取[ABCD]H是特殊的。 xor eax,eax
+ 6xmov al,bh
+dec ebp/jnz
:每次迭代2c,瓶颈在前端每个时钟4个uops。- 重复的
add dl, ch
每2个周期运行0.5次。(每2个周期1次)。读取[ABCD]H显然会为dl
创建额外的延迟。 - 重复的
add dl, cl
每个周期运行1次。
我认为对一个低8位寄存器的写操作行为类似于RMW混合到完整寄存器中,就像add eax, 123
一样,但如果ah
是脏的,则不会触发合并。因此(除了忽略AH
合并外),它的行为与根本不执行部分寄存器重命名的CPU相同。看起来AL
从未与RAX
分别重命名过?
inc al
/inc ah
对可以并行运行。mov ecx, eax
如果ah
是"dirty",则插入合并uop,但实际的mov
被重命名。这是Agner Fog描述IvyBridge及更高版本的情况。- 重复的
movzx eax, ah
每2个周期运行一次。(在写入完整寄存器后读取高8个寄存器具有额外的延迟。) movzx ecx, al
具有零延迟,在HSW和SKL上不占用执行端口。(就像Agner Fog为IvyBridge描述的那样,但他说HSW不会重命名movzx)。movzx ecx, cl
具有1c延迟并占用执行端口。(mov-elimination从不适用于same,same
情况,仅适用于不同的体系结构寄存器之间。)每次迭代都插入合并uop的循环无法从LSD(循环缓冲区)运行?
我认为AL / AH / RAX与B *,C *,DL / DH / RDX没有任何特殊之处。我已经测试了一些其他寄存器中的部分寄存器(尽管我主要为了一致性而显示AL
/AH
),并且从未注意到任何区别。
相关: 部分标志位问题与部分寄存器问题不同。请参阅INC指令与ADD 1:有关shr r32,cl
的超怪异问题,请参见各个版本(甚至在Core2 / Nehalem上进行shr r32,2
:不要从除1以外的移位读取标志)。
另请参阅某些CPU上ADC / SBB和INC / DEC在紧密循环中的问题,了解adc
循环中部分标志位的情况。
mov al, 123
每个周期只运行1次,但是movl eax, 123
重复运行每个迭代需要4个周期?不用担心,这是因为mov al, 123
不具有依赖性破坏。 - Noah