【发布时间】:2019-01-10 20:38:10
【问题描述】:
Haswell 及更早版本上的 ADC 通常为 2 微指令,具有 2 个周期延迟,因为英特尔微指令传统上只能有 2 个输入 (https://agner.org/optimize/)。在 Haswell 为 FMA 和 micro-fusion of indexed addressing modes 在某些情况下引入了 3 输入 uop 之后,Broadwell / Skylake 以及后来的单 uop ADC/SBB/CMOV。
(但是 BDW/SKL 仍然使用 2 uop 进行 adc al, imm8 短格式编码,或其他 al/ax/eax/rax、imm8/16/32/32 短格式,没有 ModRM。更多详细信息在我的回答。)
但是 adc 立即数为 0 在 Haswell 上是特殊情况,只能解码为单个 uop。 @BeeOnRope tested this,并在他的 uarch-bench 中包含了对这个 performance quirk 的检查: https://github.com/travisdowns/uarch-bench。 Haswell 服务器上 CI 的示例输出显示 adc reg,0 和 adc reg,1 或 adc reg,zeroed-reg 之间的差异。
(但仅适用于 32 位或 64 位操作数大小,不适用于 adc bl,0。因此使用 32 位 when using adc on a setcc result 将 2 个条件组合到一个分支中。)
SBB 也是如此。据我所见,对于具有相同立即值的等效编码,任何 CPU 上的 ADC 和 SBB 性能都没有任何区别。
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:我在运行 Linux 的 Core 2 E6600 (Conroe / Merom) 上使用了这个测试程序。
;; 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,即额外的 0.15 来自 xor-zero 和 dec/jg,而 adc/add 链以每时钟 1 uop 的速度运行,延迟成为瓶颈。我们预计在任何其他具有单周期延迟adc 的 uarch 上的整体 IPC 为 1.15,因为前端不是瓶颈。 (有序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 次并显示平均值 + 方差)以及多次运行,表明循环计数可重复到 1000 分之一。@987654358 中的 1c 与 2c 延迟@ 会产生比这更多更大的不同。
使用0 以外的立即数重建可执行文件不会改变Core 2 上的时间根本,这是没有特殊情况的另一个强烈迹象。这绝对值得测试。
我最初关注的是吞吐量(在每次循环迭代之前使用xor eax,eax,让 OoO exec 重叠迭代),但很难排除前端影响。我想我终于确实通过添加单微指令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 没有解码的微指令缓存,它的循环缓冲区处于预解码阶段(在找到指令边界之后)。 4 个解码器中只有 1 个可以解码多微指令指令,因此adc 是前端的多微指令瓶颈。我想我本可以使用times 5 adc eax, 0 让这种情况发生,因为管道的某些后期阶段不太可能在不执行该微指令的情况下将其丢弃。
Nehalem 的循环缓冲区回收解码后的微指令,并避免背靠背多微指令指令的解码瓶颈。
【问题讨论】:
-
这是如何要求工具或文档的?这在任何地方都没有记录,AFAIK。如果算上英特尔“发布”硬件本身,那么任何性能问题都是题外话。我希望这被记录在 Agner Fog 的微架构指南中,但事实并非如此。这就是我问的原因。如果我问“在 Nehalem、SnB 和 IvB 上
adc eax,0有多少微指令?”,那么投票反对的人会更开心吗?因为这是同一个问题,它是对事实的要求,而不是对解释它的文档的要求。 -
嗯。我有一个常春藤桥(i7-3630QM)。但是,它运行的是 other 操作系统。摆弄你的代码,我能够让它在 Windows 上运行,我看到
adc eax, 0和adc eax, 1之间的明显区别(零运行得更快)。然而,在我的 Kaby Lake 盒子 (i7-7700K) 上运行相同的代码,我看不出有什么不同。我试图弄清楚这是否意味着adc eax, 0变慢了,adc eax, 1变快了,或者我的代码只是搞砸了。这是我应该期待看到的吗? -
@DavidWohlferd:谢谢!我们已经知道 Broadwell / Skylake(包括与 SKL 相同的 uarch 仅具有物理改进的 Kaby Lake)始终将
adc r,imm作为单个 uop 运行,因此不需要特殊情况。因此,adc eax,1、adc eax,ebx和adc eax,[rsi]肯定会更快。但不是adc [rdi], eax;由于surprising microarchitectural reasons:指令内TLB一致性,仍然有很多微指令。 -
原来我也有一个 Nehalem (i7-820QM)。我也看不出有什么不同。
-
@PeterCordes 恭喜您获得 10 万声望!!
标签: performance assembly x86 intel micro-optimization