Haswell/Skylake 上的部分寄存器究竟如何执行?写AL好像对RAX有虚假依赖,和AH不一致

How exactly do partial registers on Haswell/Skylake perform? Writing AL seems to have a false dependency on RAX, and AH is inconsistent

此循环 运行s 在 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/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 的依赖(除了早期的 P6 系列,如 PII 和 PIII),仍然避免了部分寄存器合并惩罚,并节省了 1c 的 movzx 延迟。它在 CPU 上使用的 ALU 微指令也比 少一个。有关使用 setcc.

的异或归零的更多信息,请参阅 link

请注意,AMD、Intel Silvermont/KNL 和 P4 根本不进行部分寄存器重命名。它只是 Intel P6 系列 CPU 及其后代 Intel 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/early-Pentium-M 之外的所有乱序 CPU 上打破了对 eax 旧值的依赖(它仍然避免了部分寄存器合并惩罚但不会破坏部门)。 Agner Fog's microarch guide describes this. Replacing the xor-zeroing with mov eax,0 slows it down to one per 4.78 cycles on Core2: 2-3c stall (in the front-end?) to insert a partial-reg merging uopimulsetnz al 之后读取 eax

此外,我使用 movzx eax, al 来阻止移动消除,就像 mov rax,rax 一样。 (IvB、HSW 和 SKL 可以以 0 延迟重命名 movzx eax, bl,但 Core2 不能)。这使得 Core2 / SKL 的一切都相等,部分寄存器行为除外。


Core2 行为与 Agner Fog's microarch guide 一致,但 HSW/SKL 行为不同。来自 Skylake 的第 11.10 节,以及以前的 Intel uarches:

Different parts of a general purpose register can be stored in different temporary registers in order to remove false dependences.

不幸的是,他没有时间对每个新的 uarch 进行详细测试以重新测试假设,因此这种行为上的变化被漏掉了。

Agner 确实描述了通过 Skylake 为 Sandybridge 上的 high8 寄存器 (AH/BH/CH/DH) 和 SnB 上的 low8/low16 插入(不停止)合并 uop。 (不幸的是,我过去一直在传播错误信息,并说 Haswell 可以免费合并 AH。我浏览 Agner 的 Haswell 部分太快了,没有注意到后面关于 high8 寄存器的段落。如果你看到请告诉我我对其他帖子的错误评论,所以我可以删除它们或添加更正。我会尝试至少在我说过的地方找到并编辑我的答案。)


我的实际问题:究竟部分寄存器在 Skylake 上的表现如何?

从 IvyBridge 到 Skylake 一切都一样吗,包括 high8 额外延迟?

Intel's optimization manual 没有具体说明哪些 CPU 对什么有错误的依赖性(尽管它确实提到一些 CPU 有它们),并且遗漏了诸如读取 AH/BH/CH/DH(high8 寄存器)添加额外的东西延迟,即使它们没有被修改。

如果 Agner Fog 的微架构指南没有描述任何 P6 系列 (Core2/Nehalem) 行为,那也很有趣,但我可能应该将这个问题的范围限制在 Skylake 或 Sandybridge -家庭。


我的 Skylake 测试数据,将 %rep 4 短序列放入一个小的 dec ebp/jnz 循环中,运行s 100M 或 1G 迭代。我在相同的硬件(台式机 Skylake i7 6700k)上用 Linux perf 以与 相同的方式测量了周期。

除非另有说明,每条指令 运行 作为 1 个融合域 uop,使用 ALU 执行端口。 (用 ocperf.py stat -e ...,uops_issued.any,uops_executed.thread 测量)。这会检测(不存在)移动消除和额外的合并微指令。

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

仅写入 AH:防止循环从回送缓冲区(也称为循环流检测器 (LSD))执行。 lsd.uops 的计数在 HSW 上恰好为 0,在 SKL 上很小(大约 1.8k)并且不随循环迭代计数缩放。这些计数可能来自某些内核代码。当循环从 LSD 执行 运行 时,lsd.uops ~= uops_issued 到测量噪声范围内。一些循环在 LSD 或非 LSD 之间交替(例如,如果解码从错误的位置开始,它们可能不适合 uop 缓存),但我在测试时没有 运行 进入。

术语:所有这些都留下 AH(或 DH)“”,即需要合并(使用合并 uop ) 当读取寄存器的其余部分时(或在某些其他情况下)。也就是说,AH 与 RAX 分开重命名,如果我理解正确的话。 “clean”则相反。清除脏寄存器的方法有很多种,最简单的是 inc eaxmov eax, esi.

仅写入 AL:这些循环从 LSD 执行 运行:uops_issue.any ~= lsd.uops.

