此循环在英特尔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).
偏离主题:避免这些(故意)瓶颈
我想采用可理解/可预测的行为来隔离部分注册的东西,而不是最佳性能.
例如,xor
-zero/set-flags/setcc
无论如何都更好(在这种情况下,xor eax,eax
/ dec ecx
/ setnz al
).这打破了所有CPU上的eax(除了像PII和PIII这样的早期P6系列),仍然避免了部分寄存器合并处罚,并节省了1c的movzx
延迟.它还在CPU上使用少一个ALU uop来处理寄存器重命名阶段的xor-zeroing.有关使用xor-zeroing的更多信息,请参阅该链接setcc
.
请注意,AMD,Intel Silvermont/KNL和P4根本不进行部分寄存器重命名.它只是英特尔P6系列CPU及其后代英特尔Sandybridge系列中的一项功能,但似乎已逐步淘汰.
GCC遗憾的是并倾向于使用cmp
/ setcc al
/ movzx eax,al
它也可以使用xor
,而不是movzx
(Godbolt编译器资源管理器的例子),而铛使用XOR零/ CMP/setcc除非你把喜欢多个布尔条件count += (a==b) | (a==~b)
.
xor/dec/setnz版本在Skylake,Haswell和Core2上每次迭代运行3.0c(imul
吞吐量瓶颈). xor
-zeroing打破了对eax
除PPro/PII/PIII /早期Pentium-M之外的所有无序CPU 的旧值的依赖性(它仍然避免了部分寄存器合并处罚但不会破坏dep). Agner Fog的微型指南描述了这一点.更换xor- zeroing并将mov eax,0
其降低到Core2上每4.78个循环减速一次:2-3c失速(在前端?),以便在imul
读取eax
后插入部分reg合并uopsetnz al
.
另外,我使用movzx eax, al
哪个击败了mov-elimination,就像mov rax,rax
那样.(IvB,HSW和SKL可以movzx eax, bl
使用0延迟重命名,但Core2不能).除了部分寄存器行为之外,这使得Core2/SKL上的所有内容都相同.
Core2行为与Agner Fog的微观指南一致,但HSW/SKL行为并非如此.从第11.10节到Skylake,以及之前的英特尔搜索:
通用寄存器的不同部分可以存储在不同的临时寄存器中,以消除错误的依赖性.
遗憾的是,他没有时间对每个新的uarch进行详细测试以重新测试假设,因此这种行为的变化从裂缝中滑落.
Agner确实描述了通过Skylake在Sandybridge上插入high8寄存器(AH/BH/CH/DH)以及SnB上的low8/low16插入(不停止)合并uop.(遗憾的是,我过去一直散布错误的信息,并说Haswell可以免费合并AH.我过快地浏览了Agner的Haswell部分,并且没有注意到后面关于high8寄存器的段落.如果你看到,请告诉我.我对其他帖子的错误评论,所以我可以删除它们或添加更正.我会尝试至少找到并编辑我的答案,我已经说过了.)
我的实际问题:部分寄存器在Skylake上的表现究竟如何?
从IvyBridge到Skylake的一切都是一样的,包括高8的额外延迟?
英特尔的优化手册并没有具体说明哪些CPU具有错误依赖性(虽然它确实提到某些CPU具有它们),并且省略了诸如读取AH/BH/CH/DH(high8寄存器)之类的东西,即使它们没有它们也会增加额外的延迟没被修改过.
如果有任何P6家族(Core2/Nehalem)行为,Agner Fog的微观指南没有描述,那也会很有趣,但我应该将这个问题的范围限制在Skylake或Sandybridge家族.
我的Skylake测试数据,将%rep 4
短序列放入一个dec ebp/jnz
运行100M或1G迭代的小循环中.我用Linux测试循环perf
的方式与我在这里的答案相同,在相同的硬件上(桌面Skylake i7 6700k).
除非另有说明,否则每条指令都使用ALU执行端口作为1个融合域uop运行.(用ocperf.py stat -e ...,uops_issued.any,uops_executed.thread
)测量.这检测到(没有)mov-elimination和额外的合并uops.
"每循环4个"案例是对无限展开案例的推断.循环开销占用了一些前端带宽,但是每个周期优于1的任何东西都表明寄存器重命名避免了写后写输出依赖性,并且uop在内部不作为读 - 修改处理-写.
仅写入AH:防止循环从环回缓冲区(也称为循环流检测器(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次.(循环内部的破坏xor eax,eax
消除了瓶颈.)
重复setz ah
或setc ah
每循环1次运行.(一个dep-breaking xor eax,eax
让它成为p06吞吐量setcc
和循环分支的瓶颈.)
为什么ah
用通常使用ALU执行单元的指令写入对旧值有错误依赖,而mov r8, r/m8
对于reg或内存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最近根本没有被修改).
术语:所有这些都使AH(或DH)" 脏 ",即在读取其余寄存器时(或在某些其他情况下)需要合并(使用合并的uop).即如果我正确理解这一点,那么AH将与RAX分开重命名." 干净 "恰恰相反.有许多方法来清洗脏的寄存器,其中最简单的inc eax
或mov eax, esi
.
仅写入AL:这些循环从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 +循环开销).xor eax,eax
在4个组之前进行断开操作会使每个时钟的2个负载出现瓶颈.
mov al, 123
每个循环重复运行1次.
mov al, bh
每循环重复运行0.5次.(每2个循环1个).阅读[ABCD] H很特别.
xor eax,eax
+ 6x mov al,bh
+ dec ebp/jnz
:每个2c,前端每个时钟4个uop的瓶颈.
add dl, ch
每循环重复运行0.5次.(每2个循环1个).阅读[ABCD] H显然会产生额外的延迟dl
.
add dl, cl
每个循环重复运行1次.
我认为对低8 reg的写入表现为RMW混合到完整的reg中,就像add eax, 123
它一样,但如果ah
是脏的话它不会触发合并.所以(除了忽略AH
合并之外)它的行为与完全不进行部分reg重命名的CPU的行为相同.它似乎AL
从未单独重命名RAX
?
inc al
/ inc ah
pairs可以并行运行.
mov ecx, eax
如果ah
是"脏",则插入合并的uop ,但实际mov
重命名.这就是Agner Fog为IvyBridge和后来描述的内容.
movzx eax, ah
每2个循环重复一次.(在写完整个reg之后读取高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
(甚至shr r32,2
在Core2/Nehalem上:不要从1以外的移位中读取标记).
另请参阅某些CPU上的紧密循环中的ADC/SBB和INC/DEC问题,以了解循环中的部分标志内容adc
.
其他答案欢迎更详细地介绍Sandybridge和IvyBridge.我无法访问该硬件.
我没有发现HSW和SKL之间存在任何部分注册行为差异.在Haswell和Skylake上,到目前为止我测试的所有内容都支持这个模型:
AL永远不会与RAX(或r15中的r15b)分开重命名.因此,如果您从未触摸过high8寄存器(AH/BH/CH/DH),那么所有内容的行为与没有部分注册重命名的CPU(例如AMD)完全相同.
对AL的只写访问权限合并到RAX中,并依赖于RAX.对于AL的加载,这是一个微融合的ALU +加载uop,它在p0156上执行,这是它在每次写入时真正合并的最强证据之一,而不仅仅是像Agner推测的那样做一些花哨的双重记录.
Agner(和英特尔)表示,Sandybridge可能需要为AL合并uop,因此它可能会与RAX分开重命名.对于SnB,英特尔的优化手册(第3.5.2.4节"部分寄存器停顿")说
在以下情况下,SnB(不一定是后来的搜索)会插入合并的uop:
在写入寄存器AH,BH,CH或DH之一之后并且在随后读取相同寄存器的2字节,4字节或8字节形式之前.在这些情况下,插入合并微操作.插入消耗完整的分配周期,其中不能分配其他微操作.
在具有1或2个字节的目标寄存器的微操作之后,该寄存器不是指令的源(或寄存器的更大形式),并且在随后读取2字节,4字节或8字节形式的相同的注册.在这些情况下,合并微操作是流程的一部分.
我认为他们说在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
具有零延迟(重命名),并且不需要执行单元. (即,移动消除适用于HSW和SKL). 如果它很脏,它会触发AH的合并,我认为它在没有ALU的情况下工作是必要的.英特尔在引入mov-elimination的同一个uarch中降低了8的重命名,这可能不是巧合.(Agner Fog的微型指南在这里有一个错误,他说在HSW或SKL上没有消除零扩展动作,只有IvB.)
movzx eax, al
是不是在重命名消除.英特尔的mov-elimination永远不会同样适用. mov rax,rax
也没有消除,即使它不必对任何东西进行零延伸.(虽然没有必要给它特殊的硬件支持,因为它只是一个无操作,不像mov eax,eax
).无论如何,当零扩展时,无论是32位mov
还是8位,都更喜欢在两个独立的架构寄存器之间移动movzx
.
movzx eax, bx
是不是在对HSW或SKL重命名消除.它具有1c延迟并使用ALU uop.英特尔的优化手册仅提到了8位movzx的零延迟(并指出movzx r32, high8
永远不会重命名).
只写访问ah
与mov ah, reg8
或mov ah, [mem8]
做重命名AH,与旧值不存在依赖关系.这些都是通常不需要ALU uop的指令(对于32位版本).
一个RM的AH(喜欢mov ah, bl
)弄脏了它.
inc ah
取决于旧的setcc ah
,但仍然很脏.我认为ah
是相同的,但没有测试过多的角落情况.
(原因不明:涉及循环mov ah, imm8
有时可以从LSD运行,请参阅本文setcc ah
末尾的循环.也许只要循环结束时它rcr
是干净的,它可以使用LSD吗?).
如果ah
是脏的,则ah
合并到重命名的setcc ah
,而不是强制合并ah
.例如 rax
(%rep 4
/ inc al
/ test ebx,ebx
/ setcc ah
/ inc al
)不生成合并的uops,并且只运行大约8.7c(inc ah
由于来自uops 的资源冲突,8的延迟减慢inc al
.还有ah
/ inc ah
dep链).
我认为这里发生的事情setcc ah
始终是作为读 - 修改 - 写实现的.英特尔可能认为不值得使用只写setcc r8
uop来优化setcc
案例,因为编译器生成的代码非常少见setcc ah
.(但请看问题中的godbolt链接:clang4.0 setcc ah
会这样做.)
读取AX,EAX或RAX会触发合并uop(占用前端问题/重命名带宽).可能RAT(寄存器分配表)跟踪架构R [ABCD] X的高8脏状态,甚至在写入AH退出之后,AH数据也存储在与RAX不同的物理寄存器中.即使在编写AH和读取EAX之间有256个NOP,也有一个额外的合并uop.(SKL上的ROB大小= 224,因此这保证了它-m32
已经退役).使用uops_issued /执行的perf计数器检测到,这清楚地显示了差异.
AL的读取 - 修改 - 写入(例如mov ah, 123
)免费合并,作为ALU uop的一部分.(仅使用一些简单的uops进行测试,例如inc al
/ add
,不是inc
或div r8
).同样,即使AH很脏,也不会触发合并的uop.
只写到EAX/RAX(像mul r8
或lea eax, [rsi + rcx]
)清除AH脏状态(没有合并的uop).
只写AX(xor eax,eax
)首先触发AH的合并.我想这不是特殊套管,而是像任何其他RMW AX/RAX一样运行.(TODO:测试mov ax, 1
,虽然这不应该是特殊的,因为它没有被重命名.)
mov ax, bx
有1c延迟,不是破坏,仍然需要一个执行端口.
读取和/或写入AL不会强制合并,因此AH可以保持脏(并且可以在单独的dep链中独立使用).(例如xor ah,ah
/ add ah, cl
可以每个时钟运行1次(增加延迟的瓶颈).
使AH变脏可防止循环从LSD(循环缓冲区)运行,即使没有合并的uop.LSD是指CPU在队列中循环uops以提供问题/重命名阶段.(称为IDQ).
插入合并的uops有点像为堆栈引擎插入堆栈同步uops.英特尔的优化手册说SnB的LSD不能运行不匹配的循环add al, dl
/ push
,这是有道理的,但这意味着它可以运行带有平衡pop
/的循环push
.这不是我所看到的SKL:即使平衡pop
/ push
阻止来自LSD(运行如pop
/ push rax
/ pop rdx
(有可能是瑞士央行的LSD和HSW/SKL之间的真正区别: 在IDQ SNB可能只是"锁定"的微指令而不是多次重复它们,所以5-uop循环需要2个周期而不是1.25.)无论如何,当高8寄存器变脏或者包含高速8时,HSW/SKL似乎不能使用LSD.堆栈引擎uops.
此行为可能与SKL中的错误相关:
SKL150:使用AH/BH/CH/DH寄存器的短循环可能导致不可预测的系统行为
问题:在复杂的微架构条件下,使用AH,BH,CH或DH寄存器以及相应的较宽寄存器(例如AH的RAX,EAX或AX)的小于64指令的短循环可能会导致不可预测的系统行为.这只能在同一物理处理器上的两个逻辑处理器都处于活动状态时发生.
这也可能与英特尔的优化手册声明有关,即SnB至少必须在一个循环中自行发布/重命名AH合并uop.这对于前端而言是一个奇怪的区别.
我的Linux内核日志说times 6 imul rax, rdx
.Arch Linux的microcode: sig=0x506e3, pf=0x2, revision=0x84
软件包只提供更新,你必须编辑配置文件以实际加载它.所以我的Skylake测试是在i7-6700k上进行,微码修订版为0x84,不包括SKL150的修复程序.在我测试的每一个案例中,它都符合Haswell的行为,IIRC.(例如,Haswell和我的SKL都可以从LSD 运行intel-ucode
/ setne ah
/ add ah,ah
/ rcr ebx,1
循环).我启用了HT(这是SKL150显示的前提条件),但我正在测试一个主要是空闲的系统,所以我的线程有自己的核心.
使用更新的微码,LSD完全禁用所有时间,而不仅仅是部分寄存器处于活动状态. mov eax,ebx
总是精确为零,包括真正的程序而不是合成循环.硬件错误(而不是微码错误)通常需要禁用整个功能来修复.这就是据报道 SKL-avx512(SKX)没有环回缓冲区的原因.幸运的是,这不是性能问题:SKL在Broadwell上增加的uop-cache吞吐量几乎总能跟上问题/重命名.
当AH不脏时(单独重命名)读取AH会为两个操作数增加额外的延迟周期.例如lsd.uops
,从输入BL到输出BL的延迟为2c,因此即使RAX和AH不是它的一部分,它也可以增加关键路径的延迟.(我之前看到过另一个操作数的这种额外延迟,在Skylake上有矢量延迟,其中一个int/float延迟会永久地"污染"一个寄存器.TODO:写下来.)
这意味着拆包个字节add bl, ah
/ movzx ecx, al
有额外的延迟与movzx edx, ah
/ movzx
/ shr eax,8
,但还是更好的吞吐量.
阅读AH当它是脏不添加任何延迟.(movzx
或add ah,ah
/ add ah,dh
每次添加1c延迟).在很多角落里,我没有做过很多测试来证实这一点.
假设:脏的high8值存储在物理寄存器的底部.读取干净的高电平8需要移位来提取位[15:8],但读取脏的高电平8只能取物理寄存器的位[7:0],就像正常的8位寄存器读取一样.
额外延迟并不意味着吞吐量降低.即使所有add dh,ah
指令都有2c延迟(来自读取DH,未修改),该程序每2个时钟可以运行1个iter .
global _start _start: mov ebp, 100000000 .loop: add ah, dh add bh, dh add ch, dh add al, dh add bl, dh add cl, dh add dl, dh dec ebp jnz .loop xor edi,edi mov eax,231 ; __NR_exit_group from /usr/include/asm/unistd_64.h syscall ; sys_exit_group(0)
Performance counter stats for './testloop': 48.943652 task-clock (msec) # 0.997 CPUs utilized 1 context-switches # 0.020 K/sec 0 cpu-migrations # 0.000 K/sec 3 page-faults # 0.061 K/sec 200,314,806 cycles # 4.093 GHz 100,024,930 branches # 2043.675 M/sec 900,136,527 instructions # 4.49 insn per cycle 800,219,617 uops_issued_any # 16349.814 M/sec 800,219,014 uops_executed_thread # 16349.802 M/sec 1,903 lsd_uops # 0.039 M/sec 0.049107358 seconds time elapsed
一些有趣的测试循环体:
%if 1 imul eax,eax mov dh, al inc dh inc dh inc dh ; add al, dl mov cl,dl movzx eax,cl %endif Runs at ~2.35c per iteration on both HSW and SKL. reading `dl` has no dep on the `inc dh` result. But using `movzx eax, dl` instead of `mov cl,dl` / `movzx eax,cl` causes a partial-register merge, and creates a loop-carried dep chain. (8c per iteration). %if 1 imul eax, eax imul eax, eax imul eax, eax imul eax, eax imul eax, eax ; off the critical path unless there's a false dep %if 1 test ebx, ebx ; independent of the imul results ;mov ah, 123 ; dependent on RAX ;mov eax,0 ; breaks the RAX dependency setz ah ; dependent on RAX %else mov ah, bl ; dep-breaking %endif add ah, ah ;; ;inc eax ; sbb eax,eax rcr ebx, 1 ; dep on add ah,ah via CF mov eax,ebx ; clear AH-dirty ;; mov [rdi], ah ;; movzx eax, byte [rdi] ; clear AH-dirty, and remove dep on old value of RAX ;; add ebx, eax ; make the dep chain through AH loop-carried %endif
setcc版本(带有add
)具有20c循环传输延迟,并且从LSD运行,即使它具有%if 1
和setcc ah
.
00000000004000e0 <_start.loop>: 4000e0: 0f af c0 imul eax,eax 4000e3: 0f af c0 imul eax,eax 4000e6: 0f af c0 imul eax,eax 4000e9: 0f af c0 imul eax,eax 4000ec: 0f af c0 imul eax,eax 4000ef: 85 db test ebx,ebx 4000f1: 0f 94 d4 sete ah 4000f4: 00 e4 add ah,ah 4000f6: d1 db rcr ebx,1 4000f8: 89 d8 mov eax,ebx 4000fa: ff cd dec ebp 4000fc: 75 e2 jne 4000e0 <_start.loop> Performance counter stats for './testloop' (4 runs): 4565.851575 task-clock (msec) # 1.000 CPUs utilized ( +- 0.08% ) 4 context-switches # 0.001 K/sec ( +- 5.88% ) 0 cpu-migrations # 0.000 K/sec 3 page-faults # 0.001 K/sec 20,007,739,240 cycles # 4.382 GHz ( +- 0.00% ) 1,001,181,788 branches # 219.276 M/sec ( +- 0.00% ) 12,006,455,028 instructions # 0.60 insn per cycle ( +- 0.00% ) 13,009,415,501 uops_issued_any # 2849.286 M/sec ( +- 0.00% ) 12,009,592,328 uops_executed_thread # 2630.307 M/sec ( +- 0.00% ) 13,055,852,774 lsd_uops # 2859.456 M/sec ( +- 0.29% ) 4.565914158 seconds time elapsed ( +- 0.08% )
不明原因:它从LSD运行,即使它使AH变脏.(至少我认为确实如此.TODO:尝试add ah,ah
在eax
清除之前添加一些与之相关的指令.)
但是mov eax,ebx
,它在mov ah, bl
HSW/SKL上每次迭代运行5.0c(吞吐量瓶颈).(已注释掉的商店/重新加载也可以,但SKL的存储转发速度比HSW快,而且可变延迟 ......)
# mov ah, bl version 5,009,785,393 cycles # 4.289 GHz ( +- 0.08% ) 1,000,315,930 branches # 856.373 M/sec ( +- 0.00% ) 11,001,728,338 instructions # 2.20 insn per cycle ( +- 0.00% ) 12,003,003,708 uops_issued_any # 10275.807 M/sec ( +- 0.00% ) 11,002,974,066 uops_executed_thread # 9419.678 M/sec ( +- 0.00% ) 1,806 lsd_uops # 0.002 M/sec ( +- 3.88% ) 1.168238322 seconds time elapsed ( +- 0.33% )
请注意,它不再从LSD运行.