(但是BDW / SKL仍然使用2个微操作进行adc al,imm8
短格式编码,或其他没有ModRM的al / ax / eax / rax,imm8 / 16/32/32短格式。有关详细信息,请参见我的答案.)
adc
指令特殊处理为仅解码为单个uop。@BeeOnRope测试了这一点,并在他的中包含了对此性能问题的检查: https://github.com/travisdowns/uarch-bench。 在Haswell服务器上进行CI的示例输出显示了adc reg,0
与adc reg,1
或adc reg,清零寄存器
之间的差异。
(但仅适用于32位或64位操作数大小,不适用于adc bl,0
。因此,在使用setcc
结果上进行ADC时,使用32位当将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: 我在我的Core 2 E6600 (Conroe / Merom)上运行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
在旧CPU(如Core 2)上的表现不佳(它不知道如何访问所有事件,如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
这里有一个有趣的数字,IPC为0.9。
这是使用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都将具有1.15的整体IPC,因为前端不是瓶颈。(顺序Atom和P5 Pentium会略低,但xor和dec可以与P5上的adc或add配对。)
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分之1。 adc
中的1c vs. 2c延迟会产生比这更大的差异。使用与
0
不同的立即数重新构建可执行文件在Core 2上完全不改变时间,这是没有特殊情况的另一个强有力的迹象。这绝对值得测试。
我最初关注吞吐量(在每个循环迭代之前使用xor eax,eax
,让OoO执行重叠迭代),但很难排除前端效应。通过添加单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的循环缓冲区可以回收解码的uop,并避免连续的多uop指令解码瓶颈。
adc eax,0
有多少个uops?”因为这是同样的问题,而且这是一个事实请求,而不是解释它的文档。 - Peter Cordesadc eax, 0
和adc eax, 1
之间的明显差异(零运行得更快)。然而,在我的Kaby Lake电脑上(i7-7700K),运行相同的代码时,我根本看不到任何区别。我正在努力弄清楚这是否意味着adc eax, 0
变慢了,adc eax, 1
变快了,还是我的代码出了问题。这是我应该期望看到的吗? - David Wohlferdadc r,imm
作为单个uop运行,因此不需要特殊情况。因此,adc eax,1
肯定变得更快了,以及adc eax,ebx
和adc eax,[rsi]
。但是不包括adc [rdi],eax
;由于令人惊讶的微架构原因,这仍然是很多uops,即内部指令TLB一致性。 - Peter Cordes