【问题标题】:Which Intel microarchitecture introduced the ADC reg,0 single-uop special case?哪个英特尔微架构引入了 ADC reg,0 单 uop 特例?
【发布时间】:2019-01-10 20:38:10
【问题描述】:

Haswell 及更早版本上的 ADC 通常为 2 微指令,具有 2 个周期延迟,因为英特尔微指令传统上只能有 2 个输入 (https://agner.org/optimize/)。在 Haswell 为 FMA 和 micro-fusion of indexed addressing modes 在某些情况下引入了 3 输入 uop 之后,Broadwell / Skylake 以及后来的单 uop ADC/SBB/CMOV。

(但是 BDW/SKL 仍然使用 2 uop 进行 adc al, imm8 短格式编码,或其他 al/ax/eax/rax、imm8/16/32/32 短格式,没有 ModRM。更多详细信息在我的回答。)

但是 adc 立即数为 0 在 Haswell 上是特殊情况,只能解码为单个 uop。 @BeeOnRope tested this,并在他的 uarch-bench 中包含了对这个 performance quirk 的检查: https://github.com/travisdowns/uarch-bench。 Haswell 服务器上 CI 的示例输出显示 adc reg,0adc reg,1adc reg,zeroed-reg 之间的差异。

(但仅适用于 32 位或 64 位操作数大小,不适用于 adc bl,0。因此使用 32 位 when using adc on a setcc result 将 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:我在运行 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,即额外的 0.15 来自 xor-zero 和 dec/jg,而 adc/add 链以每时钟 1 uop 的速度运行,延迟成为瓶颈。我们预计在任何其他具有单周期延迟adc 的 uarch 上的整体 IPC 为 1.15,因为前端不是瓶颈。 (有序Atom和P5 Pentium会略低,但xor和dec可以与adc配对或添加到P5。)

在 SKL 上,uops_issued.any = instructions = 2.303G,确认 adc 是单 uop(它始终在 SKL 上,无论立即数具有什么值)。偶然地,jg 是新缓存行中的第一条指令,因此它不会与 SKL 上的dec 进行宏融合。使用 dec rbpsub ebp,1 代替,uops_issued.any 是预期的 2.2G。

这是非常可重复的:perf stat -r5(运行 5 次并显示平均值 + 方差)以及多次运行,表明循环计数可重复到 1000 分之一。@987654358 中的 1c 与 2c 延迟@ 会产生比这更多更大的不同。

使用0 以外的立即数重建可执行文件不会改变Core 2 上的时间根本,这是没有特殊情况的另一个强烈迹象。这绝对值得测试。


我最初关注的是吞吐量(在每次循环迭代之前使用xor eax,eax,让 OoO exec 重叠迭代),但很难排除前端影响。我想我终于确实通过添加单微指令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 没有解码的微指令缓存,它的循环缓冲区处于预解码阶段(在找到指令边界之后)。 4 个解码器中只有 1 个可以解码多微指令指令,因此adc 是前端的多微指令瓶颈。我想我本可以使用times 5 adc eax, 0 让这种情况发生,因为管道的某些后期阶段不太可能在不执行该微指令的情况下将其丢弃。

Nehalem 的循环缓冲区回收解码后的微指令,并避免背靠背多微指令指令的解码瓶颈。

【问题讨论】:

  • 这是如何要求工具或文档的?这在任何地方都没有记录,AFAIK。如果算上英特尔“发布”硬件本身,那么任何性能问题都是题外话。我希望这被记录在 Agner Fog 的微架构指南中,但事实并非如此。这就是我问的原因。如果我问“在 Nehalem、SnB 和 IvB 上 adc eax,0 有多少微指令?”,那么投票反对的人会更开心吗?因为这是同一个问题,它是对事实的要求,而不是对解释它的文档的要求。
  • 嗯。我有一个常春藤桥(i7-3630QM)。但是,它运行的是 other 操作系统。摆弄你的代码,我能够让它在 Windows 上运行,我看到 adc eax, 0adc eax, 1 之间的明显区别(零运行得更快)。然而,在我的 Kaby Lake 盒子 (i7-7700K) 上运行相同的代码,我看不出有什么不同。我试图弄清楚这是否意味着adc eax, 0 变慢了,adc eax, 1 变快了,或者我的代码只是搞砸了。这是我应该期待看到的吗?
  • @DavidWohlferd:谢谢!我们已经知道 Broadwell / Skylake(包括与 SKL 相同的 uarch 仅具有物理改进的 Kaby Lake)始终将 adc r,imm 作为单个 uop 运行,因此不需要特殊情况。因此,adc eax,1adc eax,ebxadc eax,[rsi] 肯定会更快。但不是adc [rdi], eax;由于surprising microarchitectural reasons:指令内TLB一致性,仍然有很多微指令。
  • 原来我也有一个 Nehalem (i7-820QM)。我也看不出有什么不同。
  • @PeterCordes 恭喜您获得 10 万声望!!

标签: performance assembly x86 intel micro-optimization


【解决方案1】:

根据我的微基准测试,结果可以在uops.info 上找到,这个优化是在 Sandy Bridge (https://www.uops.info/html-tp/SNB/ADC_R64_0-Measurements.html) 中引入的。 Westmere 不进行此优化 (https://uops.info/html-tp/WSM/ADC_R64_0-Measurements.html)。数据是使用 Core i7-2600 和 Core i5-650 获得的。

此外,uops.info 上的数据表明,如果使用 8 位寄存器(Sandy BridgeIvy BridgeHaswell),则不会执行优化。

【讨论】:

  • 既然您可以使用第一代SnB,也许您可​​以在Is performance reduced when executing loops whose uop count is not a multiple of processor width? 中解开谜团。 4 uop 循环每个时钟可以发出 1 个,但我在 SnB 上发现 7 uop 循环只能以每 2 个时钟 1 个运行,而不是 ~1.75,至少在有未分层时是这样。但我没有进行更详细的测试,也无法再访问 SnB,因此我们不知道 SnB 的循环缓冲区是否“展开”5 到 7 个 uop 循环以比 HSW 那样每 2 个时钟运行 1 个循环更快。
  • @PeterCordes - 我最近在考虑这个问题,我突然想到非常低的 uops(
  • @BeeOnRope:实际上,前几天我在写那条评论时也有同样的想法,也许采取分支的吞吐量会以某种方式成为一个问题。
【解决方案2】:

它不在 Nehalem 上,但在 IvyBridge 上。所以它在 Sandybridge 或 IvB 中都是新的。

我的猜测是 Sandybridge ,因为这是对解码器的重大重新设计(最多产生 4 个总微指令,而不是像 Core2 中可能的 4+1+1+1 模式/ Nehalem),如果下一条指令是 jcc,则如果它们是组中的最后一个,则继续使用可以宏融合的指令(如 addsub)。

对此很重要的是,我认为 SnB 解码器还会在立即计数移位中查看 imm8 以检查它是否为零,而不是仅在执行单元中执行此操作2

迄今为止的硬数据

  • Broadwell 及更高版本(以及 AMD 和 Silvermont/KNL)不需要此优化,adc r,immadc r,r 始终为 1 uop,除了 AL/AX/EAX/RAX 短格式1 在 Broadwell/Skylake。
  • Haswell 进行此优化:adc reg,0 为 1 uop,adc reg,1 为 2。适用于 32 位和 64 位操作数大小,而不是 8 位。
  • IvyBridge i7-3630QM 进行此优化(感谢@DavidWohlferd)。
  • 沙桥???
  • Nehalem i7-820QMadcadd 慢,不管 imm。
  • Core 2 E6600 (Conroe/Merom) 也不支持。
  • 可以假设 Pentium M 和更早版本没有。

脚注 1: 在 Skylake 上,没有 ModR/M 字节的 al/ax/eax/rax、imm8/16/32/32 短格式编码仍然解码为 2 微指令,即使当立即数为零。例如,adc eax, strict dword 0 (15 00 00 00 00) 的速度是83 d0 00 的两倍。两个微指令都处于延迟的关键路径上。

看起来英特尔忘记更新adcsbb 的其他直接形式的解码! (这同样适用于 ADC 和 SBB。)

默认情况下,对于不适合 imm8 的立即数,汇编程序将使用短格式,例如,adc rax, 12345 汇编为 48 15 39 30 00 00,而不是唯一选择的大一字节的单微指令格式用于累加器以外的寄存器。

adc rcx, 12345 而非 RAX 延迟为瓶颈的循环运行速度提高了一倍。但是adc rax, 123 不受影响,因为它使用的是单uop 的adc r/m64, imm8 编码。


脚注 2:如果后面的指令从 shl r/m32, imm8 读取标志,请参阅 INC instruction vs ADD 1: Does it matter? 以获取英特尔优化手册中有关 Core2 停止前端的引用,以防 imm8 为 0。(与implicit-1 操作码相反,解码器知道它总是写入标志。)

但是 SnB 家族不这样做; decoder 显然会检查 imm8 以查看指令是否无条件地写入标志或是否保持它们不变。所以检查 imm8 是 SnB 解码器已经做的事情,并且可以为adc 做有用的事情,以省略添加该输入的 uop,只将 CF 添加到目标。

【讨论】:

  • "adc r,imm" 实际上并不总是一个 1-μop 指令。在 Broadwell 和更高版本上:“adc (AL|*AX), imm” 特殊情况有两个 μops(参见,例如,uops.info/html-tp/SKL/ADC-2068-Measurements.html)。 IACA 对此也是错误的:它声称所有“adc R8, imm”(不仅仅是 AL 特例)都有两个 μops (uops.info/html-tp/SKL/ADC-2043-IACA3.0.html)。
  • 我对 IACA 的感觉是英特尔应该开源它,因为改进和仅来自“内部”的速度非常缓慢,而且各种利益相关方的综合知识似乎比 IACA 中嵌入的更大似乎人们愿意更新它。然而,现在,我们有来自 likwid 制造商的OSACA(所以你知道它将是高质量的软件)。假设作者愿意接受此类内容的 PR,我将继续使用并推荐它。
  • @AndreasAbel - 关于adcsbb 的eax 形式非常有趣的发现。我将它添加到我的Intel Perf Quirks 列表中。顺便说一句,直到现在才看到uops.info。看起来真棒!我不完全理解为什么这个 2-uop“错误”通常不会为 imm8 立即发送字节。 eax 特例在那种情况下不会更短吗?
  • @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 上的单微指令,adc ecx, 12345 也是。
  • @BeeOnRope:它们始终具有与寄存器相同宽度的立即数(rax 除外)。这就是为什么最近对此的编辑说“al/ax/eax/rax, imm8/16/32/32”。也许我应该在那些已经很杂乱的句子上加上“分别”。
猜你喜欢
  • 2018-01-27
  • 2019-04-18
  • 1970-01-01
  • 2016-08-30
  • 2014-04-15
  • 1970-01-01
  • 2020-05-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多