长度更改前缀 (LCP) 是否会导致简单 x86_64 指令停顿?

Ols*_*ist 4 performance assembly x86-64 cpu-architecture micro-optimization

考虑一个简单的指令,例如

mov RCX, RDI          # 48 89 f9
Run Code Online (Sandbox Code Playgroud)

48 是 x86_64 的 REX 前缀。它不是LCP。但请考虑添加 LCP(用于对齐目的):

.byte 0x67
mov RCX, RDI          # 67 48 89 f9
Run Code Online (Sandbox Code Playgroud)

67 是地址大小前缀,在本例中用于没有地址的指令。该指令也没有立即数,并且不使用 F7 操作码(假 LCP 停止;F7 将是 TEST、NOT、NEG、MUL、IMUL、DIV + IDIV)。假设它也不跨越 16 字节边界。这些是 Intel优化参考手册中提到的 LCP 停顿情况。

该指令是否会导致 LCP 停顿(在 Skylake、Haswell 等上)?两个 LCP 怎么样?

我日常驾驶的是 MacBook。所以我无法访问 VTune,也无法查看 ILD_STALL 事件。还有其他方法可以知道吗?

Pet*_*des 6

TL:DR:67h在所有 CPU 上都是安全的。在 64 位模式1中,67h只能通过从moffs 32 位绝对地址(相对于该特殊地址的正常 64 位绝对地址addr32 movabs)加载/存储累加器 (AL/AX/EAX/RAX) 来进行LCP 停止。操作码)。

对于使用 ModRM 的普通指令,67h 不会改变长度,即使在寻址模式下使用 disp32 组件也是如此,因为 32 位和 64 位地址大小使用相同的 ModRM 格式。mov 的 67h-LCP-stallable 形式很特殊,并且不使用 modrm 寻址模式。

(几乎可以肯定,它在未来的 CPU 中不会有其他含义,例如作为更长操作码的一部分,方式rep3。)

长度更改前缀是指,如果忽略前缀,则操作码(+modrm)将暗示指令机器代码的非前缀部分的不同长度(以字节为单位)。 即它改变了指令其余部分的长度。 (并行长度查找很困难,并且与完整解码分开完成:16 字节块中的后续 insn 甚至没有已知的起点。因此,这个 min(16 字节,6 条指令)阶段需要视为前缀后尽可能少的位,以便正常快速情况工作。这是 LCP 可能发生停顿的阶段。)

