你不能vpinsrq http://felixcloutier.com/x86/PINSRB:PINSRD:PINSRQ.html进入 YMM 寄存器。只有 xmm 目标可用,因此它不可避免地将整个 YMM 寄存器的上通道清零。它是随 AVX1 作为 128 位指令的 VEX 版本一起引入的。 AVX2 和 AVX512 未将其升级到 YMM/ZMM 目的地。我猜他们不想提供插入到高通道的功能,并且提供仍然只查看 imm8 最低位的 YMM 版本会很奇怪。
你将需要一个临时寄存器,然后混合到一个 YMM 中vpblendd
. 或者(在 Skylake 或 AMD 上)使用旧版 SSE 版本保持高位字节不变!在 Skylake 上,使用旧版 SSE 指令编写 XMM 寄存器会对完整寄存器产生错误的依赖关系。你want这种虚假的依赖。 (我还没有测试过这个;它可能会触发某种合并微指令)。但你不希望在 Haswell 上这样做,它会保存所有 YMM 规则的上半部分,进入“状态 C”。
显而易见的解决方案是给自己留一个临时注册表以用于vmovq
+vpblendd
(代替vpinsrq y,r,0
)。这仍然是 2 uop,但是vpblendd
在 Intel CPU 上不需要端口 5,以防万一。 (movq
使用端口 5)。如果你真的没有足够的空间,mm0..7
MMX 寄存器可用。
降低成本
通过嵌套循环,我们可以拆分工作。通过少量展开内循环,我们基本上可以消除这部分成本。
例如,如果我们有一个内部循环产生 4 个结果,我们可以在内循环中的 2 个或 4 个寄存器上使用暴力堆栈方法,从而提供适度的开销,而无需实际展开(“神奇”有效负载仅出现一次)。 3 或 4 个 uops,可选择不带环路传送的 dep 链。
; on entry, rdi has the number of iterations
.outer:
mov r15d, 3
.inner:
; some magic happens here to calculate a result in rax
%if AVOID_SHUFFLES
vmovdqa xmm3, xmm2
vmovdqa xmm2, xmm1
vmovdqa xmm1, xmm0
vmovq xmm0, rax
%else
vpunpcklqdq xmm2, xmm1, xmm2 ; { high=xmm2[0], low=xmm1[0] }
vmovdqa xmm1, xmm0
vmovq xmm0, rax
%endif
dec r15d
jnz .inner
;; Big block only runs once per 4 iters of the inner loop, and is only ~12 insns.
vmovdqa ymm15, ymm14
vmovdqa ymm13, ymm12
...
;; shuffle the new 4 elements into the lowest reg we read here (ymm3 or ymm4)
%if AVOID_SHUFFLES ; inputs are in low element of xmm0..3
vpunpcklqdq xmm1, xmm1, xmm0 ; don't write xmm0..2: longer false dep chain next iter. Or break it.
vpunpcklqdq xmm4, xmm3, xmm2
vinserti128 ymm4, ymm1, xmm4, 1 ; older values go in the top half
vpxor xmm1, xmm1, xmm1 ; shorten false-dep chains
%else ; inputs are in xmm2[1,0], xmm1[0], and xmm0[0]
vpunpcklqdq xmm3, xmm0, xmm1 ; [ 2nd-newest, newest ]
vinserti128 ymm3, ymm2, xmm3, 1
vpxor xmm2, xmm2,xmm2 ; break loop-carried dep chain for the next iter
vpxor xmm1, xmm1,xmm1 ; and this, which feeds into the loop-carried chain
%endif
sub rdi, 4
ja .outer
额外奖励:这只需要 AVX1(并且在 AMD 上更便宜,将 256 位向量保留在内部循环之外)。我们仍然获得 12 x 4 qwords 的存储空间,而不是 16 x 4。无论如何,这是一个任意数字。
有限展开
我们可以只展开内部循环,如下所示:
.top:
vmovdqa ymm15, ymm14
...
vmovdqa ymm3, ymm2 ; 12x movdqa
vinserti128 ymm2, ymm0, xmm1, 1
magic
vmovq xmm0, rax
magic
vpinsrq xmm0, rax, 1
magic
vmovq xmm1, rax
magic
vpinsrq xmm1, rax, 1
sub rdi, 4
ja .top
当我们离开循环时,ymm15..2andxmm1和0充满了有价值的数据。如果它们位于底部,它们会运行相同的次数,但 ymm2 将是 xmm0 和 1 的副本。jmp
进入循环而不执行vmovdqa
第一个 iter 上的东西是一个选项。
Per 4x magic
,端口 5 (movq +pinsrq) 需要 6 uops,12vmovdqa
(无执行单元)和 1x vinserti128(又是端口 5)。所以每 4 个 19 个微指令magic
,或 4.75 uop。
您可以交错vmovdqa
+ vinsert
与第一个magic
,或者只是在第一个之前/之后将其分开magic
。你不能破坏 xmm0 直到vinserti128
,但是如果你有一个空闲的整数寄存器,你可以延迟vmovq
.
更多嵌套
另一个循环嵌套级别,或另一个展开, 会大大减少vmovdqa
指示。不过,将数据重新整理到 YMM 规则中的成本是最低的。从 GP regs 加载 xmm https://stackoverflow.com/questions/50779309/loading-an-xmm-from-gp-regs.
AVX512可以给我们更便宜的int->xmm。 (它允许写入 YMM 的所有 4 个元素)。但我不认为它避免了展开或嵌套循环的需要,以避免每次都触及所有寄存器。
PS:
我对洗牌累加器的第一个想法是将元素向左洗牌。但后来我意识到这最终得到了 5 个状态元素,而不是 4 个,因为我们在两个寄存器中有高点和低点,加上新编写的 xmm0。 (并且可以使用 vpalignr。)
离开此处作为您可以做什么的示例vshufpd
:在一个寄存器中从低位移至高位,并合并另一寄存器中的高位作为新的低位。
vshufpd xmm2, xmm1,xmm2, 01b ; xmm2[1]=xmm2[0], xmm2[0]=xmm1[1]. i.e. [ low(xmm2), high(xmm1) ]
vshufpd xmm1, xmm0,xmm1, 01b
vmovq xmm0, rax
AVX512:将向量索引为内存
对于将向量寄存器写入内存的一般情况,我们可以vpbroadcastq zmm0{k1}, rax
并对其他重复zmm
注册到不同的k1
面具。具有合并掩码(其中掩码具有单个位集)的广播为我们提供了向量寄存器中的索引存储,但我们需要针对每个可能的目标寄存器的一条指令。
创建蒙版:
xor edx, edx
bts rdx, rcx # rdx = 1<<(rcx&63)
kmovq k1, rdx
kshiftrq k2, k1, 8
kshiftrq k3, k1, 16
...
To read从 ZMM 寄存器:
vpcompressq zmm0{k1}{z}, zmm1 ; zero-masking: zeros whole reg if no bits set
vpcompressq zmm0{k2}, zmm2 ; merge-masking
... repeat as many times as you have possible source regs
vmovq rax, zmm0
(请参阅文档vpcompressq http://felixcloutier.com/x86/VPCOMPRESSQ.html:使用零掩码会将其写入的元素上方的所有元素归零)
要隐藏 vpcompressq 延迟,您可以将多个 dep 链放入多个 tmp 向量,然后vpor xmm0, xmm0, xmm1
在最后。 (其中一个向量将全为零,另一个向量将具有选定的元素。)
在 SKX 上,它具有 3c 延迟和 2c 吞吐量,根据这个 instatx64 报告 https://github.com/InstLatx64/InstLatx64/blob/master/GenuineIntel0050654_SkylakeX_InstLatX64.txt.