【问题标题】:4-way bytewise interleave 4x 16-byte vectors from memory, with AVX512使用 AVX512 从内存中 4 路按字节交错 4 个 16 字节向量
【发布时间】:2021-02-01 04:25:41
【问题描述】:

一个 avx512 向量可以容纳 64 个 int8 值。 我想做类似以下的事情:

  1. 从内存位置 a 加载 16 个连续值,假设它们是 1
  2. 从内存位置 b 加载 16 个连续值,假设它们是 2
  3. 从内存位置 c 加载 16 个连续值,假设它们是 3 个
  4. 从内存位置 d 加载 16 个连续值,假设它们是 4 个
  5. 产生一个具有以下模式的 avx512 向量:123412341234...1234。

注意:内存负载的 16 个值预计不会相同,如上例所示。

我知道如何在功能上通过加载然后洗牌来做到这一点。 但是,我想知道就已注册使用的吞吐量和预期吞吐量而言,最有效的方法是什么。

也许为此目的优化了一些奇怪的指令。

谢谢!

【问题讨论】:

  • 你有vpermt2b的AVX512VBMI(冰湖)吗?还是像 Skylake-X / Cascade Lake 一样只有 AVX512BW?或许你可以和vpermt2d (AVX512F) 结合,然后做一个最后的内线vpshufb,即使在冰湖上也可能会更好。
  • 另一个选项是 4x vpmovzxbd 加载,然后是左移和 OR。也许我们可以利用合并屏蔽来避免单独的 OR,方法是使用 vpshufb ymm0{k1}, ymm1, shuffle_left_shift_1_byte 之类的东西。那可能更糟,总共 7 次洗牌。
  • 只推荐skylake。
  • @PeterCordes 你不需要vpermt2b,vpermt2w 就足够了godbolt,因为此时你可以将字节交错成单词。最大的问题是让 gcc 将 xmm/ymm-registers 视为 ymm/zmm-registers。据我所知,您需要 intel-intrinsics,否则 gcc 坚持为它发出指令。
  • @EOF:你在车道内使用 vpunpckl/hbw 和 vinserti128 需要 7 次随机播放。与 2x vpermt2d ymm + vpermt2d zmm 和 vpshufb 交错只需要 4 次随机播放,如果这样可以将正确的元素放入正确的通道。 (但需要 3 个 shuffle-control 向量,所以只有在可以提升这些向量的循环中使用才好) Re:内在函数:是的,_mm512_castsi256_si512 通常避免浪费指令。

标签: x86 x86-64 micro-optimization avx512


【解决方案1】:

由于您提到吞吐量是一个主要问题,因此尽量减少 shuffle 端口的后端 uops 和/或尽量减少前端 uops 总量。 (see this re: perf analysis)。总体瓶颈将取决于周围的代码。

我认为最好的策略是将所有数据有效地放入一个向量的正确 128 位块(通道)中,然后使用 vpshufb (_mm512_shuffle_epi8) 修复它。

正常的 128 位通道插入加载 (vinserti128 ymm, ymm, mem, imm) 每条指令需要 2 个微指令:加载和合并,但 ALU 部分可以在 Skylake-X 上的任何端口上运行,p015,而不仅仅是端口 5 上的随机播放单元. (或者端口 1 上的向量 ALU 单元由于 512 位 uop 在飞行中而关闭,只是 p05)。 https://uops.info/ 和 https://agner.org/optimize/。

不幸的是,vinserti128没有微熔断,所以两个微指令必须分别通过前端1。

但是,vbroadcasti32x4 ymm{K}, [mem]does micro-fuse (RETIRE_SLOTS: 1.0) 所以我们可以通过合并掩码广播加载进行 1-fused-domain-uop 插入。合并屏蔽确实需要 ALU uop,显然能够在 p015* 上运行。 (内存源vinserti128 不能以这种方式解码为 1 uop,这非常愚蠢,但这确实需要提前准备好掩码寄存器。)