通常仅使用实际的imm16 / imm32 操作码,例如66h在 中改变长度add cx, 1234,但add cx, 12在前缀后或在适当的模式下不改变 : add r/m16, imm8,并且add r/m32, imm8都是操作码 + modrm + imm8,无论如何都是 3 个字节,( https://www.felixcloutier.com/x86 /添加)。预解码硬件可以通过跳过前缀来找到正确的长度,而不是根据它所看到的修改后面的操作码 + modrm 的解释,这与66h操作码意味着 2 个立即字节而不是 4 个字节不同。汇编程序将始终在可能的情况下选择 imm8 编码因为它更短(或者对于 no-modrmadd ax, imm16特殊情况来说长度相等)。

mov r64, imm64(请注意,REX.W=1 对于vs.来说是长度变化的mov r32, imm32,但所有硬件都有效地处理相对常见的指令,因此只能66h并且67h可能实际上 LCP 停止。)

SnB 系列没有任何错误的2 LCP 停顿,可以为此操作码改变长度,但对于 66h 或 67h,这个特定指令则不然。与 Core2 和 Nehalem 不同,SnB 上这F7不是问题。(早期的 P6 系列 Intel CPU 不支持 64 位模式。)Atom/Silvermont 根本没有 LCP 处罚,AMD 或 Via CPU 也没有。


Agner Fog 的微架构指南很好地涵盖了这一点,并且解释得很清楚。搜索“长度变化的前缀”。(这个答案试图将这些部分放在一起,并提醒一些有关 x86 指令编码如何工作的信息等)

脚注 1:67h 在非 64 位模式下增加了更多的长度查找难度:

在 64 位模式下,67h从 64 位地址大小更改为 32 位地址大小,两者都使用disp0 / 8 / 32(0、1 或 4 字节立即位移作为指令的一部分),并且使用相同的ModRM + 可选 SIB 编码进行正常寻址模式。RIP+rel32 重新利用了 32 位模式的两种冗余方式的较短(无 SIB)编码来进行编码[disp32],因此长度解码不受影响。请注意,REX 已被设计为不改变长度(除了 mov r64、imm64),通过以与RBP 和 RSP 相同的方式对 R13 和 R12 施加负担作为 ModRM“转义码”来表示没有基本寄存器,或存在分别是SIB字节。

在 16 位和 32 位模式下,67h切换到 32 位或 16 位地址大小。不仅ModRM 字节后的长度不同[x + disp32](就像操作数大小前缀的立即数一样),而且 16 位地址大小也无法表示 SIB 字节。 为什么 x86 16 位寻址模式没有比例因子,而 32 位版本有比例因子? 因此,模式和 /rm 字段中的相同位可能意味着不同的长度。[x + disp16]

脚注 2:“假”LCP 失速

67h有时需要(参见脚注 1)以不同的方式查看 ModRM,甚至找出长度,这可能就是 Sandybridge 之前的 Intel CPU 在具有 ModRM 的任何指令的前缀上以 16/32 位模式出现“错误”LCP 停顿的原因,即使它们长度不变(例如寄存器寻址模式)。Core2/Nehalem 不是乐观地寻找长度并以某种方式进行检查,而是在看到 addr32 + 大多数操作码(如果它们不处于 64 位模式)时进行投注。

幸运的是,在 32 位代码中使用它的理由基本上为零,因此这主要只对使用 32 位寄存器而不切换到保护模式的 16 位代码重要。或者67h像您一样使用填充代码,但在 32 位模式下除外。 .byte 0x67/对于 Core 2 / Nehalem 来说mov ecx, edi 个问题。(我没有检查早期的 32 位 P6 系列 CPU。它们比 Nehalem 过时得多。)

假 LCP 停顿67h在 64 位模式下永远不会发生;如上所述,这是一个简单的情况,长度预解码器已经必须知道它们所处的模式,所以幸运的是,使用它进行填充没有任何缺点。rep与(可能成为未来某些操作码的一部分)不同,67h对于可以应用于某种形式的相同操作码的指令,即使实际上没有该操作数的内存操作数,也极有可能被安全地忽略。

Sandybridge 系列从来没有任何错误的 LCP 停顿,删除了 16/32 位模式地址大小 (67h) 和所有模式66 F7情况(需要查看 ModRM 来消除诸如neg dimul di来自 的指令的歧义test di, imm16。)

SnB 系列还删除了一些66h真正的LCP 停顿,例如从mov-immediate中mov word ptr [rdi], 0删除,这实际上很有用。

脚注 3:使用 67h 进行填充的前向兼容

67h一般应用于操作码时(即它可以使用内存操作数),对于具有恰好编码 reg,reg 操作数的 modrm 的相同操作码来说,它不太可能意味着其他含义。那么这对于现代 x86 上可以使用哪些方法来有效扩展指令长度是安全的?

事实上,GNU binutils 通过用地址大小前缀填充 来将6 字节“放松”call [RIP+rel32]为 5 字节,尽管这对于. (当链接使用 编译的代码时会发生这种情况,该代码用于当前编译单元中未找到的任何内容并且不具有“隐藏”可见性。)call rel32call rel3267hE8 call rel32-fno-pltcall [RIP + foo@gotpcrel]foo

但这不是一个好的先例:在这一点上,对于 CPU 供应商来说,想要打破特定的前缀+操作码组合(例如“rep ret”是什么意思?)的情况太普遍了但是程序中的一些自制的东西(比如)67h cdq不会得到供应商的待遇相同。


Sandybridge 系列 CPU 的规则

这些情况是从 Agner 的 microarch PDF 编辑/压缩的,它们可能会发生 LCP 停滞,在预解码中需要额外的 2 到 3 个周期(如果它们在 uop 缓存中丢失)。

  • 任何带有 的 ALU 操作imm16都不会imm32带有66h. (mov-immediate 除外)。
    • 请记住,mov并且test没有imm8更宽的操作数大小的形式,因此更喜欢test al, 1,或者imm32如果需要的话。或者有时即使test ah, imm8您想测试 AX 上半部分的位,但要注意在 HSW 上写入完整寄存器后读取 AH 时会出现 1 个周期的额外延迟。GCC 使用这个技巧,但也许应该开始小心使用它,也许有时bt reg, imm8在输入setccor时使用cmovcc(它不能像 JCC 那样与测试进行宏融合)。
  • 67h与 movabs moffs (64 位模式下的 A0/A1/A2/A3 操作码,也可能在 16 位或 32 位模式下)。当 LLVM 决定是否优化使用普通操作码 + modrm + sib + disp32 (以获得绝对而不是 rip-relative)时,我在 Skylake 上使用性能计数器进行的测试证实了这一点。这是指阿格纳指南的旧版本;我向他发送测试结果后,他很快就更新了。ild_stall.lcpmov al, [0x123456]67 A0 4-byte-address
  • 如果具有单个操作数的指令 NEG、NOT、DIV、IDIV、MUL 和 IMUL 之一具有 16 位操作数,并且操作码字节和 mod-reg-rm 字节之间存在 16 字节边界。这些指令具有伪造的长度更改前缀,因为这些指令与具有 16 位立即操作数的 TEST 指令具有相同的操作码 [...]无论对齐如何,SnB
    系列都不会受到任何惩罚。div cx
  • 地址大小前缀 (67H) 始终会导致任何具有 mod/reg/rm 字节的指令在 16 位和 32 位模式下出现延迟,即使它不会更改指令的长度。
    SnB-family 删除了这个惩罚,如果你小心的话,地址大小前缀可以用作填充。

或者用另一种方式总结:

  • SnB 系列没有错误的 LCP 停顿。

  • SnB 系列在所有真正的 LCP 上都有 LCP 停止,66h除了67h

    • mov r/m16, imm16mov r16, imm16无modrm版本。
    • 67h地址大小与 ModRM 交互(在 16/32 位模式下)。
      (这不包括 AL/AX/EAX/RAX 形式的 no-modrm 绝对地址加载/存储 - 它们仍然可以 LCP 停止,大概甚至在 32 位模式下,就像在 64 位模式下一样。)
  • 长度改变的 REX 不会停止(在任何 CPU 上)。


一些例子

(这部分忽略了某些 CPU 在某些非长度变化情况下出现的错误 LCP 停顿,这在此处并不重要,但这也许就是您担心67hmov reg,reg 的原因。)

在您的情况下,从 开始的其余指令字节,67无论当前地址大小是 32 还是 64,都会解码为 3 字节指令。即使使用类似这样的寻址模式也是如此mov eax, [e/rsi + 1024](reg+disp32) 或等寻址模式也是如此addr32 mov edx, [RIP + rel32]

在 16 位和 32 位模式下,67h在 16 位和 32 位地址大小之间切换。 [x + disp32]vs. ModRM 字节[x + disp16]后的长度不同,但非 16 位地址大小也可以根据 R/M 字段来表示 SIB 字节。但在64位模式下,32位和64位地址大小都使用[x + disp32],并且相同的 ModRM->SIB 或不编码。

只有一种情况是地址大小前缀在 64 位模式下67h长度发生变化:使用 8 字节与 4 字节绝对地址进行加载/存储,是的,它会导致 LCP 停止 Intel CPU。movabs(我在https://bugs.llvm.org/show_bug.cgi?id=34733#c3上发布了测试结果

例如,addr32 movabs [0x123456], al

.intel_syntax noprefix
  addr32 mov    [0x123456], cl   # non-AL to make movabs impossible
         mov    [0x123456], al   # GAS picks normal absolute [disp32]
  addr32 mov    [0x123456], al   # GAS picks A2 movabs since addr32 makes that the shortest choice, same as NASM does.
         movabs [0x123456], al   # 64-bit absolute address
Run Code Online (Sandbox Code Playgroud)

请注意,GAS(幸运的是)不会选择单独使用 addr32 前缀,即使使用as -Os( gcc -Wa,-Os) 也是如此。

$ gcc -c foo.s
$ objdump -drwC -Mintel foo.o
...
   0:   67 88 0c 25 56 34 12 00         mov    BYTE PTR ds:0x123456,cl
   8:   88 04 25 56 34 12 00    mov    BYTE PTR ds:0x123456,al   # same encoding after the 67
   f:   67 a2 56 34 12 00       addr32 mov ds:0x123456,al
  15:   a2 56 34 12 00 00 00 00 00      movabs ds:0x123456,al    # different length for same opcode
Run Code Online (Sandbox Code Playgroud)

正如您从最后 2 条指令中看到的,使用a2 mov moffs, al操作码和67指令的其余部分对于相同的操作码具有不同的长度。

在 Skylake 上造成 LCP 停顿,因此只有从 uop 缓存运行时才会很快。


当然,LCP 停顿的更常见来源是前缀66和 imm16(而不是 imm32)。就像add ax, 1234,在这个随机测试中,我想看看跳过 LCP 停止指令是否可以避免问题:Label in %rep section in NASM。但不是这样的情况add ax, 12,将使用(前缀add r/m16, imm8后的长度与66add r/m32, imm8)。

此外,据报道,Sandybridge 家族在 20 年内避免 LCP 摊位mov使用 16 位立即数避免了 LCP 停顿。

有关的:


调优建议和 uarch 详细信息:

通常不要试图节省空间addr32 mov [0x123456], al,除非需要在保存 1 个字节或使用 15 个字节的填充(包括循环内的实际 NOP)之间进行选择。(下面有更多调整建议)

对于 uop 缓存来说,一个 LCP 停顿通常不会是一场灾难,特别是如果长度解码可能不是这里的前端瓶颈(尽管如果前端根本就是瓶颈,那么通常可能会成为瓶颈)。不过,很难通过微基准测试来测试一个函数中的单个实例;只有真正的完整应用程序基准测试才能准确反映代码何时可以从 uop 缓存(英特尔性能计数器称为 DSB)运行,从而绕过传统解码 (MITE)。

现代 CPU 中的阶段之间存在队列,至少可以部分吸收停顿https://www.realworldtech.com/haswell-cpu/2/(比 PPro/PIII 中更多),并且 SnB 系列的 LCP 停顿时间比Core2/Nehalem。(但是预解码缓慢的其他原因已经影响了它们的容量,并且在 I-cache 未命中之后它们可能都是空的。)

当前缀长度不变时,找到指令边界的预解码管道阶段(在将字节块引导到实际的复杂/简单解码器或进行实际解码之前)将通过跳过所有前缀和来找到正确的指令长度/结束然后只查看操作码(以及 modrm,如果适用)。

这种预解码长度查找是 LCP 停顿发生的地方,所以有趣的事实是:即使是 Core 2 的预解码循环缓冲区也可以在后续迭代中隐藏 LCP 停顿,因为它在查找指令后锁定最多 64 字节/18 个 x86机器代码边界,使用解码队列(预解码输出)作为缓冲区。

在更高版本的 CPU 中,LSD 和 uop 缓存是解码后的,因此除非有什么东西破坏了 uop 缓存(例如令人讨厌的JCC 勘误缓解措施,或者只是在 32 字节对齐的 x86 机器代码块中为 uop 缓存提供了太多 uop) ,循环仅在第一次迭代时支付 LCP 停顿成本(如果它们尚未热的话)。

我想说的是,如果成本低廉的话,通常可以解决 LCP 停顿问题,尤其是对于通常运行“冷”的代码。或者,如果您可以只使用 32 位操作数大小并避免部分寄存器恶作剧,则通常只花费一个字节的代码大小,并且不需要额外的指令或微指令。或者,如果您连续出现多个 LCP 停顿,例如天真地使用 16 位立即数,那么缓冲区将有太多气泡无法隐藏,因此您将遇到真正的问题,并且值得花费额外的指令。(例如mov eax, imm32/ add [mem], ax,或movzx加载/添加 r32,imm32 / 存储,或其他。)


在指令边界处填充到结束 16 字节读取块:不需要

(这与在分支目标处对齐提取块的开始是分开的,考虑到 uop 缓存,这有时也是不必要的。)

Wikichip 关于Skylake 预解码的部分错误地暗示位于块末尾的部分指令必须自行预解码,而不是与包含该指令末尾的下一个 16 字节组一起预解码。它似乎是从 Agner Fog 的文本中转述的,但做了一些修改和补充,导致其错误:

[来自 wikichip...] 与以前的微架构一样,预解码器每个周期的吞吐量为 6 个宏操作,或者直到消耗完所有 16 个字节(以先发生者为准)。请注意,在前一个块完全耗尽之前,预解码器不会加载新的 16 字节块。例如,假设加载了一个新块,导致 7 条指令。在第一个周期中,将处理 6 条指令,而最后一条指令将浪费整个第二个周期。这将产生每周期 3.5 条指令的低得多的吞吐量,这远低于最佳值。
[这部分是从 Agner Fog 的 Core2/Nehalem 部分转述的,并添加了“完全”一词”]

同样,如果 16 字节块仅产生 4 条指令,其中接收到第 5 条指令的 1 字节,则前 4 条指令将在第一个周期中处理,最后一条指令将需要第二个周期。这将产生每个周期 2.5 条指令的平均吞吐量。[阿格纳指南的当前版本中没有出现这样的内容,我不知道这个错误信息是从哪里来的。也许是基于对阿格纳所说内容的误解而编造的,但未经测试。]

幸运的是没有。指令的其余部分位于下一个提取块中,因此现实更有意义:剩余字节被添加到下一个 16 字节块中。

(从这条指令开始启动一个新的 16 字节预解码块也是合理的,但我的测试规则是:2.82 IPC 具有重复的 5,6,6 字节 = 17 字节模式。如果它只是看起来16 字节,并将部分 5 或 6 字节指令留作下一个块的开始,这将为我们提供 2 个 IPC。)

多次展开的3x 5 字节指令的重复模式%rep 2500(NASM或 GAS.rept 2500块,因此约 36kiB 中的 7.5k 条指令)以 3.19 IPC 运行,预解码和解码每个周期约 16 字节。(16 字节/周期) / (5 字节/insn) =理论上每周期 3.2 条指令

(如果 wikichip 是正确的,它会预测 3-1 模式中接近 2 个 IPC,这当然是不合理的低,并且在从传统解码运行时对于长或中等长度的长时间运行来说不是英特尔可接受的设计.2 IPC 比 4 宽管道窄得多,即使对于传统解码也行不通。英特尔从 P4 中了解到,即使您的 CPU 缓存了解码的 uops,至少从传统解码中良好地运行也很重要。这就是为什么SnB 的 uop 缓存可以非常小,只有 ~1.5k uops。比 P4 的跟踪缓存小很多,但 P4 的问题是试图用跟踪缓存替换L1i,并且解码器很弱。(而且事实上它是一个跟踪缓存,所以它多次缓存相同的代码。))

这些性能差异足够大,您可以在 Mac 上使用足够大的重复计数来验证它,因此您不需要性能计数器来验证 uop 缓存未命中。(请记住,L1i 包含 uop 缓存,因此不适合 L1i 的循环也会从 uop 缓存中逐出自身。)无论如何,只需测量总时间并了解您将达到的近似 max-turbo 就足以满足像这样进行健全性检查。

即使在启动开销和保守的频率估计之后,比 wikichip 预测的理论最大值更好,也将完全排除这种行为,即使在没有性能计数器的机器上也是如此。

$ nasm -felf64 && ld       # 3x 5 bytes, repeated 2.5k times

$ taskset -c 3 perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,uops_issued.any,uops_retired.retire_slots,uops_executed.thread,idq.dsb_uops -r2 ./testloop

 Performance counter stats for './testloop' (2 runs):

            604.16 msec task-clock                #    1.000 CPUs utilized            ( +-  0.02% )
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
                 1      page-faults               #    0.002 K/sec                  
     2,354,699,144      cycles                    #    3.897 GHz                      ( +-  0.02% )
     7,502,000,195      instructions              #    3.19  insn per cycle           ( +-  0.00% )
     7,506,746,328      uops_issued.any           # 12425.167 M/sec                   ( +-  0.00% )
     7,506,686,463      uops_retired.retire_slots # 12425.068 M/sec                   ( +-  0.00% )
     7,506,726,076      uops_executed.thread      # 12425.134 M/sec                   ( +-  0.00% )
                 0      idq.dsb_uops              #    0.000 K/sec                  

         0.6044392 +- 0.0000998 seconds time elapsed  ( +-  0.02% )

(and from another run):
      7,501,076,096      idq.mite_uops             # 12402.209 M/sec                   ( +-  0.00% )
Run Code Online (Sandbox Code Playgroud)

不知道为什么idq.mite_uops:u不等于发布或退休。没有什么可以取消层压,并且不需要堆栈同步微指令,因此我不知道额外的已发布+已退役的微指令可能来自哪里。多余的部分在运行中是一致的,并且我认为与 %rep 计数成正比。

使用 5-5-6(16 字节)和 5-6-6(17 字节)等其他模式,我得到类似的结果。

当 16 字节组相对于绝对 16 字节边界未对齐或未对齐(nop在循环顶部放置 a)时,我有时会测量出细微的差异。但这似乎只发生在重复次数较多的情况下。 %rep 2500对于 39kiB 的总大小,我仍然得到 2.99 IPC(每个周期略低于一个 16 字节组),具有 0 DSB uops,无论对齐还是未对齐。

我仍然在 处得到 2.99IPC %rep 5000,但我在 处看到了差异%rep 10000:2.95 IPC 未对齐与 2.99 IPC 对齐。最大的 %rep 计数约为 156kiB,并且仍然适合 256k L2 缓存,因此不知道为什么任何内容都会与该大小的一半不同。(它们比 32k Li1 大得多)。我想早些时候我在 5k 时看到了不同的结果,但现在我无法重现它。也许这是 17 字节组的情况。


实际循环1000000在 下的静态可执行文件中运行时间_start,原始syscall到 _exit,因此整个过程的性能计数器(和时间)基本上只是循环。(特别是perf --all-user只计算用户空间。)

; complete Linux program
default rel
%use smartalign
alignmode p6, 64

global _start
_start:
    mov     ebp, 1000000

align 64
.loop:
%ifdef MISALIGN
    nop
%endif
 %rep 2500
    mov eax, 12345        ; 5 bytes.
    mov ecx, 123456       ; 5 bytes.  Use r8d for 6 bytes
    mov edx, 1234567      ; 5 bytes.  Use r9d for 6 bytes
 %endrep
    dec ebp
    jnz .loop
.end:

   xor edi,edi
    mov eax,231   ; __NR_exit_group  from /usr/include/asm/unistd_64.h
    syscall       ; sys_exit_group(0)
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

669 次

最近记录:

4 年,1 月 前