汇编 – Haswell/Skylake的部分寄存器究竟如何执行?写AL似乎对R
这个循环在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依赖于rax,则3ximul / setcc / movzx序列形成循环携带的依赖链。如果没有,每个setcc / movzx / 3ximul链是独立的,从更新循环计数器的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延迟。它还在handle xor-zeroing in the register-rename stage的CPU上使用少一个ALU uop。有关使用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替换xor-zeroing时,0将它降低到Core2:2-3c stall (in the front-end?) to insert a partial-reg merging uop时每4.78个周期减1,当imul在setnz al之后读取eax时。 另外,我使用了movzx eax,它击败了mov-elimination,就像mov rax一样,rax也是如此。 (IvB,HSW和SKL可以重命名movzx eax,bl具有0延迟,但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寄存器的段落。如果你看到了,请告诉我。我对其他帖子的错误评论,所以我可以删除它们或添加更正。我会尝试至少找到并编辑我已经说过的答案。) 我的实际问题:部分寄存器在Skylake上的表现究竟如何? 从IvyBridge到Skylake的一切都是一样的,包括高8的额外延迟? Intel’s optimization manual没有具体说明哪些CPU具有错误依赖性(虽然它确实提到某些CPU具有它们),并且省略了诸如读取AH / BH / CH / DH(high8寄存器)之类的东西,即使它们没有被修改了。 如果Agner Fog的微观指南没有描述任何P6系列(Core2 / Nehalem)行为,那也会很有趣,但我应该将这个问题的范围限制在Skylake或Sandybridge家族。 我的Skylake测试数据,将%rep 4短序列放入一个运行100M或1G迭代的小型dec ebp / jnz循环中。我使用相同的硬件(桌面Skylake i7 6700k)测试Linux的周期与in my answer here相同。 除非另有说明,否则每条指令都使用ALU执行端口作为1个融合域uop运行。 (用 “每循环4个”案例是对无限展开案例的推断。循环开销占用了一些前端带宽,但是每个周期优于1的任何东西都表明寄存器重命名避免了write-after-write output dependency,并且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是。 为什么用通常使用ALU执行单元的指令写入啊对旧值有错误的依赖,而mov r8,r / m8没有(对于reg或内存src)? (那么对于mov r / m8,r8呢?肯定你用于reg-reg的两个操作码中的哪一个移动无关紧要?) 术语:当读取寄存器的其余部分(或在某些其他情况下)时,所有这些都使AH(或DH)“脏”,即需要合并(使用合并的uop)。即,如果我正确理解这一点,那么AH将与RAX分开重命名。 “干净”恰恰相反。有很多方法可以清理脏寄存器,最简单的方法是使用emp或者eax,即esi。 仅写入AL:这些循环从LSD运行:uops_issue.any~ = lsd.uops。 >重复mov al,bl每循环1次运行。偶尔破坏xor eax,每组eax允许OOO执行瓶颈uop吞吐量,而不是延迟。 我认为写入低8的注册表现为RMW混合到完整注册表中,就像添加eax一样,123会是,但如果啊是脏的话,它不会触发合并。所以(除了忽略AH合并之外)它的行为与完全不进行部分reg重命名的CPU的行为相同。似乎AL永远不会与RAX分开重命名? > inc al / inc ah对可以并行运行。 每次迭代插入合并uop的循环都无法从LSD(循环缓冲区)运行? 我不认为AL / AH / RAX与B *,C *,DL / DH / RDX有什么特别之处。我已经在其他寄存器中测试了一些部分寄存器(尽管我主要是为了保持一致性而显示AL / AH),并且从未发现任何差异。 我们如何用一个关于微内部如何在内部工作的合理模型来解释所有这些观察结果? 相关:部分标记问题与部分寄存器问题不同。有关shr r32,cl(甚至是Core2 / Nehalem上的shr r32,2的一些超奇怪的东西,请参阅INC instruction vs ADD 1: Does it matter?:不要读取除1之外的移位标记)。 有关adc循环中的部分标记内容,另请参见Problems with ADC/SBB and INC/DEC in tight loops on some CPUs。
其他答案欢迎更详细地介绍Sandybridge和IvyBridge。
我无法访问该硬件。 我没有发现HSW和SKL之间存在任何部分注册行为差异。 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,Intel’s optimization manual (section 3.5.2.4 Partial Register Stalls)说
我认为他们说在SnB上,添加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也没有。 高8寄存器可以与寄存器的其余部分分开重命名,并且需要合并uop。 >只写访问啊用mov啊,r8或者mov啊,[mem]做重命名AH,不依赖于旧值。这些都是通常不需要ALU uop的指令(对于32位版本)。 (不明原因:一个涉及setcc的循环啊有时候可以从LSD运行,请看这个帖子末尾的rcr循环。也许只要在循环结束时干净啊,它可以使用LSD吗?)。 如果啊是脏的,setcc啊合并到重命名啊,而不是强制合并到rax。例如%rep 4(包括/ test ebx,ebx / setcc啊/ inc al / inc啊)没有生成合并的uops,只能在大约8.7c内运行(因为来自uops的资源冲突,8的延迟减慢了8。还有inc啊/ setcc啊dep链)。 我想这里发生的事情是setcc r8总是被实现为读 – 修改 – 写。英特尔可能认为不值得使用只写setcc uop来优化setcc啊的情况,因为编译器生成的代码很少用于setcc啊。 (但请看问题中的godbolt链接:clang4.0与-m32会这样做。) 使AH变脏可防止循环从LSD(循环缓冲区)运行,即使没有合并的uop。 LSD是指CPU在队列中循环uops以提供问题/重命名阶段。 (称为IDQ)。 插入合并的uops有点像为堆栈引擎插入堆栈同步uops。英特尔的优化手册说,SnB的LSD无法运行具有不匹配的推/弹的循环,这是有道理的,但这意味着它可以运行具有平衡推/弹的循环。这不是我在SKL上看到的:即使是平衡的推/弹也阻止了从LSD运行(例如推rax / pop rdx /次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.)无论如何,当高8寄存器变脏或者包含堆栈引擎微操作时,似乎HSW / SKL不能使用LSD。 此行为可能与an erratum in SKL有关:
这也可能与英特尔的优化手册声明有关,即SnB至少必须在一个循环中自行发布/重命名AH合并uop。对于前端而言,这是一个奇怪的区别。 我的Linux内核日志说微码:sig = 0x506e3,pf = 0x2,revision = 0x84。 使用更新的微码,LSD完全禁用所有时间,而不仅仅是部分寄存器处于活动状态。 lsd.uops总是正好为零,包括真正的程序而不是合成循环。硬件错误(而不是微码错误)通常需要禁用整个功能来修复。这就是为什么SKL-avx512(SKX)是reported to not have a loopback buffer.幸运的是,这不是性能问题:SKL在Broadwell上的uop-cache吞吐量增加几乎总能跟上问题/重命名。 额外的AH / BH / CH / DH潜伏期: >当它不脏时(单独重命名)读取AH会为两个操作数增加额外的延迟周期。例如添加bl,ah从输入BL到输出BL的延迟为2c,因此即使RAX和AH不是它的一部分,它也会增加关键路径的延迟。 (我之前看到过另一个操作数的这种额外延迟,在Skylake上有矢量延迟,其中一个int / float延迟会永久地“污染”一个寄存器.TODO:写下来。) 这意味着使用movzx ecx解压缩字节,al / movzx edx,ah与movzx / shr eax,8 / movzx有额外的延迟,但仍然有更好的吞吐量。 >在脏时读取AH不会增加任何延迟。 (加啊,啊还是加啊,dh / add dh,每次加1都有1c延迟)。在很多角落里,我没有做过很多测试来证实这一点。 假设:脏的high8值存储在物理寄存器的底部。读取干净的高电平8需要移位来提取位[15:8],但读取脏的高电平8只能取物理寄存器的位[7:0],就像正常的8位寄存器读取一样。 额外延迟并不意味着吞吐量降低。即使所有添加指令都有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,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 ; 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版本(带%if 1)有20c循环延迟,并且从LSD运行即使它有setcc啊加啊,啊。 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,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:尝试在mov eax之前添加一些与eax做某事的指令,ebx清除它。) 但是对于mov啊,bl,它在HSW / SKL上每次迭代运行5.0c(imul吞吐量瓶颈)。 (已注释掉的商店/重装也有效,但SKL的存储转发速度比HSW快,而且是variable-latency …) # mov ah,bl version 5,785,393 cycles # 4.289 GHz ( +- 0.08% ) 1,000,315,930 branches # 856.373 M/sec ( +- 0.00% ) 11,728,338 instructions # 2.20 insn per cycle ( +- 0.00% ) 12,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运行。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |