Haswell及更早版本的ADC通常为2 uops,有2个周期延迟,因为Intel uops传统上只能有2个输入(https://agner.org/optimize/).在Haswell为FMA引入3输入微指令和某些情况下的索引寻址模式的微融合之后,Broadwell/Skylake及其后来都有单uop ADC/SBB/CMOV .
(但不适用于adc al, imm8
短格式编码,或其他al/ax/eax/rax,imm8/16/32/32短格式,没有ModRM.我的答案中有更详细的说明.)
但是adc
,即时0是特殊的Haswell解码为只有一个uop. @BeeOnRope测试了这个,并在他的uarch-bench中包含了对这个性能怪癖的检查:https://github.com/travisdowns/uarch-bench.从输出样本CI一个的Haswell服务器上示出之间的差adc reg,0
和adc reg,1
或adc reg,zeroed-reg
.
(对于SBB也是如此.就我所见,在任何CPU上具有相同立即数的等效编码,ADC和SBB性能之间从来没有任何差别.)
这个优化何时adc bl,0
推出?
我测试了Core 2 1,发现imm=0
延迟是2个周期,相同adc eax,0
.同时,也是循环计数是与吞吐量测试一些变化相同的adc eax,3
对比0
,所以第一代的Core 2(Conroe处理器/ Merom处理器)并没有这样做优化.
回答这个问题的最简单方法可能是在Sandybridge系统上使用我的测试程序,看看是否3
比它快adc eax,0
.但基于可靠文档的答案也可以.
(顺便说一句,如果有人可以访问Sandybridge上的perf计数器,你还可以通过运行@ BeeOnRope的测试代码来清除在执行uop计数不是处理器宽度倍数的循环时性能降低的谜团.或者是性能我在不再工作的SnB上观察到的只是因为未分层与正常的uops有什么不同?)
脚注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 adc eax,1
在像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延迟的静态分析的期望是perf
:adc
循环中的指令(5*(1+3) + 3) = 23
,延迟的周期=每循环迭代的周期.23/25 = 0.92.
Skylake的赔率为1.15. 5*(2+3) = 25
,即额外的.15来自xor-zero和dec/jg,而adc/add链每个时钟正好以1 uop运行,这在延迟方面存在瓶颈.我们期望这个1.15整体IPC在任何其他uarch上也具有单周期延迟(5*(1+3) + 3) / (5*(1+3)) = 1.15
,因为前端不是瓶颈.(有序Atom和P5 Pentium会略低,但xor和dec可以与adc配对或在P5上添加.)
在SKL上,adc
= uops_issued.any
= 2.303G,确认instructions
是单个uop(它总是在SKL上,无论立即有什么值).偶然的,adc
是新缓存行中的第一条指令,因此它不会与jg
SKL上的宏指令融合.有dec
或dec rbp
相反,sub ebp,1
是预期的2.2G.
这是非常可重复的:uops_issued.any
(跑5次,并显示平均+方差),以及多个运行,表明循环数为可重复1份在1000 1C与2C潜伏期perf stat -r5
会作出很多比这更大的区别.
重建可执行文件除了adc
不会改变Core 2上的时间,这是另一个没有特殊情况的强烈信号.这绝对值得测试.
我最初看的是吞吐量(0
在每次循环迭代之前,让OoO exec重叠迭代),但很难排除前端效果.我想,我终于做到避免通过增加单UOP前端瓶颈xor eax,eax
指令.内循环的吞吐量测试版本如下所示:
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指令,因此add
前端有多个uop瓶颈.我想我可以让这种情况发生,adc
因为管道的某个后期阶段不可能在不执行它的情况下抛出该uop.
Nehalem的循环缓冲区可以回收已解码的uop,并且可以避免解码背对背多uop指令的瓶颈问题.