(*: uops.info detailed results 奇怪地显示没有实际在端口 0 上运行的微指令,but a ZMM version does。如果测试显示 ymm 版本(正在运行的 512 位微指令)实际上只在 p5 上运行,那么我猜测使用 0x00f0 合并掩码将广播加载到 ZMM 寄存器中。)

如果您可以提升 2 个随机控制向量的负载并设置掩码寄存器,我建议您这样做。 [a] 和[c] 可以是任何寻址模式,但像[rdi + rcx] 这样的索引寻址模式可能会破坏广播的微融合,使其不分层。 (或者 maybe not 如果它算作像 add eax, [rdi + rcx] 这样的 2 操作数指令,因此可以在 Haswell/Skylake 的后端保持微融合。)

## ahead of the loop
   mov         eax,  0xf0                   ; broadcast loads will write high 4 dwords
   kmovb       k1, eax
   vpmovzxbd   zmm6, [vpermt2d_control]     ; "compress" controls with shuffle/bcast loads
   vbroadcasti32x4   zmm7, [vpshufb_control]

## Inside the loop, the actual load+interleave
   vmovdqu     xmm0, [a]                 ; 1 uop, p23
   vmovdqu     xmm1, [c]                 ; 1 uop, p23
   vbroadcasti32x4  ymm0{k1}, [b]        ; 1 uop micro-fused, p23 + p015
    ; ZMM0 = 00... 00...  BBBBBBBBBBBBBBBB  AAAAAAAAAAAAAAAA
   vbroadcasti32x4  ymm1{k1}, [d]        ; 1 uop micro-fused, p23 + p015

   vpermt2d    zmm0, zmm6, zmm1          ; 1 uop, p5.  ZMM6 = shuffle control
    ;ZMM0 = DDDDCCCCBBBBAAAA  DDDDCCCCBBBBAAAA ...
   vpshufb     zmm0, zmm0, zmm7          ; 1 uop, p5.  ZMM7 = shuffle control
    ;ZMM0 = DCBADCBADCBADCBA  DCBADCBADCBADCBA ...

如果你想在循环之后 avoid vzeroupper,你可以使用 xmm/ymm/zmm16 和 17 或其他东西,在这种情况下你会想要 vmovdqu32 xmm20, [a],它比 VEX 编码的 @ 占用更多的代码大小987654346@.

随机播放常量:

default rel           ; you always want this for NASM
section .rodata
align 16
vpermt2d_control: db 0,4,16,20, 1,5,17,21, ...   ; vpmovzxbd load this
vpshufb_control:  db 0,4,8,12,  1,5,9,13, ...    ; 128-bit bcast load this
; The top 2x 128-bit parts of each ZMM is zero
; I think this is right; edits welcome with full constants (_mm512_set... syntax is fine)

如果我们用 vpermd 然后 vpshufb 洗牌一个 ZMM(在 3 次插入之后,见下文),我认为这将是相同的常数扩展 2 种不同的方式(将字节扩大到双字,或重复 4 次),做同样的洗牌ZMM 中的 16 个 dword,然后每个通道中的 16 个字节。所以你会在 .rodata 中节省空间。

(您可以按任何顺序加载:如果有理由期望其中 2 个源将首先准备好(存储转发,或者更有可能缓存命中,或者首先加载地址准备好),您可以将它们用作源vmovdqu 加载。或者将它们配对,以便合并 uop 可以更快地执行并在 RS aka 调度程序中腾出空间。我以这种方式配对它们以使随机播放控制常量更加人性化。)

