哪个英特尔微架构引入了ADC reg,0单uop特殊情况?

10
Haswell及之前的ADC通常是2个微操作,延迟为2个周期,因为Intel微操作传统上只能有2个输入(https://agner.org/optimize/)。Broadwell / Skylake及更高版本具有单微操作ADC / SBB / CMOV,在Haswell引入FMA和一些情况下的索引寻址模式的微融合后,引入了3个输入微操作。

(但是BDW / SKL仍然使用2个微操作进行adc al,imm8短格式编码,或其他没有ModRM的al / ax / eax / rax,imm8 / 16/32/32短格式。有关详细信息,请参见我的答案.)

但是,在Haswell上,使用立即数0的adc指令特殊处理为仅解码为单个uop。@BeeOnRope测试了这一点,并在他的中包含了对此性能问题的检查: https://github.com/travisdowns/uarch-bench。 在Haswell服务器上进行CI的示例输出显示了adc reg,0adc reg,1adc 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相同。通过几种吞吐量测试的变化,03的循环计数也相同,因此第一代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配对。)

在SKL上,uops_issued.any=instructions=2.303G,验证了adc是单个uop(无论立即数的值如何,在SKL上始终如此)。恰好,jg是新缓存行中的第一条指令,因此在SKL上它不会和dec宏观融合。使用dec rbpsub ebp,1uops_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指令解码瓶颈。


1
这是在请求工具或文档吗?据我所知,这里没有任何记录。如果你认为英特尔“发布”了硬件本身,那么任何性能问题都不是主题。我希望这在Agner Fog的微架构指南中有记录,但事实并非如此。这就是我提问的原因。谁给我点踩的人会不会更高兴,如果我问“在Nehalem、SnB和IvB上adc eax,0有多少个uops?”因为这是同样的问题,而且这是一个事实请求,而不是解释它的文档。 - Peter Cordes
1
嗯。我有一台Ivy Bridge(i7-3630QM)电脑。然而,它运行的是那个“其他”的操作系统。通过调整你的代码,我成功地让它在Windows上运行,并且我看到了adc eax, 0adc eax, 1之间的明显差异(零运行得更快)。然而,在我的Kaby Lake电脑上(i7-7700K),运行相同的代码时,我根本看不到任何区别。我正在努力弄清楚这是否意味着adc eax, 0变慢了,adc eax, 1变快了,还是我的代码出了问题。这是我应该期望看到的吗? - David Wohlferd
@DavidWohlferd:谢谢!我们已经知道Broadwell / Skylake(包括与SKL相同的uarch Kaby Lake,仅具有物理改进)始终将adc r,imm作为单个uop运行,因此不需要特殊情况。因此,adc eax,1肯定变得更快了,以及adc eax,ebxadc eax,[rsi]。但是不包括adc [rdi],eax;由于令人惊讶的微架构原因,这仍然是很多uops,即内部指令TLB一致性。 - Peter Cordes
1
原来我也有一台 Nehalem(i7-820QM)电脑。但我这里也没有看到任何区别。 - David Wohlferd
3
@PeterCordes 恭喜你获得10万声望值!!<3 - Tommylee2k
显示剩余2条评论
2个回答

10

由于您可以访问第一代SnB,也许您可以澄清执行uop计数不是处理器宽度的倍数的循环时性能是否降低?的谜团。一个4 uop循环可以每个时钟周期发出1个,但我发现在SnB上,一个7 uop循环只能以每2个时钟周期1个的速度运行,而不是约为1.75个,至少当存在非层压时如此。但我没有进行更详细的测试,也不再拥有SnB的访问权限,因此我们不知道SnB的循环缓冲区是否会将5到7 uop循环“展开”以使它们比HSW更快地运行每2个时钟周期1个。 - Peter Cordes
@PeterCordes - 我最近在思考这个问题,我发现对于非常低的uops(<10),其行为可能可以通过规则来解释,其中显然“正常”的taken分支吞吐量仅为每2个周期1次,只有“非常小”的循环才能访问允许每个周期1次的特殊行为。因此,7个uops(指令?)可能只是违反了“非常小”条件的点。也许“非常小”的东西甚至不是以uops或指令的形式进行测量的,而是以指令大小或uop缓存放置或其他一些东西为基础,但仍然在该测试中停止工作。 - BeeOnRope
@BeeOnRope:其实我在写那个评论的时候也有同样的想法,可能是由于分支预测错误导致了执行效率问题。 - Peter Cordes

5

在 Nehalem 上不存在,但在 IvyBridge 上存在。因此,它要么是在 Sandybridge 或 IvB 中新出现的。

我猜测是 Sandybridge,因为那是解码器的重大重新设计(产生最多 4 个总 uops,而不是像 Core2 / Nehalem 中可能的 4+1+1+1 模式),并保留可以宏融合的指令(如 addsub),如果它们是一组中的最后一个,以防下一条指令是 jcc

对于这个问题,我认为 SnB 解码器还会查看立即计数移位中的 imm8 是否为零,而不仅仅在执行单元中进行2

到目前为止的硬数据

  • Broadwell及更高版本(以及AMD和Silvermont/KNL)不需要此优化,adc r,immadc r,r始终为1个uop。除了AL/AX/EAX/RAX的imm无modrm短格式1在Alder Lake之前为2个uops。
  • Haswell进行了这种优化:adc reg,0为1个uop,adc reg,1为2个uop。适用于32位和64位操作数大小,而不是8位。
  • IvyBridge i7-3630QM也进行了这种优化(感谢@DavidWohlferd)。
  • Sandybridge ???
  • Nehalem i7-820QM没有进行此优化,adcadd慢,无论imm如何。
  • Core 2 E6600(Conroe/Merom)也没有。
  • 可以安全地假设Pentium M及更早版本也没有。

注脚1: 在Skylake上,没有ModR/M字节的al/ax/eax/rax、imm8/16/32/32短格式编码即使立即数为零仍然解码为2个uop。例如,adc eax, strict dword 015 00 00 00 00)比83 d0 00慢两倍。这两个uop都在延迟的关键路径上。