我认为写入低 8 reg 的行为类似于 RMW 混合到完整 reg 中,就像 add eax, 123 一样,但如果 ah 是脏的,它不会触发合并.因此(除了忽略 AH 合并)它的行为与根本不进行部分注册重命名的 CPU 相同。 AL 似乎从未与 RAX?

分开重命名

我不认为 AL/AH/RAX 与 B*、C*、DL/DH/RDX 有什么特别之处。我已经在其他寄存器中使用部分寄存器测试了一些(尽管我主要显示 AL/AH 以保持一致性),但从未发现任何差异。

我们如何用微架构内部工作原理的合理模型来解释所有这些观察结果?


相关:部分 flag 问题不同于部分 register 问题。请参阅 了解一些带有 shr r32,cl 的超级奇怪的东西(甚至 Core2/Nehalem 上的 shr r32,2:不要从除 1 以外的移位中读取标志)。

另请参阅 了解 adc 循环中的部分标志。

其他答案欢迎更详细地解决 Sandybridge 和 IvyBridge。 我无法访问该硬件。


我没有发现 HSW 和 SKL 之间有任何局部调节行为差异。 在 Haswell 和 Skylake 上,到目前为止我测试过的所有东西都支持这个模型:

AL 从未与 RAX(或 r15 的 r15b)分开重命名。因此,如果您从不接触 high8 寄存器(AH/BH/CH/DH),则一切都与 CPU 上的行为完全一样,没有部分寄存器重命名(例如 AMD)。

对 AL 的只写访问合并到 RAX 中,依赖于 RAX。对于加载到 AL 中,这是一个在 p0156 上执行的微融合 ALU+加载 uop,这是最有力的证据之一,表明它在每次写入时真正合并,而不仅仅是像 Agner 推测的那样进行一些花哨的双重簿记。

Agner(和 Intel)说 Sandybridge 可能需要一个用于 AL 的合并 uop,因此它可能与 RAX 分开重命名。对于 SnB,Intel's optimization manual (section 3.5.2.4 Partial Register Stalls) 表示

