哪个英特尔微体系结构引入了 ADC reg,0 单 uop 特例?

Which Intel microarchitecture introduced the ADC reg,0 single-uop special case?

Haswell 和更早版本上的 ADC 通常为 2 微指令,具有 2 个周期延迟,因为英特尔微指令传统上只能有 2 个输入(https://agner.org/optimize/). Broadwell / Skylake and later have single-uop ADC/SBB/CMOV, after Haswell introduced 3-input uops for FMA and micro-fusion of indexed addressing modes 在某些情况下。

(但是 BDW/SKL 仍然使用 2 微码用于 adc al, imm8 短格式编码,或者其他 al/ax/eax/rax, imm8/16/32/32 没有 ModRM 的短格式。更多细节在我的回答中。)

但是 adc 立即数 0 在 Haswell 上是特殊情况,只能解码为单个 uop。 , and included a check for this performance quirk in his uarch-bench: https://github.com/travisdowns/uarch-bench。 Haswell 服务器上 CI 的示例输出显示 adc reg,0adc reg,1adc reg,zeroed-reg.

之间的差异

(但仅适用于 32 位或 64 位操作数大小,而不是 adc bl,0。因此使用 32 位 将 2 个条件合并为一个分支。)

SBB 也一样。据我所知,对于具有相同立即值的等效编码,ADC 和 SBB 在任何 CPU 上的性能都没有任何差异。


imm=0 的优化是什么时候引入的?

我在Core 21上测试,发现adc eax,0延迟是2个周期,和adc eax,3一样。此外,03 的一些吞吐量测试变体的循环计数相同,因此第一代 Core 2 (Conroe/Merom) 不进行此优化。

回答这个问题最简单的方法可能是在 Sandybridge 系统上使用我下面的测试程序,看看 adc eax,0 是否比 adc eax,1 快。但是基于可靠文档的答案也可以。


脚注 1: 我在我的 Core 2 E6600 (Conroe / Merom) 上使用了这个测试程序,运行ning Linux.

;; NASM / YASM
;; assemble / link this into a 32 or 64-bit static executable.

global _start
_start:
mov     ebp, 100000000

align 32
.loop:

    xor  ebx,ebx  ; avoid partial-flag stall but don't break the eax dependency
%rep 5
    adc    eax, 0   ; should decode in a 2+1+1+1 pattern
    add    eax, 0
    add    eax, 0
    add    eax, 0
%endrep

    dec ebp       ; I could have just used SUB here to avoid a partial-flag stall
    jg .loop


%ifidn __OUTPUT_FORMAT__, elf32
   ;; 32-bit sys_exit would work in 64-bit executables on most systems, but not all.  Some, notably Window's subsystem for Linux, disable IA32 compat
    mov eax,1
    xor ebx,ebx
    int 0x80     ; sys_exit(0) 32-bit ABI
%else
    xor edi,edi
    mov eax,231   ; __NR_exit_group  from /usr/include/asm/unistd_64.h
    syscall       ; sys_exit_group(0)
%endif

Linux perf 在像 Core 2 这样的旧 CPU 上不能很好地工作(它不知道如何访问像 uops 这样的所有事件),但是它确实知道如何读取硬件计数器的周期和指令。够了。

我用

构建并分析了它
 yasm -felf64 -gdwarf2 testloop.asm
 ld -o testloop-adc+3xadd-eax,imm=0 testloop.o

    # optional: taskset pins it to core 1 to avoid CPU migrations
 taskset -c 1 perf stat -e task-clock,context-switches,cycles,instructions ./testloop-adc+3xadd-eax,imm=0

 Performance counter stats for './testloop-adc+3xadd-eax,imm=0':

       1061.697759      task-clock (msec)         #    0.992 CPUs utilized          
               100      context-switches          #    0.094 K/sec                  
     2,545,252,377      cycles                    #    2.397 GHz                    
     2,301,845,298      instructions              #    0.90  insns per cycle        

       1.069743469 seconds time elapsed

0.9 IPC 是这里有趣的数字。

这是关于我们对具有 2 uop / 2c 延迟的静态分析的期望 adc:循环中的 (5*(1+3) + 3) = 23 条指令,5*(2+3) = 25 延迟周期 = 每个周期循环迭代。 23/25 = 0.92.

Skylake 上是 1.15。 (5*(1+3) + 3) / (5*(1+3)) = 1.15,即额外的 .15 来自异或零和 dec/jg,而 adc/add 链 运行s 恰好每个时钟 1 uop,延迟瓶颈。我们希望在任何其他具有单周期延迟 adc 的 uarch 上也有 1.15 的总体 IPC,因为前端不是瓶颈。 (In-order Atom 和 P5 Pentium 会略低,但 xor 和 dec 可以与 adc 配对或添加到 P5。)

在 SKL 上,uops_issued.any = instructions = 2.303G,确认 adc 是单个 uop(它始终在 SKL 上,无论立即数有什么值)。偶然地,jg 是新缓存行中的第一条指令,因此它不会与 SKL 上的 dec 宏融合。使用 dec rbpsub ebp,1 代替,uops_issued.any 是预期的 2.2G。

这是非常可重复的:perf stat -r5(到 运行 它 5 次并显示平均值 + 方差),以及多个 运行s,表明循环计数是可重复的1000 分之 1。adc 中的 1c 与 2c 延迟会产生 更大的差异。

0 以外的立即数重建可执行文件根本不会改变 Core 2 上的时间 ,这是不存在特殊情况的另一个有力迹象。这绝对值得测试。


我最初关注的是吞吐量(在每次循环迭代之前使用 xor eax,eax,让 OoO exec 重叠迭代),但很难排除前端效应。我想我终于 did 通过添加单 uop add 指令避免了前端瓶颈。内部循环的吞吐量测试版本如下所示:

    xor  eax,eax  ; break the eax and CF dependency
%rep 5
    adc    eax, 0   ; should decode in a 2+1+1+1 pattern
    add    ebx, 0
    add    ecx, 0
    add    edx, 0
%endrep

这就是延迟测试版本看起来有点奇怪的原因。但是无论如何,请记住 Core2 没有解码的 uop 缓存,并且它的循环缓冲区处于预解码阶段(在找到指令边界之后)。 4 个解码器中只有 1 个可以解码多 uop 指令,因此 adc 是前端的多 uop 瓶颈。我想我可以让这种情况发生,使用 times 5 adc eax, 0,因为管道的某些后期阶段不太可能在不执行它的情况下丢弃该 uop。

Nehalem 的循环缓冲区回收已解码的 uops,并将避免背靠背多 uop 指令的解码瓶颈。

它不在 Nehalem 上,但在 IvyBridge 上。所以它在 Sandybridge 或 IvB 中都是新的。

我的猜测是 Sandybridge ,因为这是对解码器的重大重新设计(产生多达 4 个微指令,而不是像 4+1+1+1 这样的模式这在 Core2 / Nehalem 中是可能的),如果下一条指令是 jcc.

对于这一点很重要,我认为 SnB 解码器还会在立即计数移位中查看 imm8 以检查它是否为零,而不是仅在执行单元中这样做 2

目前硬数据:

  • Broadwell 及更高版本(以及 AMD 和 Silvermont/KNL)不需要此优化,adc r,immadc r,r 始终为 1 uop,除了AL/AX/EAX/RAX imm 短格式1 Broadwell/Skylake。
  • Haswell 做了这个优化:adc reg,0 是 1 uop,adc reg,1 是 2。对于 32 位和 64 位操作数大小,而不是 8 位。
  • IvyBridge i7-3630QM 做了这个优化(感谢@DavidWohlferd)。
  • 桑迪布里奇???
  • Nehalem i7-820QM adcadd 慢,无论 imm.
  • Core 2 E6600 (Conroe/Merom) 也没有。
  • 假设 Pentium M 和更早的版本不会。

脚注 1: 在 Skylake 上,没有 ModR/M 字节的 al/ax/eax/rax、imm8/16/32/32 短格式编码仍然解码为 2 微码,即使立即数为零。例如,adc eax, strict dword 0 (15 00 00 00 00) 比 83 d0 00 慢两倍。两个 uops 都在延迟的关键路径上。

看起来英特尔忘记更新 adcsbb 的其他直接形式的解码! (这同样适用于 ADC 和 SBB。)

对于不适合 imm8 的立即数,汇编程序默认使用短格式,因此例如 adc rax, 12345 汇编为 48 15 39 30 00 00 而不是大一字节的单 uop形式是除累加器以外的寄存器的唯一选择。

adc rcx, 12345 而不是 RAX 延迟上出现瓶颈的循环运行速度是原来的两倍。但是 adc rax, 123 不受影响,因为它使用 adc r/m64, imm8 编码,它是单 uop。


脚注 2:请参阅 引用英特尔优化手册中关于 Core2 在后续指令从 shl r/m32, imm8 读取标志时停止前端的内容,以防 imm8 为 0。(与隐式 1 操作码相反,解码器知道它总是写入标志。)

但 SnB 家族不这样做; 解码器 显然会检查 imm8 以查看指令是无条件写入标志还是保持它们不变。因此,检查 imm8 是 SnB 解码器已经做的事情,并且可以有用地为 adc 省略添加该输入的 uop,只留下将 CF 添加到目的地。

根据我的微基准测试,其结果可以在 uops.info, this optimization was introduced with Sandy Bridge (https://www.uops.info/html-tp/SNB/ADC_R64_0-Measurements.html). Westmere does not do this optimization (https://uops.info/html-tp/WSM/ADC_R64_0-Measurements.html) 上找到。数据是使用 Core i7-2600 和 Core i5-650 获得的。

此外,uops.info shows that the optimization is not performed if an 8-bit register is used (Sandy Bridge, Ivy Bridge, Haswell).

上的数据