Haswell / Skylake上的部分寄存器如何执行? Writi
此循环在英特尔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 dep(PII和PIII之类的早期P6系列除外),仍然避免了部分寄存器合并惩罚,并节省了1个movzx
延迟。 在处理寄存器重命名阶段的异或操作的CPU上,它也使用了更少的ALU uop。 有关使用setcc
xor- setcc
更多信息,请参阅该链接。
请注意,AMD,英特尔Silvermont / KNL和P4不会进行部分寄存器重命名。 这只是英特尔P6系列CPU及其后代英特尔Sandybridge系列中的一项功能,但似乎正逐渐被淘汰。
GCC遗憾的是并倾向于使用cmp
/ setcc al
/ movzx eax,al
在那里也可以使用xor
代替movzx
(Godbolt编译器资源管理器的例子),而铛使用XOR零/ CMP / setcc除非你把喜欢多个布尔条件count += (a==b) | (a==~b)
count += (a==b) | (a==~b)
。
异或/ DEC / setnz版本在每次迭代3.0C上SKYLAKE微架构,Haswell的,和酷睿2(瓶颈上运行imul
吞吐量)。 xor
zeroing在PPro / PII / PIII / early-Pentium-M以外的所有无序CPU上打破了对eax
旧值的依赖(它仍然避免了部分寄存器合并惩罚,但不会破坏dep )。 Agner Fog的微型指南介绍了这一点。 用mov eax,0
替代xor-zero,在Core2:2-3c档位(在前端?),当imul
在setnz al
之后setnz al
读取eax
时,插入一个部分reg合并imul
,将其减慢为每4.78个周期setnz al
。
另外,我使用了movzx eax, al
它可以消除mov消除,就像mov rax,rax
一样。 (IvB,HSW和SKL可以将movzx eax, bl
重命名为0,但Core2不能)。 除了部分寄存器行为,这使得Core2 / SKL中的所有内容都相同。
Core2的行为与Agner Fog的微型指南相一致,但HSW / SKL行为不是。 从Skylake的第11.10部分开始,以前的英特尔架构也是如此:
通用寄存器的不同部分可以存储在不同的临时寄存器中,以消除错误依赖。
他很遗憾没有时间为每一个新的测试重新测试假设做详细的测试,所以这种行为上的变化滑过了裂缝。
Agner确实介绍了通过Skylake在Sandybridge上插入(不拖延)高位寄存器(AH / BH / CH / DH)的合并uop,以及SnB上的低8位/低位16。 (不幸的是,过去我一直在传播错误信息,并且说Haswell可以免费合并AH,我太快地浏览了Agner的Haswell部分,并没有注意到后面的关于high8寄存器的段落,让我知道如果你看到我在其他帖子上的错误评论,所以我可以删除它们或添加一个更正。我会尽力至少找到并编辑我的答案,我已经说过了。)
我的实际问题: 部分寄存器在Skylake上的真实行为如何?
从IvyBridge到Skylake的一切都一样吗,包括高8的额外延迟?
英特尔的优化手册并不具体说明哪些CPU对某些内容具有错误的依赖关系(尽管它确实提到了某些CPU具有这些依赖关系),并且忽略了诸如读取AH / BH / CH / DH(high8寄存器)等事件,甚至在他们避难时也增加了额外的延迟没有被修改。
如果有Agner Fog的微指南没有描述的任何P6系列(Core2 / Nehalem)行为,那也会很有趣,但我应该将这个问题的范围限制在Skylake或Sandybridge系列。
我的Skylake测试数据 ,将%rep 4
短序列放入一个运行100M或1G迭代的小型dec ebp/jnz
循环中。 我测量周期的Linux perf
相同的方式,在这里我的答案,在相同的硬件(桌面SKYLAKE微架构酷睿i7 6700K)。
除非另有说明,否则每条指令都使用ALU执行端口作为1个融合域uop运行。 (用ocperf.py stat -e ...,uops_issued.any,uops_executed.thread
)。 这检测(没有)mov消除和额外的合并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 eax, ebx
就是这样。 mov ah, [rsi]
以每个周期2(负载吞吐量瓶颈)运行。 mov ah, 123
每周期运行1次。 (dep循环xor eax,eax
循环内的xor eax,eax
消除了瓶颈。) 重复的setz ah
或setc ah
每个循环运行1次。 (一个dep-breaking xor eax,eax
让setcc
和loop分支的setcc
吞吐量成为瓶颈。)
为什么写ah
与通常会使用一个ALU执行单元对旧值虚假的依赖性的指令,而mov r8, r/m8
并没有(对已注册或内存SRC)? (那么mov r/m8, r8
怎么样?当然,reg-reg操作中使用的两个操作码中的哪一个并不重要?)
重复add ah, 123
,按预期在每个周期1次运行add ah, 123
次。
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
per group让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个dec ebp/jnz
瓶颈。 add dl, ch
以每个循环0.5次运行。 (每2个周期1次)。 阅读[ABCD] H显然会为dl
创造额外的延迟。 add dl, cl
以每个循环1次运行。 我认为编写一个低8位的注册表会像RMW混合到整个注册表中一样,就像add eax, 123
会一样,但是如果ah
很脏,它不会触发合并。 所以(除了忽略AH
合并),它的行为与根本不做部分reg重命名的CPU相同。 看起来AL
从来没有与RAX
分开重新命名?
inc al
对/ inc ah
可以并行运行。 mov ecx, eax
如果ah
是“脏” mov ecx, eax
插入一个合并的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以外的转换中读取标志)。
另请参见adc
循环中部分标志位的某些CPU上的紧密环路中的ADC / SBB和INC / DEC问题。
其他答案欢迎来到Sandybridge和IvyBridge更详细的地址。 我无法访问该硬件。
我还没有发现HSW和SKL之间的任何局部区域性行为差异。 在Haswell和Skylake上,到目前为止我测试过的所有东西都支持这个模型:
AL不会从RAX单独重命名 (或r15中的r15b)。 所以如果你从不碰到high8寄存器(AH / BH / CH / DH),那么所有的行为都与没有部分reg命名的CPU(例如AMD)完全相同。
对AL的只写访问合并到RAX中,依赖于RAX。 对于加载到AL中,这是一个微型融合的ALU +加载uop,在p0156上执行,这是在每次写入时真正融合的最强大证据之一,而不仅仅是像Agner所推测的那样进行一些奇特的双簿记。
Agner(和Intel)表示Sandybridge可能需要一个合并的uop,所以它可能与RAX分开重新命名。 对于SnB,英特尔的优化手册(第3.5.2.4部分寄存器暂停)说
在下列情况下,SnB(不一定是后来的uarches)会插入一个合并的uop:
写入寄存器AH,BH,CH或DH之后,以及之后读取同一寄存器的2位,4位或8位字节形式之前。 在这些情况下插入合并微操作。 插入消耗一个完整的分配周期,其中不能分配其他微操作。
在目标寄存器为1或2个字节(不是指令源(或寄存器的更大形式))的微操作之后,以及在接下来读取2或4或8字节形式的相同的寄存器 在这些情况下,合并微操作是流程的一部分 。
我认为他们说在SnB上add al,bl
会将RMX全部RAX而不是单独重命名,因为其中一个源寄存器是RAX的一部分。 我的猜测是,这不适用于像mov al, [rbx + rax]
这样的负载; 在寻址模式下的rax
可能不算作为源。
我还没有测试过high8合并的uops是否仍然需要在HSW / SKL上自行发布/重命名。 这会使前端影响相当于4个uops(因为这是问题/重命名管道宽度)。
xor al,al
不起作用, mov al, 0
也不起作用。 movzx ebx, al
具有零延迟(重命名),并且不需要执行单元。 (即在HSW和SKL上运行mov-elimination)。 它触发了AH的合并,如果它很脏的话 ,我认为它是在没有ALU的情况下工作的必要条件。 英特尔在引入mov-elimination的同一个uarch中降低了重新命名可能不是巧合。 (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
从未重命名)。 高8区可以与其他区域分开重新命名,并且需要合并uops。
ah
用mov ah, r8
或mov ah, [mem]
不重命名AH,与旧值不存在依赖关系。 这些都是通常不需要ALU uop的指令(对于32位版本)。 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
)不产生合并微指令,并仅运行在8约8.7c(延迟inc al
从微指令用于减慢由资源冲突ah
还有inc ah
/ setcc ah
dep链)。
我认为这里发生的事情是setcc r8
总是作为读 - 修改 - 写入来实现的。 英特尔可能决定不用写一个只写的setcc
来优化setcc ah
情况,因为编译器生成的代码非常少见setcc ah
。 (但请参阅问题中的godbolt链接:带-m32
clang4.0将这样做。)
读取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很脏,也不会触发合并操作。
只写入EAX / RAX(如lea eax, [rsi + rcx]
或xor eax,eax
)会清除AH-dirty状态(不合并uop)。
mov ax, 1
)会首先触发AH的合并。 我想不是特殊的套管,而是像AX / RAX的任何其他RMW一样运行。 (TODO:测试mov ax, bx
,尽管这不应该是特殊的,因为它没有被重命名。) xor ah,ah
有1c延迟xor ah,ah
没有破解,还需要一个执行端口。 add ah, cl
/ add al, dl
可以每个时钟运行1次(瓶颈添加延迟)。 使AH变脏可防止循环从LSD (循环缓冲区)运行,即使没有合并的uops。 LSD是CPU何时回收发布/重命名阶段的队列中的uops。 (称为IDQ)。
插入合并uops有点像为堆栈引擎插入堆栈同步uops。 英特尔的优化手册说,SnB的LSD无法运行具有不匹配的push
/ pop
循环,这很有道理,但这意味着它可以通过平衡的push
/ pop
运行循环。 这不是我所看到的SKL:即使平衡push
/ pop
阻止来自LSD运行(如push rax
/ pop rdx
/ times 6 imul rax, rdx
(有可能是瑞士央行的LSD和HSW / SKL之间的真正区别: SnB可能只是“锁定”了IDQ中的uops而不是多次重复它们,所以5-uop循环需要2个周期来发布而不是1.25)。无论如何,看起来HSW / SKL不能在使用LSD时使用LSD高8位寄存器是脏的,或者它包含堆栈引擎的uops。
这种行为可能与SKL中的错误有关:
SKL150:使用AH / BH / CH / DH寄存器的短循环可能导致不可预知的系统行为
问题:在复杂的微体系结构条件下,使用AH,BH,CH或DH寄存器以及相应较宽寄存器(例如RA的RAX,EAX或AX)的小于64条指令的短循环可能导致不可预知的系统行为。 这只有在同一个物理处理器上的两个逻辑处理器都处于活动状态时才会发生。
这也可能与英特尔的优化手册声明有关,即SnB至少必须在一个周期内自行发布/重命名AH-merge uop。 对于前端来说这是一个奇怪的差异。
我的Linux内核日志说microcode: sig=0x506e3, pf=0x2, revision=0x84
。 Arch Linux的intel-ucode
软件包只提供更新,您必须编辑配置文件才能实际加载它。 因此, 我的Skylake测试是在微码修订版0x84的i7-6700k上进行的,不包括SKL150的修复 。 它与我在测试的每种情况下IIRC的Haswell行为相匹配。 (例如,Haswell和我的SKL都可以运行setne ah
/ add ah,ah
/ rcr ebx,1
/ mov eax,ebx
来自LSD的mov eax,ebx
循环)。 我启用了HT(这是SKL150的一个前提条件),但我正在测试一个大部分空闲的系统,所以我的线程本身就是核心。
使用更新的微码,LSD对于所有内容始终是完全禁用的,而不仅仅是当部分寄存器处于活动状态时。 lsd.uops
始终为零,包括真正的程序而不是合成循环。 硬件错误(而不是微码错误)通常需要禁用整个功能才能修复。 这就是为什么SKL-avx512(SKX)被报告没有回送缓冲区的原因。 幸运的是,这不是一个性能问题:与Broadwell相比,SKL增加的uop-cache吞吐量几乎总能跟上问题/重命名。
额外的AH / BH / CH / DH延迟:
add bl, ah
从输入BL到输出BL的延迟为2c,所以即使RAX和AH不属于它,它也可以为关键路径添加延迟。 (我之前已经看到过其他操作数的这种额外的延迟,Skylake的矢量延迟,int / float延迟永远会“污染”一个寄存器TODO:写出来。) 这意味着使用movzx ecx, al
/ movzx edx, ah
来解压字节movzx edx, ah
对movzx
/ shr eax,8
/ movzx
有额外的延迟,但仍然有更好的吞吐量。
当脏的时候读AH不会增加任何延迟。 ( add ah,ah
或者add ah,dh
/ add dh,ah
每个添加有1c延迟add dh,ah
)。 在许多角落案例中,我还没有做过很多测试来证实这一点。
假设:一个脏high8值存储在物理寄存器的底部 。 读取一个干净的high8需要转换到提取位[15:8],但读取一个脏high8只需要像普通的8位寄存器读取那样读取一个物理寄存器的位[7:0]。
额外的延迟并不意味着吞吐量下降。 即使所有add
指令都有2c延迟(从读取未修改的DH中),该程序仍可以每2个时钟运行1次。
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版本(使用%if 1
)有20c循环延迟,并且从LSD运行,即使它具有setcc ah
并add ah,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:尝试在mov eax,ebx
之前添加一些与eax
做某些事情的mov eax,ebx
清除它。)
但是对于mov ah, bl
,它在HSW / SKL上以每次迭代5.0c( imul
throughput bottleneck)运行。 (注释掉的商店/重新加载也行得通,但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运行。
链接地址: http://www.djcxy.com/p/4915.html下一篇: c++