SnB (not necessarily later uarches) inserts a merging uop in the following cases:

  • After a write to one of the registers AH, BH, CH or DH and before a following read of the 2-, 4- or 8-byte form of the same register. In these cases a merge micro-op is inserted. The insertion consumes a full allocation cycle in which other micro-ops cannot be allocated.

  • After a micro-op with a destination register of 1 or 2 bytes, which is not a source of the instruction (or the register's bigger form), and before a following read of a 2-,4- or 8-byte form of the same register. In these cases the merge micro-op is part of the flow.

我认为他们是说在 SnB 上,add al,bl 将对完整的 RAX 进行 RMW 而不是单独重命名,因为其中一个源寄存器是 RAX(的一部分)。我的猜测是这不适用于像 mov al, [rbx + rax] 这样的负载; rax 在寻址模式下可能不算作来源。

我还没有测试 high8 合并 uops 是否仍然必须在 HSW/SKL 上自行 issue/rename。这将使前端影响相当于 4 微指令(因为这是 issue/rename 管道宽度)。

  • 如果不写 EAX/RAX,就无法打破涉及 AL 的依赖关系。 xor al,al 无济于事,mov al, 0.
  • 也无济于事
  • movzx ebx, al,不需要执行单元。(即 mov-elimination 适用于 HSW 和 SKL)。 如果它是脏的,它会触发 AH 的合并,我想这是它在没有 ALU 的情况下工作所必需的。英特尔在引入 mov-elimination 的同一个 uarch 中放弃了 low8 重命名可能不是巧合。 (Agner Fog的micro-arch guide这里有个错误,说在HSW或SKL上不消除zero-extended moves,只有IvB。)
  • movzx eax, al 不会 在重命名时被删除。英特尔的 mov-elimination 永远不会适用于相同的,相同的。 mov rax,rax 也没有被消除,即使它不需要零扩展任何东西。 (虽然没有必要为其提供特殊的硬件支持,因为它只是一个空操作,不像 mov eax,eax)。无论如何,无论是 32 位 mov 还是 8 位 movzx.
  • ,在零扩展时更喜欢在两个独立的体系结构寄存器之间移动
  • movzx eax, bx 在 HSW 或 SKL 重命名时被删除。它具有 1c 延迟并使用 ALU uop。 Intel 的优化手册只提到了 8 位 movzx 的零延迟(并指出 movzx r32, high8 从未重命名)。

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

  • 使用 mov ah, reg8mov ah, [mem8]ah 的只写访问权限重命名 AH,不依赖于旧值。这些都是 32 位版本通常不需要 ALU 微指令的指令。 (但是 mov ah, bl 而不是 消除;它确实需要一个 p0156 ALU uop,所以这可能是巧合)。
  • AH的RMW(如inc ah)弄脏了它。
  • setcc ah依赖老ah,还是脏了。我认为 mov ah, imm8 是一样的,但还没有测试那么多极端情况。

    (未解释:涉及 setcc ah 的循环有时可以从 LSD 中 运行,请参阅此 post 末尾的 rcr 循环。可能只要ah在循环的[​​=180=]结尾是干净的,它可以使用LSD?)。

    如果 ah 是脏的,setcc ah 合并到重命名的 ah,而不是强制合并到 rax。例如%rep 4 (inc al / test ebx,ebx / setcc ah / inc al / inc ah) 不生成合并微指令,并且只生成 运行s大约 8.7c(8 inc al 的延迟因来自 ah 的 uops 的资源冲突而减慢。还有 inc ah / setcc ah dep 链)。

    我认为这里发生的事情是 setcc r8 总是作为读-修改-写实现的。英特尔可能认为不值得使用只写 setcc uop 来优化 setcc ah 情况,因为编译器生成的代码很少见 setcc ah。 (但是看到问题中的神栓 link : clang4.0 with -m32 will do so.)

  • 读取 AX、EAX 或 RAX 会触发合并 uop(占用前端 issue/rename 带宽)。可能是 RAT(寄存器分配 Table)跟踪架构 R[ABCD]X 的高 8 脏状态,甚至在写入 AH 退出后,AH 数据存储在单独的物理寄存器中来自 RAX。即使在写入 AH 和读取 EAX 之间有 256 个 NOP,也有一个额外的合并 uop。 (SKL 上的 ROB 大小 = 224,因此这保证了 mov ah, 123 已退休)。用 uops_issued/executed 性能计数器检测到,可以清楚地显示差异。

  • AL(例如 inc al)的读取-修改-写入免费合并,作为 ALU uop 的一部分。 (只测试了一些简单的微指令,比如 add/inc,而不是 div r8mul r8)。同样,即使 AH 是脏的,也不会触发合并 uop。

  • 只写到 EAX/RAX(如 lea eax, [rsi + rcx])清除 AH-dirty 状态(无合并 uop)。

  • 只写到 AX (mov ax, 1) 首先触发 AH 的合并。我想这不是特殊的外壳,它 运行 就像 AX/RAX 的任何其他 RMW 一样。 (TODO:测试 mov ax, bx,虽然这应该不是特别的,因为它没有重命名。)
  • xor ah,ah 有 1c 的延迟,没有 dep-breaking,仍然需要一个执行端口。
  • 读取 and/or 写入 AL 不会强制合并,因此 AH 可以保持脏状态(并在单独的 dep 链中独立使用)。 (例如 add ah, cl / add al, dl 可以 运行 每个时钟 1 个(在添加延迟时出现瓶颈)。

使 AH 变脏可防止来自 LSD(循环缓冲区)的循环 运行ning,即使没有合并 uops。 LSD 是当 CPU 回收队列中的 uops 给 issue/rename 阶段。 (称为 IDQ)。

插入合并微指令有点像为堆栈引擎插入堆栈同步微指令。 Intel 的优化手册上说 SnB 的 LSD 不能 运行 loop with mismatched push/pop,这是有道理的,但它暗示它 可以 运行 循环,平衡 push/pop。这不是我在 SKL 上看到的:即使是平衡的 push/pop 也会阻止 LSD 的 运行ning(例如 push rax / pop rdx / times 6 imul rax, rdx. (SnB的LSD和HSW/SKL之间可能有一个真正的区别:.)无论如何,HSW/SKL在高8位寄存器时似乎不能使用LSD脏,或者当它包含堆栈引擎微指令时。

此行为可能与 an erratum in SKL:

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

Problem: Under complex micro-architectural conditions, short loops of less than 64 instruction that use AH, BH, CH, or DH registers as well as their corresponding wider registers (e.g. RAX, EAX, or AX for AH) may cause unpredictable system behaviour. This can only happen when both logical processors on the same physical processor are active.

这也可能与英特尔的优化手册声明SnB至少要issue/rename一个循环中的AH-merge uop本身有关。这对于前端来说是一个奇怪的差异。

我的 Linux 内核日志显示 microcode: sig=0x506e3, pf=0x2, revision=0x84。 Arch Linux 的 intel-ucode 软件包仅提供更新 you have to edit config files to actually have it loaded. So 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 吞吐量几乎总是可以跟上 issue/rename.


额外 AH/BH/CH/DH 延迟:

  • 在 AH 不脏时读取它(单独重命名)会为两个操作数增加一个额外的延迟周期。例如add bl, ah 从输入 BL 到输出 BL 有 2c 的延迟,因此即使 RAX 和 AH 不是关键路径的一部分,它也会增加关键路径的延迟。 (我以前见过其他操作数的这种额外延迟,在 Skylake 上有矢量延迟,其中 int/float 延迟 "pollutes" 一个寄存器永远。TODO:写下来。)

这意味着使用 movzx ecx, al / movzx edx, ah 解包字节比 movzx/shr eax,8/movzx 有额外的延迟,但吞吐量仍然更好。

  • 脏时读取 AH 不会增加任何延迟。 (add ah,ahadd ah,dh/add dh,ah 每次添加有 1c 延迟)。我没有在许多极端情况下进行大量测试来证实这一点。

    假设:一个脏的high8值存储在物理寄存器的底部。读取一个干净的 high8 需要移位以提取位 [15:8],但是读取一个脏的 high8 可以像普通的 8 位寄存器读取一样只需要物理寄存器的位 [7:0]。

额外的延迟并不意味着吞吐量降低。该程序可以 运行 每 2 个时钟迭代 1 次,即使所有 add 指令都有 2c 延迟(从读取 DH,未修改。)

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 的 运行s,即使它具有 setcc ahadd 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 ah, bl,它 运行 在两个 HSW/SKL 上每次迭代都在 5.0c 内(imul 吞吐量瓶颈)。 (注释掉的 store/reload 也有效,但 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。

更新:可能的证据表明 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 之后在 CPU 上产生 1c 吞吐量结果的原因,它正在做类似 运行 的块 movsx dx, al.在重命名 dx 与 RDX 分开而不是合并的 CPU 上,只能 运行 超过 1 个 IPC。所以我们可以得出结论,IvB 实际上仍然重命名 low8 / low16 寄存器,与完整寄存器分开,直到 Haswell 才放弃。 (但这里有些可疑:如果这个解释是正确的,我们应该在 AMD 上看到相同的 1c 吞吐量,它不会重命名部分寄存器。但我们没有,见下文。)

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

Haswell 结果具有神秘的 0.58c 吞吐量 movsx/zx r16, r8:

其他较早和较晚的 Haswell(和 CrystalWell)/Broadwell/Skylake 结果都是这两个测试的 1.0c 吞吐量。

  • HSW with 4.1.570.0 Jun 5 2013, BDW with 4.3.15787.0 Oct 12 2018, BDW 4.3.739.0 2017 年 3 月 17 日。

正如我在 github 上的链接 InstLat 问题中所报告的那样,movzx r32, r8 的 "latency" 数字忽略了移动消除,大概是像 movzx eax, al.[=47 这样的测试=]

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

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

但是 MOVSX r1_16, r2_8 测试也测量了 1c 延迟 on Sandybridge 所以也许我的理论关于 movsx r16, r8 测试是什么是错误的告诉我们。


On Ryzen(AIDA64 build 4.3.781.0 Feb 21 2018),我们知道它根本不做任何部分寄存器重命名,如果测试确实重复写入相同的 16 位寄存器,结果不会显示我们预期的 1c 吞吐量效果。我也没有在任何旧的 AMD CPU 上找到它,使用旧版本的 InstLatX64,如 K10 或 Bulldozer 系列。

## Instlat Zen tests of ... something?
  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

不知道为什么吞吐量不是所有的 0.25;似乎很奇怪。这可能是 0.58c Haswell 吞吐量效应的一个版本。 MOVZX 数字相同,读取 R8 并写入 R32 的无前缀版本的吞吐量为 0.25。也许 fetch/decode 上存在较大指令的瓶颈?但是 movsx r32, r16movsx r32, r8.

大小相同

独立注册测试显示了与英特尔相同的模式,不过,只有必须合并的测试有 1c 的延迟。 MOVZX也是一样。

## Instlat Zen separate-reg tests
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

挖掘机的结果也与此非常相似,但吞吐量当然较低。

https://www.uops.info/table.html confirms that Zen+ has the expected 0.25c throughput (and 1c latency) for MOVSX_NOREX (R16, R8),与 Instlat 在其单独注册测试中发现的相同。

也许 InstLat 对 MOVSX r16, r8(而非 MOVSX r1_16, r2_8)的吞吐量测试仅使用 2 或 3 个 dep 链,这对于现代 CPU 来说还不够吗?或者偶尔打破 dep 链以便 OoO exec 可以重叠一些?