此循环在Intel Conroe / Merom上每3个周期运行一次,按预期方式在 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
/ 3x imul
链都是独立的,与更新循环计数器的 dec
分开 . 在HSW / SKL上测量的每次迭代11c完全由延迟瓶颈解释:3x3c(imul)1c(由setcc读取 - 修改 - 写入)1c(movzx在同一寄存器中) .
Off topic: avoiding these (intentional) bottlenecks
我想采用可理解/可预测的行为来隔离部分注册的东西,而不是最佳性能 .
例如, xor
-zero / set-flags / setcc
无论如何都更好(在这种情况下, xor eax,eax
/ dec ecx
/ setnz al
) . 这打破了所有CPU上的eax(除了像PII和PIII这样的早期P6系列),仍然避免了部分寄存器合并处罚,并节省了1c的 movzx
延迟 . 它还在CPU上使用了少量ALU uop handle xor-zeroing in the register-rename stage . 有关使用 setcc
进行xor-zeroing的更多信息,请参阅该链接 .
请注意,AMD,Intel Silvermont / KNL和P4根本不进行部分寄存器重命名 . 它只是英特尔P6系列CPU及其后代英特尔Sandybridge系列中的一项功能,但似乎已逐步淘汰 .
不幸的是,gcc确实倾向于使用 cmp
/ setcc al
/ movzx eax,al
,它可以使用 xor
而不是 movzx
(Godbolt compiler-explorer example),而clang使用xor-zero / cmp / setcc,除非你结合多个布尔条件,如 count += (a==b) | (a==~b)
.
xor / dec / setnz版本在Skylake,Haswell和Core2上每次迭代运行3.0c(瓶颈在 imul
吞吐量上) . xor
-zeroing打破了对除PPro / PII / PIII /早期Pentium-M以外的所有无序CPU的旧值 eax
的依赖性(它仍然避免了部分注册合并处罚,但没有打破dep ) . Agner Fog's microarch guide describes this . 用 mov eax,0
替换xor-zeroing将其降低到Core2上每4.78个周期减1:当 imul
在 setnz al
之后读取 eax
时2-3c stall (in the front-end?) to insert a partial-reg merging uop .
另外,我使用 movzx eax, al
,它击败了mov-elimination,就像 mov rax,rax
一样 . (IvB,HSW和SKL可以使用0延迟重命名 movzx eax, bl
,但Core2不能) . 除了部分寄存器行为之外,这使得Core2 / SKL上的所有内容都相同 .
Core2行为与Agner Fog's microarch guide一致,但HSW / SKL行为不符合 . 从第11.10节到Skylake,以及之前的英特尔搜索:
通用寄存器的不同部分可以存储在不同的临时寄存器中,以消除错误依赖 .
遗憾的是,他没有时间对每个新的uarch进行详细测试以重新测试假设,因此这种行为的变化从裂缝中滑落 .
Agner确实描述了通过Skylake在Sandybridge上插入high8寄存器(AH / BH / CH / DH)以及SnB上的low8 / low16插入(不停止)合并uop . (遗憾的是,我过去一直散布错误的信息,并说Haswell可以免费合并AH . 我过快地浏览了Agner的Haswell部分,并且没有注意到后面关于high8寄存器的段落 . 如果你看到,请告诉我 . 我对其他帖子的错误评论,所以我可以删除它们或添加更正 . 我会尝试至少找到并编辑我的答案,我已经说过了 . )
我的实际问题: How exactly do partial registers really behave on Skylake?
Is everything the same from IvyBridge to Skylake, including the high8 extra latency?
Intel's optimization manual并不具体说明哪些CPU具有错误依赖性(虽然它确实提到某些CPU具有它们),并且省略了诸如读取AH / BH / CH / DH(high8寄存器)之类的东西,即使它们没有被修改了 .
如果有任何P6家族(Core2 / Nehalem)行为,Agner Fog的微观指南没有描述,那也会很有趣,但我应该将这个问题的范围限制在Skylake或Sandybridge家族 .
My Skylake test data ,将 %rep 4
短序列放入一个运行100M或1G迭代的小型 dec ebp/jnz
循环中 . 我使用Linux perf
以与in my answer here相同的方式在相同的硬件(桌面Skylake i7 6700k)上测量周期 .
除非另有说明,否则每条指令使用ALU作为1个融合域uop运行执行端口 . (用ocperf.py stat -e ...,uops_issued.any,uops_executed.thread测量) . 这检测到(没有)mov-elimination和额外的合并uops .
"4 per cycle"案例是对无限展开案例的推断 . 循环开销占用了一些前端带宽,但是每个周期优于1的任何东西都表明寄存器重命名避免了write-after-write output dependency,并且uop在内部不作为读 - 修改 - 写处理 .
Writing to AH only :阻止循环从环回缓冲区(又称循环流检测器(LSD))执行 . lsd.uops
的计数在HSW上正好为0,在SKL上为小(约为1.8k),并且不随循环迭代计数而缩放 . 可能这些计数来自某些内核代码 . 当循环从LSD运行时, lsd.uops ~= uops_issued
到测量噪声内 . 一些循环在LSD或no-LSD之间交替(例如,如果解码在错误的地方开始,它们可能不适合uop缓存),但是在测试时我没有碰到它 .
-
重复
mov ah, bh
和/或mov ah, bl
每循环运行4次 . 它需要一个ALU uop,所以它不像mov eax, ebx
那样被淘汰 . -
重复
mov ah, [rsi]
每周期运行2次(负载吞吐量瓶颈) . -
重复
mov ah, 123
每循环运行1次 . (循环中的dep-breaking xor eax,eax消除了瓶颈 . ) -
重复
setz ah
或setc ah
每循环运行1次 . (破解xor eax,eax
让它成为setcc
和循环分支的p06吞吐量的瓶颈 . )
Why does writing ah with an instruction that would normally use an ALU execution unit have a false dependency on the old value, while mov r8, r/m8 doesn't (for reg or 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是特殊的,因为它们是"clean"(在这种情况下,RCX最近没有被修改) .
Terminology :所有这些离开AH(或DH)“ dirty ", i.e. in need of merging (with a merging uop) when the rest of the register is read (or in some other cases). i.e. that AH is renamed separately from RAX, if I'm understanding this correctly. " clean ”是相反的 . 有很多方法可以清理脏寄存器,最简单的方法是 inc eax
或 mov eax, esi
.
Writing to AL only :这些循环确实从LSD运行: uops_issue.any
~ = lsd.uops
.
-
重复
mov al, bl
每循环运行1次 . 每个组偶尔会发生一次破坏xor eax,eax
,这使得OOO执行瓶颈的uop吞吐量,而不是延迟 . -
重复
mov al, [rsi]
每循环运行1次,作为微融合ALU加载uop . (uops_issued = 4G循环开销,uops_executed = 8G循环开销) . 在一组4之前,一个破坏性的xor eax,eax
使每个时钟的2个负载成为瓶颈 . -
重复
mov al, 123
每循环运行1次 . -
重复
mov al, bh
每循环运行0.5次 . (每2个循环1个) . 阅读[ABCD] H很特别 . -
xor eax,eax
6xmov al,bh
dec ebp/jnz
:每个2c,前端每个时钟4个uop的瓶颈 . -
重复
add dl, ch
每循环运行0.5次 . (每2个循环1个) . 读[ABCD] H显然会为dl
创造额外的延迟 . -
重复
add dl, cl
每循环运行1次 .
我认为对低8寄存器的写操作就像RMW混合到完整的reg中一样,就像 add eax, 123
一样,但如果 ah
是脏的,它不会触发合并 . 因此(除了忽略 AH
合并),它的行为与完全不进行部分注册重命名的CPU的行为相同 . 似乎 AL
永远不会与 RAX
分开重命名?
-
inc al
/inc ah
对可以并行运行 . -
mov ecx, eax
如果ah
是"dirty",则插入合并的uop,但重命名实际的mov
. 这就是IvyBridge及其后的Agner Fog describes . -
重复
movzx eax, ah
每2个循环运行一次 . (在写完整个reg之后读取高8位寄存器会产生额外的延迟 . ) -
movzx ecx, al
具有零延迟,并且没有重命名movzx) . -
movzx ecx, cl
具有1c延迟并占用执行端口 . (mov-elimination never works for the same,same case,仅在不同的架构寄存器之间 . )
每次迭代插入合并uop的循环都无法从LSD(循环缓冲区)运行?
我对AL / AH / RAX与B *,C *,DL / DH / RDX有什么特别之处 . 我已经在其他寄存器中测试了一些部分寄存器(尽管为了一致性我主要显示 AL
/ AH
),并且从未发现任何差异 .
How can we explain all of these observations with a sensible model of how the microarch works internally?
相关:部分 flag 问题与部分 register 问题不同 . 有关INC instruction vs ADD 1: Does it matter?的一些非常奇怪的东西,请参阅INC instruction vs ADD 1: Does it matter?(在Core2 / Nehalem上甚至是 shr r32,2
:不要读取除1之外的移位标记) .
有关 adc
中的部分标记内容,另请参阅Problems with ADC/SBB and INC/DEC in tight loops on some CPUs循环 .
1 回答
其他答案欢迎更详细地介绍Sandybridge和IvyBridge . 我无法访问该硬件 .
我没有发现HSW和SKL之间存在任何部分注册行为差异 . 在Haswell和Skylake上,到目前为止我测试的所有内容都支持这个模型:
AL is never renamed separately from RAX (或r15的r15b) . 因此,如果您从未触摸过high8寄存器(AH / BH / CH / DH),那么一切都与没有部分注册重命名的CPU(例如AMD)完全相同 .
对AL的只写访问权限合并到RAX中,并依赖于RAX . 对于加载到AL的负载,这是一个微融合的ALU加载uop,它在p0156上执行,这是它在每次写入时真正合并的最有力的证据之一,而不仅仅是像Agner推测的那样进行一些花哨的双重记录 .
Agner(和英特尔)表示,Sandybridge可能需要为AL合并uop,因此它可能会与RAX分开重命名 . 对于SnB,Intel's optimization manual (section 3.5.2.4 Partial Register Stalls)说
我认为他们说在SnB上,
add al,bl
将RMW完整的RAX而不是单独重命名,因为其中一个源寄存器是(部分)RAX . 我的猜测是,这不适用于像mov al, [rbx + rax]
这样的负载;在寻址模式下rax
可能不算作源 .我还没有测试过high8合并uops是否仍然需要在HSW / SKL上自行发布/重命名 . 这将使前端影响相当于4 uops(因为这是问题/重命名管道宽度) .
如果不编写EAX / RAX,就无法打破涉及AL的依赖关系 .
xor al,al
没有帮助,mov al, 0
也没有 .movzx ebx, al has zero latency (renamed), and needs no execution unit. (即,移动消除适用于HSW和SKL) . It triggers merging of AH if it's dirty ,我想这是没有ALU工作所必需的 . 它是微型导游在这里有一个错误,他说在HSW或SKL上没有消除零扩展移动,只有IvB . )
movzx eax, al
在重命名时未被删除 . 英特尔的mov-elimination永远不会同样适用 .mov rax,rax
isn 't eliminated either, even though it doesn' t必须对任何事物进行零扩展 . (虽然'd be no point to giving it special hardware support, because it'只是一个无操作,不像mov eax,eax
) . 无论如何,当零扩展时,更喜欢在两个独立的架构寄存器之间移动,无论是32位mov
还是8位movzx
.在HSW或SKL重命名时,
movzx eax, bx
未被删除 . 它具有1c延迟并使用ALU uop . 英特尔的优化手册仅提到了8位movzx的零延迟(并指出movzx r32, high8
永远不会重命名) .High-8 regs可以与寄存器的其余部分分开重命名,并且需要合并uop .
对
ah
的只读访问权限mov ah, r8
或mov ah, [mem]
重命名AH,不依赖于旧值 . 这些都是通常不需要ALU uop的指令(对于32位版本) .AH的RMW(如
inc ah
)弄脏了它 .setcc ah
取决于旧的ah
,但仍然会弄脏它 . 我认为mov ah, imm8
是相同的,但没有测试过多的角落情况 .(原因不明:涉及
setcc ah
的循环有时可以从LSD运行,请参阅本文末尾的rcr
循环 . 也许只要ah
在循环结束时是干净的,它可以使用LSD吗?) .如果
ah
是脏的,setcc ah
将合并到重命名的ah
中,而不是强制合并到rax
. 例如%rep 4
(inc al
/test ebx,ebx
/setcc ah
/inc al
/inc ah
)不会生成合并的uops,并且仅在大约8.7c内运行(由于ah
的uops资源冲突而导致延迟8inc al
减速 . 此外inc ah
/setcc ah
dep链) .我认为这里发生的事情是
setcc r8
总是被实现为读 - 修改 - 写 . 英特尔可能认为不值得使用只写setcc
uop来优化setcc ah
情况,因为编译器生成的代码非常罕见setcc ah
. (但请参阅问题中的godbolt链接:clang4.0 with-m32
会这样做 . )读取AX,EAX或RAX会触发合并uop(占用前端问题/重命名带宽) . 可能RAT(寄存器分配表)跟踪架构R [ABCD] X的高8脏状态,甚至在写入AH退出之后,AH数据也存储在与RAX不同的物理寄存器中 . 即使在编写AH和读取EAX之间有256个NOP,也有一个额外的合并uop . (SKL上的ROB大小= 224,因此这可以保证
mov ah, 123
已经退役) . 使用uops_issued /执行的perf计数器检测到,这清楚地显示了差异 .AL的读 - 修改 - 写(例如
inc al
)免费合并,作为ALU uop的一部分 . (仅使用一些简单的uops进行测试,例如add
/inc
,而不是div r8
或mul r8
) . 同样,即使AH很脏,也不会触发合并的uop .只写EAX / RAX(如
lea eax, [rsi + rcx]
或xor eax,eax)清除AH脏状态(无合并uop) .只写AX(
mov ax, 1
)首先触发AH的合并 . 我想这不是特殊套管,而是像任何其他RMW AX / RAX一样运行 . (TODO:测试mov ax, bx
,虽然不应该重命名't be special because it' . )xor ah,ah
具有1c延迟,不是dep-breaking,仍然需要执行端口 .读取和/或写入AL不会强制合并,因此AH可以保持脏(并且可以在单独的dep链中独立使用) . (例如
add ah, cl
/add al, dl
可以每时钟运行1次(加密等待时间瓶颈) .Making AH dirty prevents a loop from running from the LSD (循环缓冲区),即使没有合并的uops . LSD是指CPU在队列中循环uops以提供问题/重命名阶段 . (称为IDQ) .
插入合并的uops有点像为堆栈引擎插入堆栈同步uops . 英特尔's optimization manual says that SnB'的LSD无法运行具有不匹配的循环
push
/pop
,这是有道理的,但它意味着它可以运行具有 balancerpush
/pop
的循环 . 那个's not what I'在SKL上看到:甚至 balancerpush
/pop
阻止从LSD运行(例如push rax
/pop rdx
/times 6 imul rax, rdx
. (SnB的LSD和HSW / SKL之间可能存在真正的区别:SnB may just "lock down" the uops in the IDQ instead of repeating them multiple times, so a 5-uop loop takes 2 cycles to issue instead of 1.25 . )无论如何,似乎HSW /当高8寄存器变脏或者包含堆栈引擎微操作时,SKL不能使用LSD .此行为可能与an erratum in SKL有关:
这也可能与英特尔的优化手册声明有关,即SnB至少必须在一个循环中自行发布/重命名AH合并uop . 这对于前端而言是一个奇怪的区别 .
我的Linux内核日志说
microcode: sig=0x506e3, pf=0x2, revision=0x84
. Arch Linux的intel-ucode
包只提供更新,you have to edit config files to actually have it loaded . 所以 my Skylake testing was on an i7-6700k with microcode revision 0x84, which doesn't include the fix for SKL150 . 在我测试的每一个案例中,它都符合Haswell的行为,IIRC . (例如,Haswell和我的SKL都可以从LSD运行setne ah
/add ah,ah
/rcr ebx,1
/mov eax,ebx
循环) . 我启用了HT(这是SKL150显示的前提条件),但我正在测试一个主要是空闲的系统,所以我的线程有自己的核心 .使用更新的微码,LSD完全禁用所有时间,而不仅仅是部分寄存器处于活动状态 .
lsd.uops
始终为零,包括真正的程序而不是合成循环 . 硬件错误(而不是微码错误)通常需要禁用整个功能来修复 . 这就是为什么SKL-avx512(SKX)是reported to not have a loopback buffer . 幸运的是,这不是性能问题:SKL在Broadwell上的uop-cache吞吐量增加几乎总能跟上问题/重命名 .额外的AH / BH / CH / DH潜伏期:
add bl, ah
从输入BL到输出BL的延迟为2c,因此即使RAX和AH不属于它,它也会增加关键路径的延迟 . (之前我已经看到了另一个操作数的这种额外延迟,在Skylake上有矢量延迟,其中一个int / float延迟"pollutes"永远是一个寄存器.TODO:写出来 . )这意味着使用
movzx ecx, al
/movzx edx, ah
解压缩字节与movzx
/shr eax,8
/movzx
相比具有额外的延迟,但仍然更好吞吐量 .add ah,ah
或add ah,dh
/add dh,ah
每次添加有1c延迟) . 在很多角落里,我没有做过很多测试来证实这一点 .Hypothesis: a dirty high8 value is stored in the bottom of a physical register . 读取干净的高电平8需要移位来提取位[15:8],但读取脏的高电平8只能取物理寄存器的位[7:0],就像正常的8位寄存器读取一样 .
额外延迟并不意味着吞吐量降低 . 即使所有
add
指令都有2c延迟(来自读取DH,未经修改),此程序每2个时钟运行1 iter .Some interesting test loop bodies :
setcc版本(带有
%if 1
)具有20c循环传输延迟,并且即使它具有setcc ah
和add ah,ah
,也从LSD运行 .不明原因:它从LSD运行,即使它使AH变脏 . (至少我认为确实如此.TODO:尝试在
mov eax,ebx
清除它之前添加一些与eax
做某事的指令 . )但是对于
mov ah, bl
,它在HSW / SKL上每次迭代运行5.0c(imul
吞吐量瓶颈) . (已注释掉的商店/重装也有效,但SKL的存储转发速度比HSW快,而且variable-latency ......)请注意,它不再从LSD运行 .