如果这个不是在一个循环中(所以你不能提升常量设置)不值得花费 2 微秒来设置 k1,只是使用vinserti128 ymm0, ymm0, [b], 1 和ymm1, [d] 相同。 (每个 2 uop,非微融合,p23 + p015)。此外,vpshufb 控制向量可以是 64 字节的内存源操作数。如果您想避免加载任何常量,则可能值得考虑使用 vpuncklbw / hbw 和插入 (@EOF's comment) 的不同策略,但这会更加混乱。或者可能vpmovzxbd 加载+移位/合并?

性能分析

  • 前端总成本:6 微秒。 (SKX 为 1.5 个时钟周期)。使用vinserti128,从 8 微指令/2 个周期降低

  • 总后端成本:每个结果至少 2 个周期

    • p23 的 4 次加载
    • 2 p5 洗牌
    • 2 p05 合并(插入),希望安排到 p0。 (当任何 512 位微指令在运行时,端口 1 的向量执行单元会关闭。它仍然可以运行 imul、lea 和简单整数等内容。)

(任何缓存未命中都将导致合并微指令在数据到达时必须重播。)

运行只是这种背靠背将成为端口 2/3(负载)和 0、5(向量 ALU)的后端吞吐量的瓶颈。有一些空间可以通过前端挤压更多的微指令,例如将其存储在某个地方和/或在其他端口上运行的一些循环开销。或者对于不太完美的前端吞吐量。矢量 ALU 工作将导致 p0 / p5 瓶颈。

使用内在函数,clang 的 shuffle 优化器可能会将屏蔽的广播转换为 vinserti128,但希望不会。 GCC 可能不会发现这种去优化。您没有说您使用的是什么语言,并没有提到寄存器,所以我将在答案中使用 asm。很容易翻译成 C 内在函数,可能是 C# SIMD 的东西,或者你实际使用的任何其他语言。 (在生产代码中通常不需要或不值得使用手写 asm,尤其是如果您希望可移植到其他编译器。)


也可以做一个vmovdqu、vinserti128 ymm 和2​​x vinserti32x4 zmm。 (或等效的 1-uop 合并屏蔽广播负载)。但这会使 ILP 更差,无法合并,我们仍然需要 vpermd + vpshufb,因为 vpermb 需要 AVXM512VBMI(Ice Lake,而不是 Skylake-X)。

但是,如果您也有 AVX512VBMI,vpermb 在 Ice Lake 上只有 1 uop,因此 3x 插入 + vpermb 将是吞吐量的理想选择。 使用 merge-broadcats 进行插入将需要 2 个单独的合并掩码,0xf0(与 ymm 32x4 和 zmm 64x2 一起使用)和 0xf000(与 zmm 32x4 一起使用,最后加载 [d]),或者一些变化。

将vpermt2b 与并行插入设置一起使用会更糟:Ice Lake vpermt2b 花费 3 微秒 (p05 + 2p5)。


两个 shuffle 常量可以在内存中压缩到每个 16 字节:用 vpmovzxbd 加载 vpermt2d 向量以将字节扩展为 dwords,用 VBROADCASTI64X2 zmm1, m128 加载 vpshufb 控件以重复通道内 shuffle矢量 4 次。可能值得将两个常量放入同一个缓存行,即使这需要在循环外进行加载+洗牌。

如果你用 C 内部函数来实现它,只需使用 _mm512_set_epi8/32;编译器通常会通过不断传播来挫败你变得聪明的尝试。 Clang 和 gcc 有时足够聪明,可以为您压缩常量,但通常只是广播加载,而不是 vpmovzx。


脚注 1: Agner Fog 的指令表表明 VINSERTI32x4 z,z,m,i 可以微熔断(1 个前端 uop),但 uops.info 的 mechanical testing results 不同意:RETIRE_SLOTS: 2.0 匹配 UOPS_EXECUTED.THREAD : 2.0。可能是 Agner 表中的一个错字;具有立即数的内存源指令不会微熔断是正常的。

(也可能它在解码器和 uop 缓存中进行微融合,但不在后端;我认为 Agner 的微融合测试是基于 uop 缓存,而不是问题/rename 瓶颈或性能计数器。RETIRE_SLOTS 计算无序后端中的融合域 uops,在问题/重命名之前/期间可能未分层之后。)

但无论如何,VINSERTI32x4 绝对无助于解决问题/重命名瓶颈,这在紧密循环中更常见。而且我怀疑它是否真的在解码器/uop-cache 中进行了微熔断。不幸的是,Agner 的表格确实有错别字。


替代策略:vpermt2d 记忆中(没有优势)

在我想出使用广播负载作为 1-uop 插入之前,这具有更少的前端 uop,但代价是更多的 shuffle,以及为 4 个源中的 2 个从内存中进行更广泛的负载。我不认为这有什么好处。

vpermt2d ymm, ymm, [mem] 可以在 Skylake 上为前端微融合为 1 个负载+随机播放 uop。 (uops.info result:注意 RETIRE_SLOTS:1.0 与 UOPS_EXECUTED.THREAD:2.0)

这需要从四个 128 位内存操作数中的两个执行 256 位加载。如果它在没有 128 位加载的情况下越过缓存线边界,那将会更慢。 (如果进入未映射的页面,可能会出错)。它还需要更多的洗牌控制向量。但是可以节省前端 uops 与 vinserti128,但不能与合并掩码 vbroadcasti32x4

;; Worse, don't use
; setup: ymm6, zmm7: vpermt2d/q shuffle controls: zmm8: vpshufb control
    vmovdqu   xmm0, [a]                   ; 1 uop p23
    vmovdqu   xmm1, [b]                   ; 1 uop p23
    vpermt2d  ymm0, ymm6, [c]             ; 1 uop micro-fused, p23 + p5.  256-bit load
    vpermt2d  ymm1, ymm6, [d]             ; 1 uop micro-fused, p23 + p5

   vpermt2q    zmm0, zmm7, zmm1           ; 1 uop, p5
    ;ZMM0 = DDDDCCCCBBBBAAAA  DDDDCCCCBBBBAAAA ...
   vpshufb     zmm0, zmm0, zmm8           ; 1 uop, p5
    ;ZMM0 = DCBADCBADCBADCBA  DCBADCBADCBADCBA ...
  • 前端成本:6 微指令
  • 后端成本:端口 5 为 4 uops,p2/3 为 4 uops

可能对组合对和最终的 ZMM vpermt2d 或 q 使用相同的 shuffle 控制。也许用vpermt2q 组合对和vpermt2d 最后?我还没有真正考虑过这一点,您是否可以选择 ZMM 洗牌向量,以便低 YMM 可以用于组合具有不同元素大小的一对向量。应该不会吧。

不幸的是vpblendd ymm, ymm, [mem], imm8 没有微熔断。

如果您碰巧知道[a..d] 中的任何一个是如何相对于缓存线边界对齐的,那么您可以在执行 256 位加载时避免缓存线拆分,其中包括您想要的数据作为低或高 128位,适当地选择您的vpermt2d shuffle 控件。


混合数据顺序的替代策略,除非您有 AVX512VBMI

适用于 AVX512VBMI vpermb(冰湖)而不是 AVX512BW vpshufb
5 个融合域微指令,1 个向量常量,3 个掩码

通过使用不同的掩码广播将每个 16 字节源块的 4 个 dword 分配到单独的通道中来避免 vpermt2d,这样每个字节都会在某个地方结束,并且结果的每个 16 字节通道都有来自所有 4 个的数据向量。 (使用vpermb,不需要跨车道分​​布;如上所述,您可以使用0xf0 之类的掩码进行全车道掩蔽)

每个通道都有来自 a、b、c 和 d 的 4 个字节的数据,没有重复,因为每个掩码在每个半字节中都有不同的设置位。

# before the loop: setup
  ;mov      eax, 0x8421      ; A_mask.  Implicit, later merges leave these elements
  mov       eax, 0x4218      ; B_mask
  kmovw     k1, eax
  mov       eax, 0x2184      ; C_mask
  kmovw     k2, eax
  mov       eax, 0x1842      ; D_mask
  kmovw     k3, eax
  vbroadcasti32x4  zmm7, [inlane_shuffle]    ; for vpshufb


## Inside the loop, the actual load+interleave
  vbroadcasti32x4  zmm0, [a]
      ; ZMM0 = AAAA AAAA AAAA AAAA   (each A is a 4-byte chunk)
  vbroadcasti32x4  zmm0{k1}, [b]          ; b_mask = 0x4218
      ; ZMM0 = A3B2A1A0  AAB1A    AAAB0    B3A2A1A0
  vbroadcasti32x4  zmm0{k2}, [c]          ; c_mask = 0x2184
      ; ZMM0 = A3B2C1A0  AAB1C0   C3AAB0   B3C2A1A0
  vbroadcasti32x4  zmm0{k3}, [d]          ; d_mask = 0x1842
      ; ZMM0 = A3B2C1D0  D3A2B1C0 C3D2A1B0 B3C2D1A0

  vpshufb  zmm0, zmm0, zmm7    ; not lane-crossing >.<

使用 64 字节的 shuffle 掩码,您可以在每个通道中进行 shuffle,生成 DCBA...

这可能没用(没有vpermb),但我开始写这个想法,然后才意识到屏蔽广播不可能将[a]的前4个字节放入与[b] 的前 4 个字节相同的通道,以此类推。

掩码设置实际上可以优化为更小的代码和更少的前端微指令,但在 k2 和 k3 实际准备好使用之前会有更高的延迟。使用 ak reg 作为需要 16 个掩码位的 SIMD 指令的掩码会忽略掩码 reg 中的较高位,因此我们可以将掩码数据变为 1 并将其右移几次以在低 16 位中生成我们想要的掩码.

mov       eax, 0x42184218
                          ; 0x8421  A_mask
kmovd     k1, eax         ; 0x4218 in low 16 bits
kshiftrd  k2, k1, 12      ; 0x2184 in low 16 bits   ; 4 cycle latency, port  5 only.
kshiftrd  k3, k1, 8       ; 0x1842 in low 16

但同样,如果您有 vpermb,那么您只需要 2 个掩码,0xf0 和 0xf000,使用带有 vbroadcasti32x4 ymm{k1}, [b] 和 vbroadcasti64x2 zmm{k1}, [c] 的 0xf0 掩码。

【讨论】:

  • 我认为您可能在显示的代码中犯了错误。 ## 循环内部,实际加载+交错 vmovdqu xmm0, [a] ; 1 uop,p23 vmovdqu xmm1,[c]; 1 uop,p23 vbroadcasti32x4 ymm0{k1},[b]; 1 uop micro-fused, p23 + p015 vbroadcasti32x4 ymm1{k1}, [d] ; 1 uop 微熔,p23 + p015 ; YMM1 = DDDDDDDDDDDDDDDD BBBBBBBBBBBBBBBB。但是 ymm1 现在应该是 DD....DD CC ...CC 对吧?只要确保
  • @bumpbump:是的,谢谢,已修复。您当然可以根据需要配对负载,只要vpermt2d 控制向量将数据放在需要去的地方,但我将 C 和 D 配对。
  • 另一件小事:代码中的 vmovdqu 应该是 vmovdqu8 吗?
  • @bumpbump:不,我不需要在 vmovdqu xmm 加载中进行屏蔽,因此没有理由使用更长的 EVEX 编码(AVX512VL vmovdqu32 或 vmovdqu8)而不是更短的AVX1 vmovdqu 带有 2 字节 VEX 前缀。 AVX 是为未来兼容而精心设计的,无论最大向量长度是多少,都可以进行零扩展。使用 AVX512 编码的唯一原因是像[reg + 128] 这样的寻址模式,其中 EVEX 允许更短的 scaled-disp8 位移,但这里不是这种情况。或者,如果您想使用 x/y/zmm16 或更高版本来避免在完成后需要 vzeroupper。
  • @bumpbump: example 带有机器码转储以显示 insn 长度,对于 vmovss,它对于 AVX 和 AVX512 编码具有相同的助记符,与 vmovdqu / vmovdqu8/16/32/64 不同
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多