Haswell / Skylake的部分寄存器究竟如何表现?写AL似乎对RAX有错误依赖,AH不一致

时间:2017-08-13 12:05:33

标签: assembly x86 intel cpu-architecture micro-optimization

此循环在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)。

偏离主题:避免这些(故意)瓶颈

我采用可理解/可预测的行为来隔离部分注册内容,而不是最佳性能。

例如,xor - 零/设置标记/ 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 - 归零可以打破除PPro / PII / PIII /早期Pentium-M之外的所有无序CPU对eax的旧值的依赖性(它仍然避免部分寄存器合并)处罚,但没有打破dep)。 Agner Fog's microarch guide describes this。当mov eax,0imul后读取eax时,用setnz al替换xor-zeroing会使其降低到Core2上每4.78个周期一个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如果你在其他帖子上看到我的错误评论,请告诉我。所以我可以删除它们或添加更正。我会尝试至少找到并编辑我的答案,我已经说过了。)

我的实际问题:

从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循环内。我使用与in my answer here相同的方式在同一硬件(桌面Skylake i7 6700k)上测量Linux perf的周期。

除非另有说明,否则每条指令都使用ALU执行端口作为1个融合域uop运行。 (用ocperf.py stat -e ...,uops_issued.any,uops_executed.thread测量)。这检测到(没有)mov-elimination和额外的合并uops。

每个周期" 4"案例是对无限展开案例的推断。循环开销占用了一些前端带宽,但是每个周期优于1的任何东西都表明寄存器重命名避免了write-after-write output dependency,并且uop在内部不作为读取修改处理-write。

仅写入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次。 (循环内的dep-breaking xor eax,eax消除了瓶颈。)
  • 重复setz ahsetc ah每个周期运行1次。 (一个破坏性xor eax,eax让它成为setcc和循环分支的p06吞吐量的瓶颈。)

    为什么用通常使用ALU执行单元的指令编写ah对旧值有错误的依赖,而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 eaxmov 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 +循环开销)。 在一组4之前,一个破解xor eax,eax会让它在每个时钟的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注册的写入表现为完整注册表中的RMW混合,就像add eax, 123一样,但如果ah很脏,它就不会触发合并。所以(除了忽略AH合并),它的行为与完全不进行部分注册重命名的CPU的行为相同。似乎AL永远不会与RAX分开重命名?

  • inc al / inc ah对可以并行运行。
  • 如果mov ecx, eax为"脏",则
  • ah会插入合并的uop,但实际的mov会重命名。这是IvyBridge及其后的Agner Fog describes
  • 重复movzx eax, ah每2个循环运行一次。 (在写完整个寄存器后读取高8位寄存器会产生额外的延迟。)
  • movzx ecx, al没有延迟,也没有在HSW和SKL上执行执行端口。 (就像Agner Fog为IvyBridge所描述的那样,但他说HSW并没有重命名为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的一致性),并且从未注意到任何差异。

我们怎样才能用一个合理的模型来解释所有这些观察结果?微观组织如何在内部工作?

