Haswell及更早版本的ADC通常为2 uop,2周期延迟,因为英特尔uop传统上只能有2个输入(https://agner.org/optimize/)。Broadwell / Skylake及更晚版本有单uop ADC/SBB/CMOV,此前Haswell在某些情况下为FMA和micro-fusion of indexed addressing modes引入了3输入uop。
(But BDW/SKL仍然使用2个uop来进行adc al, imm8
短格式编码,或者其他al/ax/eax/rax、imm 8/16/32/32短格式编码,而不使用ModRM。
但是带有立即数0的**adc
在Haswell上是特殊情况,只作为一个uop解码。**@BeeOnRope测试了这一点,并在他的uarch-bench中包含了对这个performance quirk的检查:https://github.com/travisdowns/uarch-bench。Haswell服务器上CI的示例输出,显示adc reg,0
与adc reg,1
或adc reg,zeroed-reg
之间的差异。
(But仅适用于32位或64位操作数大小,不适用于adc bl,0
。因此,请使用32位when using adc on a setcc result将2个条件合并到一个分支中。)
SBB也是如此。据我所知,对于立即数相同的等效编码,任何CPU上的ADC和SBB性能都没有任何差异。
针对imm=0
的此优化是何时推出的?
我在酷睿21上进行了测试,发现adc eax,0
的延迟为2个周期,与adc eax,3
相同。此外,0
与3
在吞吐量测试的一些变体中的周期计数也相同,因此第一代酷睿2(Conroe/Merom)没有进行这种优化。
回答这个问题最简单的方法可能是在Sandybridge系统上使用我下面的测试程序,看看adc eax,0
是否比adc eax,1
快,但是基于可靠文档的答案也是可以的。
脚注1:我在运行Linux的酷睿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. (5*(1+3) + 3) / (5*(1+3)) = 1.15
上的IPC为1.15,即额外的0.15来自异或零和dec/jg,而ADC/add链每时钟运行1微操作,延迟瓶颈。我们预计任何其他单周期延迟为adc
的uarch上的IPC也为1.15。因为前端不是瓶颈。(按序Atom和P5 Pentium会稍微低一些,但xor和dec可以与ADC配对或添加到P5上。)
在SKL上,uops_issued.any
= instructions
= 2.303G,确认adc
为单微操作(它总是在SKL上,不管立即数具有什么值)。偶然地,jg
是新高速缓存线中的第一指令,因此它不与SKL上的dec
宏融合。uops_issued.any
是预期的2.2G。
这是极其可重复的:perf stat -r5
(运行5次,显示平均值+方差)和多次运行表明,周期计数可重复到千分之一。adc
中的1c与2c延迟相比,差异要大得多。
用0
以外的立即数重新构建可执行文件并没有改变酷睿2的计时,这是另一个没有特殊情况的明显迹象,绝对值得测试。
我最初关注的是吞吐量(在每次循环迭代之前使用xor eax,eax
,让Oooexec重叠迭代),但很难排除前端效应。我想我最终通过添加单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
这就是为什么延迟测试版本看起来有点奇怪。但是无论如何,记住Core 2没有解码uop缓存,它的循环缓冲区处于预解码阶段(在找到指令边界之后)。4个解码器中只有一个可以解码多uop指令,所以adc
是前端的多uop瓶颈。我想我可以让这种情况发生,使用times 5 adc eax, 0
,因为流水线的某些后续阶段不可能在不执行该uop的情况下将其抛出。
Nehalem的循环缓冲器回收解码的微指令,避免了连续多微指令的解码瓶颈。
2条答案
按热度按时间jyztefdp1#
根据我的微基准测试(其结果可以在uops.info上找到),桑迪Bridge(https://www.uops.info/html-tp/SNB/ADC_R64_0-Measurements.html)引入了此优化。韦斯特米尔没有进行此优化(https://uops.info/html-tp/WSM/ADC_R64_0-Measurements.html)。数据是使用酷睿i7-2600和酷睿i5-650获得的。
此外,uops.info上的数据显示,如果使用8位寄存器(Sandy Bridge、Ivy Bridge、Haswell),则不执行优化。
dsekswqp2#
Nehalem上没有,但是IvyBridge上有,所以在Sandybridge和IvB上都是新的。
我猜是Sandybridge,因为它对解码器进行了重大重新设计(总共产生多达4个微指令,而不是Core 2/ Nehalem中可能的4+1+1+1模式),如果指令组中的最后一条指令是
jcc
,则保留可以宏融合的指令(如add
或sub
)。值得注意的是,我认为SnB解码器也会在立即数移位中查看imm 8,以检查它是否为零,而不仅仅是在执行单元中这样做2。
迄今为止的硬数据:
adc r,imm
和adc r,r
始终为1 uop。除了AL/AX/EAX/RAXimm
非modrm短格式1在桤木Lake之前为2 uop。adc reg,0
为1 uop,adc reg,1
为2。适用于32位和64位操作数大小,不适用于8位。*桑德布里奇?
adc
比add
慢,与IMM无关。**脚注1:**在Skylake上,不带ModR/M字节的al/ax/eax/rax,imm 8/16/32/32短格式编码仍然解码为2个微指令,即使立即数为零。例如,
adc eax, strict dword 0
(15 00 00 00 00
)的速度是83 d0 00
的两倍。两个微指令都在延迟的关键路径上。看起来英特尔忘记了更新其他直接形式的
adc
和sbb
的解码!(这同样适用于ADC和SBB。)他们终于在桤木Lake P内核(金湾)中修复了这个问题;https://uops.info/分别测试adc AL,0
和adc AL, I8
以及adc R8l, 0
和adc R8l, I8
;Ice/Tiger/Rocket Lake之前的Intel主流CPU(包括P6家族和Sandybridge)运行adc al, 0
时为2 uop(Silvermont家族等低功耗CPU运行时为1 uop)。汇编器会默认使用短格式来处理不适合imm 8的立即数,因此例如
adc rax, 12345
汇编为48 15 39 30 00 00
,而不是一个字节的单微指令形式,后者是除累加器之外的寄存器的唯一选择。一个在
adc rcx, 12345
而不是RAX延迟上瓶颈的循环运行速度是原来的两倍,但是adc rax, 123
不受影响,因为它使用adc r/m64, imm8
编码,这是一个单一的uop。脚注2:请参阅INC instruction vs ADD 1: Does it matter?,了解英特尔优化手册中有关在imm 8为0的情况下,如果后面的指令从
shl r/m32, imm8
读取标志,则Core 2会停止前端的引文(与隐式-1操作码相反,解码器知道隐式-1操作码总是写入标志)。但SnB家族不会这么做显然,decoder 会检查imm 8,以查看指令是否无条件写入标志,或者是否保持标志不变。因此,检查imm 8是SnB解码器已经做过的事情,并且可以有效地为
adc
做,以省略添加该输入的uop,只将CF添加到目的地。