看起来Intel忘记更新adcsbb的其他立即形式的解码!(这同样适用于ADC和SBB。)他们最终在Alder Lake P-cores(Golden Cove)中修复了这个问题;https://uops.info/测试adc AL,0adc AL,I8adc R8l,0adc R8l,I8分开;r32也是如此。在Ice/Tiger/Rocket Lake之前的Intel主流CPU(包括P6系列和Sandybridge)将adc al,0作为2个uop运行。(像Silvermont系列这样的低功耗CPU将其作为1个uop运行。)

汇编器默认使用短格式来表示不适合于imm8的立即数,因此例如adc rax,12345汇编为48 15 39 30 00 00,而不是对于寄存器除累加器以外没有其他选项的单字节更大的单uop形式。

一个循环如果瓶颈在adc rcx, 12345而不是RAX延迟,运行速度会加快两倍。但是adc rax, 123不受影响,因为它使用的是adc r/m64,imm8编码,这是单uop。

注脚2:请参阅INC instruction vs ADD 1: Does it matter?,其中引用了英特尔优化手册关于Core2在读取来自shl r/m32, imm8的标志位时会阻塞前端的引用。这是针对imm8为0的情况。(与隐式-1操作码相反,解码器知道它总是写入标志位。)

但SnB系列不会这样做;解码器显然会检查imm8,以确定指令是否无条件地写入标志位或者是否保持不变。因此,检查imm8是SnB解码器已经执行的操作,并且可以有用地用于adc,以省略添加该输入的微操作,只留下将CF添加到目标的操作。


4
"adc r,imm"并不总是在Broadwell及以后的处理器上只有一个μop。特殊情况下,例如"adc (AL|*AX), imm"会有两个μops(参见http://uops.info/html-tp/SKL/ADC-2068-Measurements.html)。IACA也有错误:它声称所有 "adc R8, imm"(不仅是AL的特殊情况)都有两个μops(http://uops.info/html-tp/SKL/ADC-2043-IACA3.0.html)。 - Andreas Abel
1
我对IACA的感觉是英特尔应该开源它,因为改进非常缓慢,只能从“内部”进行,而各方利益相关者的综合知识似乎比嵌入在IACA中的知识更大,而且人们似乎愿意更新它。然而,现在我们有了来自likwid制造商的OSACA(所以您知道它将是高质量的软件)。我将继续使用和推荐OSACA,假设作者愿意接受此类PR。 - BeeOnRope
1
@AndreasAbel - 关于adcsbb的eax形式,真是个有趣的发现。我已经将它添加到我的英特尔性能问题列表中。顺便说一句,我从未见过uops.info,看起来很棒!我不完全明白为什么这个2-uop“错误”通常不适用于imm8立即数。在这种情况下,eax是否是特殊情况? - BeeOnRope
1
@BeeOnRope: adc eax, imm32 是5个字节。adc r/m32,imm8是3个字节,所以任何好的汇编器都会使用后一种编码来使用adc eax,-128..127。短格式编码只保存ModRM字节,不足以弥补imm8和imm32之间的3个字节差异。我知道英特尔有时会让rep movs微码在新的uarches上过时(次优),但在Broadwell/Skylake上忘记更新某些形式的insn的硬连解码似乎真的很奇怪。我检查过了,add bl,0在SKL上是单uop,adc ecx,12345也是。 - Peter Cordes
1
@BeeOnRope:它们始终具有与寄存器相同的立即数宽度(除了rax)。这就是为什么最近关于此的编辑说“al / ax / eax / rax,imm8 / 16/32/32”。也许我应该在那些已经混乱的句子中添加“分别”一词。 - Peter Cordes
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接