相关:部分标记问题与部分注册问题不同。有关shr r32,cl(甚至是{2}在Core2 / Nehalem上的shr r32,2的一些非常奇怪的内容,请参阅INC instruction vs ADD 1: Does it matter?:不要读取除1之外的移位标记。

另请参阅Problems with ADC/SBB and INC/DEC in tight loops on some CPUs了解adc循环中的部分标记内容。

2 个答案:

答案 0 :(得分:18)

其他答案欢迎更详细地介绍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(和英特尔)表示,Sandybridge可能需要为AL合并uop,因此它可能会与RAX分开重命名。对于SnB,Intel's optimization manual (section 3.5.2.4 Partial Register Stalls)

  

SnB(以后不一定是uarches)在以下情况下插入合并的uop:

     
      
  • 写入其中一个寄存器AH,BH,CH或DH之后和之后   在读取同一寄存器的2-,4-或8字节形式之后。在   这些情况下插入了合并微操作。 插入消耗a   完整的分配周期,其中不能分配其他微观操作。

  •   
  • 在目标寄存器为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, alzero latency (renamed),并且不需要执行单元。(即mov-elimination适用于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
  • 在HSW或SKL上重命名时,
  • movzx eax, bx 被删除。它具有1c延迟并使用ALU uop。英特尔的优化手册仅提到了8位movzx的零延迟(并指出永远不会重命名movzx r32, high8)。

High-8 regs可以与寄存器的其余部分分开重命名,并且需要合并uops。

  • ahmov ah, r8的{​​{1}}的只读访问权限重命名AH,不依赖于旧值。这些都是通常不需要ALU uop的指令(对于32位版本)。
  • 一个AH的RMW(如mov ah, [mem])弄脏了它。
  • inc ah取决于旧setcc ah,但仍然会弄脏它。我认为ah是相同的,但没有经过多次转角测试。

    (原因不明:涉及mov ah, imm8的循环有时可以从LSD运行,请参阅本文末尾的setcc ah循环。也许只要rcr清除<循环的em> end ,它可以使用LSD吗?)。

    如果ah变脏,ah会合并到重命名的setcc ah,而不是强制合并到ah。例如rax%rep 4 / inc al / test ebx,ebx / setcc ah / inc al)不会生成合并的uops,只会在大约8.7c内运行(延迟8 inc ahinc al的uops资源冲突而减慢。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,而不是incdiv r8。同样,即使AH很脏,也不会触发合并的uop。

  • 只写EAX / RAX(如mul r8xor eax,eax)会清除AH-dirty状态(不合并uop)。

  • 只写AX(lea eax, [rsi + rcx])会首先触发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的循环,这是有道理的,但这意味着它可以< / em>使用均衡pop / push运行循环。这不是我在SKL上看到的:即使是平衡pop / push也阻止了从LSD运行(例如pop / push rax / {{ 1}}。(SnB&amp; 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 / SKL在高频时不能使用LSD。 8寄存器是脏的,或者它包含堆栈引擎uops。

此行为可能与an erratum in SKL

有关
  

SKL150: Short Loops Which Use AH/BH/CH/DH Registers May Cause Unpredictable System Behaviour

     

问题:在复杂的微架构条件下,使用AH,BH,CH或DH寄存器以及相应的较宽寄存器(例如AH的RAX,EAX或AX)的小于64指令的短循环可能导致不可预测的系统行为。只有当同一物理处理器上的两个逻辑处理器都处于活动状态时,才会发生这种情况。

这也可能与英特尔的优化手册声明有关,即SnB至少必须在一个循环中自行发布/重命名AH合并uop。这对于前端而言是一个奇怪的区别。

我的Linux内核日志显示pop rdx。 Arch Linux的times 6 imul rax, rdx包只提供更新you have to edit config files to actually have it loaded。所以我的Skylake测试是在i7-6700k上,微码修订版0x84,doesn't include the fix for SKL150 。在我测试的每一个案例中,它都符合Haswell的行为,IIRC。 (例如,Haswell和我的SKL都可以从LSD运行microcode: sig=0x506e3, pf=0x2, revision=0x84 / intel-ucode / setne ah / add ah,ah循环。我启用了HT(这是SKL150显示的前提条件),但是我在一个大多数空闲的系统上进行测试,所以我的线程有自己的核心。

使用更新的微码,LSD完全禁用所有时间,而不仅仅是部分寄存器处于活动状态。 rcr ebx,1总是正好为零,包括真正的程序而不是合成循环。硬件错误(而不是微码错误)通常需要禁用整个功能来修复。这就是为什么SKL-avx512(SKX)是reported to not have a loopback buffer。幸运的是,这不是性能问题:SKL在Broadwell上的uop-cache吞吐量增加几乎总能跟上问题/重命名。

额外的AH / BH / CH / DH潜伏期:

  • 当它不脏时(单独重命名)读取AH会为两个操作数增加额外的延迟周期。例如mov eax,ebx从输入BL到输出BL的延迟为2c,因此即使RAX和AH不属于它,它也会增加关键路径的延迟。 (我之前已经看到了另一个操作数的这种额外延迟,在Skylake上有矢量延迟,其中int / float延迟&#34;永远污染&#34;寄存器.TODO:写出来。)< / LI>

这意味着使用lsd.uops / add bl, ah解包字节与movzx ecx, al / movzx edx, ah / movzx相比有额外的延迟,但仍然有更好的吞吐量。

  • 脏时读取AH并不会增加任何延迟。 (shr eax,8movzx / add ah,ah每次添加有1c延迟)。在许多角落里,我还没有做过很多测试来证实这一点。

    假设:脏的high8值存储在物理寄存器的底部。读取干净的高电平8需要移位来提取位[15:8],但读取脏的高电平8只能取物理寄存器的位[7:0],就像正常的8位寄存器读取一样。

额外延迟并不意味着吞吐量降低。即使所有add ah,dh指令都有2c延迟(来自读取DH,未经修改),该程序每2个时钟运行1个itier。

add dh,ah

add

一些有趣的测试循环体

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)

setcc版本( 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 )具有20c循环延迟,即使它有%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 %if 1,也会从LSD运行。

setcc ah

原因不明:它从LSD运行,即使它使AH变脏。 (至少我认为是这样.TODO:尝试在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% ) 做某事的说明。)

但是对于eax,它在HSW / SKL上每次迭代运行5.0c(mov eax,ebx吞吐量瓶颈)。 (已注释掉的商店/重新加载也有效,但SKL的存储转发速度比HSW快,而且它variable-latency ...)

mov ah, bl

请注意,它不再从LSD运行。

答案 1 :(得分:0)

更新:可能的证据表明,IvyBridge仍与完整的寄存器分开重命名了low16 / low8寄存器,例如Sandybridge,但不同于Haswell及其以后的版本。

InstLatX64的SnB和IvB结果显示movsx r16, r8的吞吐量为0.33c(正如所料,movsx从未被淘汰,在Haswell之前只有3个ALU)。

但显然InstLat的movsx r16, r8测试瓶颈在1c吞吐率下限制了Haswell / Broadwell / Skylake(另请参阅this bug report on the instlat github)。可能是通过编写相同的体系结构寄存器来创建合并链。

(在我的Skylake上,使用单独的目标寄存器的该指令的实际吞吐量为0.25c。使用7条movsx指令写入eax..edi和r10w / r11w进行了测试,所有指令均从cl中读取。还有一个dec ebp/jnz作为循环分支以进行偶数8 uop循环。)

如果我猜对了在IvB之后 IvB在CPU上产生1c吞吐量结果的原因,它的工作类似于运行movsx dx, al块。而且,在将dx与RDX分开重命名而不合并的CPU上,只能在超过1个IPC上运行。因此,我们可以得出结论,IvB实际上仍然确实将low8 / low16寄存器与完整寄存器分开重命名,直到Haswell放弃了。 (但是这里有些可疑:如果这个解释是正确的,我们应该在AMD上看到相同的1c吞吐量,它不会重命名部分寄存器。但是我们没有看到,请参阅下文。)

movsx r16, r8(和movzx r16, r8)测试的吞吐量约为0.33c:

对于0.58c,Haswell的结果具有令人难以置信的movsx/zx r16, r8吞吐量:

其他早晚的Haswell(和CrystalWell)/ Broadwell / Skylake结果在这两个测试中均为1.0c。

  • HSW(2013年6月5日为4.1.570.0),BDW(2018年10月12日为4.3.15787.0),BDW(2017年3月17日为4.3.739.0)。

正如我在github上的InstLat链接问题中所报道的那样,movzx r32, r8的“等待时间”数字忽略了消除运动,大概是像movzx eax, al一样进行了测试。

更糟糕的是,具有单独寄存器测试版本的较新版本的InstLatX64(例如MOVSX r1_32, r2_8)显示的延迟数低于1个周期,例如Skylake上的MOV SX 的延迟值为0.3c 。这完全是胡说八道;我测试只是为了确定。

MOVSX r1_16, r2_8测试的确显示了1c的延迟,因此显然它们只是测量输出(错误)依赖项的延迟。 (对于32位及更宽的输出不存在)。

但是该MOVSX r1_16, r2_8测试也测量了1c延迟on Sandybridge因此,也许我的理论对movsx r16, r8测试告诉我们的观点是错误的。


On Ryzen (AIDA64版本4.3.781.0,2018年2月21日),我们完全不进行任何部分寄存器重命名,结果如果测试确实在重复写入相同的16位寄存器,则不会显示出我们期望的1c吞吐量影响。我也没有在使用K10或Bulldozer系列的InstLatX64较旧版本的任何较旧的AMD CPU上找到它。

  43 X86     :MOVSX r16, r8                L:   0.28ns=  1.0c  T:   0.11ns=  0.40c
  44 X86     :MOVSX r32, r8                L:   0.28ns=  1.0c  T:   0.07ns=  0.25c
  45 AMD64   :MOVSX r64, r8                L:   0.28ns=  1.0c  T:   0.12ns=  0.43c
  46 X86     :MOVSX r32, r16               L:   0.28ns=  1.0c  T:   0.12ns=  0.43c
  47 AMD64   :MOVSX r64, r16               L:   0.28ns=  1.0c  T:   0.13ns=  0.45c
  48 AMD64   :MOVSXD r64, r32              L:   0.28ns=  1.0c  T:   0.13ns=  0.45c

IDK为什么所有的吞吐量都不为0.25;似乎很奇怪。这可能是0.58c Haswell吞吐量效果的版本。 MOVZX编号相同,无前缀版本的0.25吞吐量(读取R8和写入R32)。也许较大指令的获取/解码存在瓶颈?但是movsx r32, r16movsx r32, r8的大小相同。

分离式reg测试显示出与英特尔相同的模式,但是只有合并的延迟为1c。 MOVZX相同。

2252 X86     :MOVSX r1_16, r2_8            L:   0.28ns=  1.0c  T:   0.08ns=  0.28c
2253 X86     :MOVSX r1_32, r2_8            L:   0.07ns=  0.3c  T:   0.07ns=  0.25c
2254 AMD64   :MOVSX r1_64, r2_8            L:   0.07ns=  0.3c  T:   0.07ns=  0.25c
2255 X86     :MOVSX r1_32, r2_16           L:   0.07ns=  0.3c  T:   0.07ns=  0.25c

挖掘机的结果也与此类似,但是产量较低。