问题描述
avx512 向量可以容纳64个int8值。 我想做以下事情:
- 从内存位置a加载16个连续值,说它们是1
- 从内存位置b加载16个连续值,说它们是2
- 从内存位置c加载16个连续值,说它们是3
- 从内存位置d加载16个连续值,说它们是4
- 产生具有以下模式的 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],1
和ymm1,[d]
即可。 (每个2微码,未微融合,p23 + p015)。同样,vpshufb
控制向量可以是64字节的内存源操作数。如果要避免加载任何常量,可能值得考虑使用vpuncklbw
/ hbw
和插入(@EOF's comment)的另一种策略,但这会带来更多洗牌。还是vpmovzxbd
加载+移位/合并?
性能分析
-
前端总成本:6微克。 (在SKX上为1.5个时钟周期)。使用
从8微秒/ 2个周期下降vinserti128
-
后端总成本:每个结果至少2个周期
- p23的4次加载
- 2个p5随机播放
- 2个p05合并(插入),希望排定为p0。 (当正在运行任何512位uops时,端口1的向量执行单元将关闭。它仍然可以运行诸如
imul
,lea
和简单整数之类的东西。)
(任何高速缓存未命中都会导致合并uops在数据到达时必须重播。)
仅运行 just 会在端口2/3(负载)和0、5(矢量ALU)的后端吞吐量上产生瓶颈。还有一些空间可以压缩前端的更多信息,例如将其存储在某个地方和/或在其他端口上运行的某些循环开销。或用于不理想的前端吞吐量。 Vector ALU工作将加剧p0 / p5瓶颈。
使用内部函数,clang的shuffle优化器可能会将屏蔽的广播转换为vinserti128
,但希望不会。而GCC可能不会发现这种去优化。您没有说使用什么语言,没有提到寄存器,所以我只会在答案中使用asm。足够容易地转换为C内在函数,可能是C#SIMD东西,或者您实际使用的任何其他语言。 (在生产代码中通常不需要手写asm或值得使用,特别是如果您希望可移植到其他编译器。)
也可以做一个vmovdqu
,vinserti128 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
的两个掩码,0xf0
和0xf000
1}}。