assembly 哪种英特尔微体系结构引入了ADC reg,0单微指令特例?

a11xaf1n  于 2023-03-12  发布在  其他
关注(0)|答案(2)|浏览(126)

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,0adc reg,1adc 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相同。此外,03在吞吐量测试的一些变体中的周期计数也相同,因此第一代酷睿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的循环缓冲器回收解码的微指令,避免了连续多微指令的解码瓶颈。

jyztefdp

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 BridgeIvy BridgeHaswell),则不执行优化。

dsekswqp

dsekswqp2#

Nehalem上没有,但是IvyBridge上有,所以在Sandybridge和IvB上都是新的。

我猜是Sandybridge,因为它对解码器进行了重大重新设计(总共产生多达4个微指令,而不是Core 2/ Nehalem中可能的4+1+1+1模式),如果指令组中的最后一条指令是jcc,则保留可以宏融合的指令(如addsub)。

值得注意的是,我认为SnB解码器也会在立即数移位中查看imm 8,以检查它是否为零,而不仅仅是在执行单元中这样做2。

迄今为止的硬数据

  • Broadwell和更高版本(以及AMD和Silvermont/KNL)不需要此优化,adc r,immadc r,r始终为1 uop。除了AL/AX/EAX/RAX imm非modrm短格式1在桤木Lake之前为2 uop。
  • Haswell进行了以下优化:adc reg,0为1 uop,adc reg,1为2。适用于32位和64位操作数大小,不适用于8位。
  • IvyBridge i7- 3630 QM进行了此优化(感谢@DavidWohlferd)。
    *桑德布里奇?
  • Nehalem i7- 820 QMadcadd慢,与IMM无关。
  • 酷睿2 E6600(Conroe/Merom)也没有。
  • 可以放心地假设奔腾M和更早的没有。
    **脚注1:**在Skylake上,不带ModR/M字节的al/ax/eax/rax,imm 8/16/32/32短格式编码仍然解码为2个微指令,即使立即数为零。例如,adc eax, strict dword 015 00 00 00 00)的速度是83 d0 00的两倍。两个微指令都在延迟的关键路径上。

看起来英特尔忘记了更新其他直接形式的adcsbb的解码!(这同样适用于ADC和SBB。)他们终于在桤木Lake P内核(金湾)中修复了这个问题;https://uops.info/分别测试adc AL,0adc AL, I8以及adc R8l, 0adc 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添加到目的地。

相关问题