哪个英特尔微体系结构引入了 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,0
和 adc reg,1
或 adc 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
一样。此外,0
与 3
的一些吞吐量测试变体的循环计数相同,因此第一代 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 rbp
或 sub 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,imm
和 adc 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 不,
adc
比 add
慢,无论 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 都在延迟的关键路径上。
看起来英特尔忘记更新 adc
和 sbb
的其他直接形式的解码! (这同样适用于 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).
上的数据
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。 adc reg,0
和 adc reg,1
或 adc reg,zeroed-reg
.
(但仅适用于 32 位或 64 位操作数大小,而不是 adc bl,0
。因此使用 32 位
SBB 也一样。据我所知,对于具有相同立即值的等效编码,ADC 和 SBB 在任何 CPU 上的性能都没有任何差异。
imm=0
的优化是什么时候引入的?
我在Core 21上测试,发现adc eax,0
延迟是2个周期,和adc eax,3
一样。此外,0
与 3
的一些吞吐量测试变体的循环计数相同,因此第一代 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 rbp
或 sub 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,imm
和adc 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 不,
adc
比add
慢,无论 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 都在延迟的关键路径上。
看起来英特尔忘记更新 adc
和 sbb
的其他直接形式的解码! (这同样适用于 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:请参阅 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).
上的数据