使用AVX512,从存储器进行4路按字节交织4x 16字节向量 性能分析混合数据顺序的替代策略,除非您具有AVX512VBMI

问题描述

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

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

注意:与上面的示例一样,预计内存加载中的16个值将不相同。

我知道如何通过加载然后随机播放来实现此功能。 但是,我想知道最有效的方法是从已注册的已使用数量和预期吞吐量方面。

也许有一些针对此目的而优化的怪异指令。

谢谢!

解决方法

由于您将吞吐量作为主要考虑因素,因此,将混洗端口的后端uops最小化是一个好主意,和/或将前端uops总数最小化。 (see this re: perf analysis)。总体瓶颈将取决于周围的代码。

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

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

不幸的是,vinserti128并没有 微型保险丝,因此两个uops都必须分别通过前端 1

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

(*:uops.info detailed results奇怪地显示没有实际在端口0 but a ZMM version does上运行的uops。如果测试表明ymm版本(运行512位uops)实际上仅在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编码的vmovdqu

随机播放常数:

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,8,12,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(3x插入后,见下文),我认为这是相同的常量以2种不同的方式扩展(将字节扩展为dword,或重复4次),进行相同的改组到ZMM中的16 dword,然后在每个通道中为16字节。因此,您可以在.rodata中节省空间。

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

如果此不是循环(因此您无法提升固定设置),则不值得花费2 uops来设置k1 ,只需使用vinserti128 ymm0,ymm0,[b],1ymm1,[d]即可。 (每个2微码,未微融合,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位uops时,端口1的向量执行单元将关闭。它仍然可以运行诸如imullea和简单整数之类的东西。)

(任何高速缓存未命中都会导致合并uops在数据到达时必须重播。)

仅运行 just 会在端口2/3(负载)和0、5(矢量ALU)的后端吞吐量上产生瓶颈。还有一些空间可以压缩前端的更多信息,例如将其存储在某个地方和/或在其他端口上运行的某些循环开销。或用于不理想的前端吞吐量。 Vector ALU工作将加剧p0 / p5瓶颈。

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


也可以做一个vmovdquvinserti128 ymm和2x vinserti32x4 zmm。 (或等效的1 uop合并掩盖广播负载)。但这会使合并的ILP变得更糟,并且我们仍然需要vpermd + vpshufb,因为vpermb需要AVXM512VBMI(Ice Lake,而不是Skylake-X)。

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

vpermt2b与并行插入设置一起使用会更糟:Ice Lake vpermt2b的成本为3 oups(p05 + 2p5)。


这两个shuffle常量可以在内存中分别压缩为16个字节:使用vpermt2d加载vpmovzxbd向量以将字节扩展为dword,使用{{1}加载vpshufb控件}重复车道内随机播放向量4次。可能值得将这两个常量都放入同一条缓存行中,尽管这会在循环外造成负载+随机播放。

如果使用C内部函数实现此功能,则只需使用VBROADCASTI64X2 zmm1,m128;编译器通常会通过不断传播来击败您变得聪明的尝试。 Clang和gcc有时很聪明,可以为您压缩常量,但通常仅是广播加载,而不是vpmovzx。


脚注1: Agner Fog的说明表表明_mm512_set_epi8/32可以进行微熔(1个前端uop),但是uops.info的mechanical testing results对此表示反对:RETIRE_SLOTS:2.0符合UOPS_EXECUTED.THREAD:2.0。可能是Agner桌子上的错字;带有立即数的内存源指令不要微熔断是正常的。

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

但是无论如何,VINSERTI32x4绝对无助于解决问题/重命名瓶颈,这在紧密循环中更为常见。而且我怀疑即使在解码器/ uop-cache中,它实际上也是微熔丝的。不幸的是,阿格纳的桌子上确实有错字。


备用策略:VINSERTI32x4 z,z,m,i来自内存(无优势)

在我想出使用广播负载作为1-uop插入内容之前,它的前端uops较少,但代价是更多的混洗,并且需要为4个源中的2个进行更大的内存负载。我认为这没有任何优势。

vpermt2d可以在Skylake上微融合为前端的1个load + shuffle uop。 (uops.info result:注意RETIRE_SLOTS:1.0与UOPS_EXECUTED。线程:2.0)

这将需要从四个128位内存操作数中的两个进行256位加载。如果在没有128位负载的情况下越过缓存行边界,则速度会变慢。 (如果跨入未映射的页面,可能会出错)。它还将需要更多的随机控制向量。但是可以节省与vpermt2d ymm,[mem]相对的前端操作,但不能与合并屏蔽的vinserti128相对保存

vbroadcasti32x4
  • 前端费用:6微克
  • 后端成本:端口5的4微克和p2 / 3端口的4微克

对于合并对和最终的ZMM vpermt2d或q,可能使用相同的混洗控件。也许可以使用;; 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,[d] ; 1 uop micro-fused,p23 + p5 vpermt2q zmm0,zmm7,zmm1 ; 1 uop,p5 ;ZMM0 = DDDDCCCCBBBBAAAA DDDDCCCCBBBBAAAA ... vpshufb zmm0,zmm8 ; 1 uop,p5 ;ZMM0 = DCBADCBADCBADCBA DCBADCBADCBADCBA ... 来组合配对,最后使用vpermt2q吗?我是否还没有真正考虑过这一点,是否可以选择ZMM随机向量,以使低YMM可以用于组合具有不同元素大小的一对向量。可能不是。

不幸的是,vpermt2d并不是微型保险丝。

如果您碰巧知道vpblendd ymm,[mem],imm8中的任何一个相对于高速缓存行边界对齐的方式,那么在进行256位加载(其中包括所需的低位或低位数据)时,可以避免高速缓存行分裂。高128位,适当地选择您的[a..d]混洗控制。


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

可以使用AVX512VBMI vpermt2d(Ice Lake)代替AVX512BW vpermb
5个融合域微词,1个矢量常量,3个掩码

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

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

0xf0

使用64字节的混洗掩码,您可以在每个泳道中进行混洗,从而在每个泳道中产生DCBA ...,但要使用来自非对应源位置的数据。

这可能没有用(没有# 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,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,zmm7 ; not lane-crossing >.< ),但是我开始写这个想法,然后才意识到屏蔽广播不可能获得{{1 }}与vpermb的前4个字节位于同一通道,依此类推。

实际上,可以将掩码设置优化为更小的代码和更少的前端操作,但是实际上可以使用k2和k3之前有更高的延迟。将ak reg用作需要16个屏蔽位的SIMD指令的屏蔽会忽略屏蔽reg中的高位,因此我们可以将屏蔽数据取一并将其右移两次以产生所需的低16位屏蔽

[a]

但是同样,如果您有[b],则只需使用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,8 ; 0x1842 in low 16 vpermb的两个掩码,0xf00xf000 1}}。