Haswell和更早版本上的ADC通常为2 UOP,具有2个周期的延迟,因为Intel UOP传统上只能有2个输入(
https://agner.org/optimize/
). 在Haswell为FMA和CMOV引入3输入uop之后,Broadwell/Skylake和后来的版本都有单uop ADC/SBB/CMOV
micro-fusion of indexed addressing modes
在某些情况下。
(但是BDW/SKL仍然使用2 uops作为
adc al, imm8
短格式编码,或其他al/ax/eax/rax、imm8/16/32/32短格式,无ModRM。我的回答中有更多细节。)
但是
adc
立即数为0的特殊大小写在Haswell上,只能作为单个uop解码。
@BeeOnRope tested this
,还包括一张支票
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
将两个条件合并为一个分支。)
SBB也是。据我所见,在任何CPU上,ADC和SBB的性能都没有任何区别,对于具有相同即时值的等效编码。
这是什么时候的优化
imm=0
介绍?
我在核心2测试过
一
,并发现
adc eax,0
延迟为2个周期,与
adc eax,3
. 并且对于吞吐量测试的一些变化,循环计数是相同的
0
与。
3
,所以第一代Core2(Conroe/Merom)没有进行这种优化。
回答这个问题最简单的方法可能是在Sandybridge系统上使用下面的测试程序,看看
adc eax,0
比
adc eax,1
. 但基于可靠文件的答案也可以。
脚注1
:我在运行Linux的Core2E6600(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
在旧的CPU(如Core2)上工作得不太好(它不知道如何访问UOP之类的所有事件),但它知道如何读取HW计数器的周期和指令。够了。
我用
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.9ipc是这里有趣的数字。
这就是我们从静态分析中所期望的2uop/2c延迟
模数转换器
:
(5*(1+3) + 3) = 23
循环中的指令,
5*(2+3) = 25
延迟周期=每个循环迭代的周期。23/25=0.92。
天湖1点15分。
(5*(1+3) + 3) / (5*(1+3)) = 1.15
,也就是说,额外的.15是来自xor zero和dec/jg,而adc/add链的运行速度正好是每时钟1 uop,延迟受到限制。我们期望在任何其他单周期延迟的uarch上使用1.15的IPC
模数转换器
因为前端不是瓶颈。(按顺序,Atom和P5 Pentium将略低,但xor和dec可以与adc或附加P5配对。)
在SKL上,
uops_issued.any
=
instructions
=2.303G,确认
模数转换器
是单uop(不管immediate的值是多少,它总是在SKL上)。碰巧,
jg
是新缓存线中的第一条指令,因此它不与
dec
在斯科尔。与
dec rbp
或
sub ebp,1
相反,
uops U发布。任何
是预期的2.2G。
这是极其可重复的:
perf stat -r5
(运行5次,显示平均值+方差),多次运行,显示循环计数可重复到1000分之一。1c与2c的延迟
模数转换器
会使
许多的
更大的差别。
使用除
零
不会改变时间
完全
在核心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指令,因此
模数转换器
前端的多uop瓶颈。我想我可以让它发生
times 5 adc eax, 0
,因为管道的某个后期阶段不太可能在不执行的情况下抛出该uop。
Nehalem的循环缓冲区回收已解码的uop,并将避免背对背多uop指令的解码瓶颈。