Haswell及更早版本的ADC通常为2 uops,具有2个周期延迟,因为Intel uops传统上只能有2个输入(https://agner.org/optimize/)。 Broadwell / Skylake后来有单uop ADC / SBB / CMOV,在Haswell在某些情况下为FMA和micro-fusion of indexed addressing modes引入了3输入uop。
(但不是adc al, imm8
短格式编码,或其他al / ax / eax / rax,imm8 / 16/32/32短格式,没有ModRM。我的答案中有更详细的信息。)
但是,具有0的adc
特别适用于Haswell解码为仅一个uop。 @BeeOnRope tested this,并在他的uarch-bench中包括了对这个performance quirk的支票:https://github.com/travisdowns/uarch-bench。 CI on a Haswell server的样本输出显示adc reg,0
和adc reg,1
或adc reg,zeroed-reg
之间的差异。
(对于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
更快。但基于可靠文档的答案也可以。
(顺便说一句,如果有人可以访问Sandybridge上的perf计数器,你也可以通过运行@ BeeOnRope的测试代码来清除Is performance reduced when executing loops whose uop count is not a multiple of processor width?的神秘面孔。或者我在不再使用的SnB上观察到的性能急剧下降仅仅是因为un - 层压与普通的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 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来自xor-zero和dec / jg,而adc / add链每个时钟正好以1 uop运行,在延迟方面存在瓶颈。我们期望这个1.15整体IPC在任何其他uarch上也具有单周期延迟adc
,因为前端不是瓶颈。 (有序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分中的1分.1c与adc
中的2c延迟会产生更大的差异比起那个来说。
使用0
以外的直接重建可执行文件并不会改变Core 2上的时间,这是另一个没有特殊情况的强烈信号。这绝对值得测试。
我最初看的是吞吐量(在每次循环迭代之前使用xor eax,eax
,让OoO exec重叠迭代),但很难排除前端效果。我想我最终通过添加单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指令的瓶颈问题。
根据我的微基准测试,其结果可以在uops.info上找到,这个优化是在Sandy Bridge(http://uops.info/html-tp/SNB/ADC_R64_I8-Measurements.html)中引入的。 Westmere不做这种优化(http://uops.info/html-tp/WSM/ADC_R64_I8-Measurements.html)。使用Core i7-2600和Core i5-650获得数据。
此外,uops.info上的数据表明,如果使用8位寄存器(Sandy Bridge,Ivy Bridge,Haswell),则不执行优化。
它不在Nehalem上,但在IvyBridge上。所以它在Sandybridge或IvB都是新的。
我的猜测是Sandybridge,因为这是对解码器的一次重大重新设计(产生多达4个uop,而不是Core2 / Nehalem中可能的4 + 1 + 1 + 1模式),并且依旧指示如果下一个指令是add
,它们是组中的最后一个,可以宏观融合(如sub
或jcc
)。
值得注意的是,我认为SnB解码器还会在立即计数移位中查看imm8以检查它是否为零,而不是仅在执行单元2中执行此操作。
到目前为止硬数据:
adc r,imm
和adc r,r
总是1 uop,除了AL / AX / EAX / RAX imm short form1adc reg,0
是1 uop,adc reg,1
是2。adc
比add
慢,无论imm。脚注1:在Skylake上,没有ModR / M字节的al / ax / eax / rax,imm8 / 16/32/32短格式编码仍然可以解码为2 uop,即使立即数为零。例如,adc eax, strict dword 0
(15 00 00 00 00
)的速度是83 d0 00
的两倍。两个uops都处于延迟的关键路径上。
看起来英特尔忘记更新其他直接形式的adc
和sbb
的解码! (这同样适用于ADC和SBB。)
默认情况下,汇编程序将使用短格式表示不适合imm8的immediates,因此例如adc rax, 12345
汇编为48 15 39 30 00 00
而不是单字节较大的单uop格式,这是除累加器之外的寄存器的唯一选项。
在adc rcx, 12345
而不是RAX延迟上遇到瓶颈的循环运行速度是原来的两倍。但adc rax, 123
不受影响,因为它使用单个uop的adc r/m64, imm8
编码。
脚注2:如果后来的指令从INC instruction vs ADD 1: Does it matter?读取标志,如果imm8为0,则请参阅shl r/m32, imm8
以获取英特尔优化手册中关于Core2停止前端的报价。(与隐含的1操作码相反,解码器始终知道写标志。)
但SnB家族并不这样做;解码器显然检查imm8以查看指令是否无条件地写入标记或是否使它们保持不变。因此,检查imm8是SnB解码器已经做过的事情,并且可以有效地为adc
省略添加该输入的uop,只留下添加CF到目的地。