你不能将 vpinsrq
插入到 YMM 寄存器中。只有 XMM 目标可用,因此它不可避免地将完整的 YMM 寄存器的上半部分清零。它是AVX1作为128位指令的VEX版本引入的。 AVX2和AVX512没有将其升级为YMM / ZMM目的地。我猜想他们不想提供插入到高位的功能,而且提供一个仅查看imm8最低位的YMM版本会很奇怪。
你需要一个暂存寄存器,然后使用 vpblendd
将其混合到 YMM 中。或者(在 Skylake 或 AMD 上)使用旧版SSE版本以保持上半字节不变! 在 Skylake 上,使用旧版SSE指令写入 XMM 寄存器会对整个寄存器产生错误依赖。你希望出现这种错误依赖。(我还没有测试过;它可能会触发某种合并uop) 但是在Haswell上则不需要,因为它会保存所有YMM寄存器的上半部分,进入“状态C”。
明显的解决方案是留下一个scratch reg来用于vmovq+vpblendd(而不是vpinsrq y,r,0)。这仍然需要2个uops,但vpblendd在英特尔CPU上不需要5号端口,如果有影响的话。movq使用端口5。如果你真的很缺空间,mm0..7 MMX寄存器可用。
降低成本
通过嵌套循环,我们可以分割工作。稍微展开内部循环,我们就可以大部分消除这部分成本。
例如,如果我们的内部循环产生4个结果,我们可以在内部循环中使用您的暴力堆栈方法在2或4个寄存器上,没有实际的展开(“魔术”负载只出现一次),从而带来适度的开销。3或4个微操作,可选地没有循环传递的依赖链。
.outer:
mov r15d, 3
.inner:
%if AVOID_SHUFFLES
vmovdqa xmm3, xmm2
vmovdqa xmm2, xmm1
vmovdqa xmm1, xmm0
vmovq xmm0, rax
%else
vpunpcklqdq xmm2, xmm1, xmm2
vmovdqa xmm1, xmm0
vmovq xmm0, rax
%endif
dec r15d
jnz .inner
vmovdqa ymm15, ymm14
vmovdqa ymm13, ymm12
...
%if AVOID_SHUFFLES
vpunpcklqdq xmm1, xmm1, xmm0
vpunpcklqdq xmm4, xmm3, xmm2
vinserti128 ymm4, ymm1, xmm4, 1
vpxor xmm1, xmm1, xmm1
%else
vpunpcklqdq xmm3, xmm0, xmm1
vinserti128 ymm3, ymm2, xmm3, 1
vpxor xmm2, xmm2,xmm2
vpxor xmm1, xmm1,xmm1
%endif
sub rdi, 4
ja .outer
奖励:这只需要AVX1(在AMD上更便宜,将256位向量保持在内部循环之外)。我们仍然获得12 x 4个qword的存储空间,而不是16 x 4个。那只是一个任意的数字。
有限展开
我们可以像这样展开内部循环:
.top:
vmovdqa ymm15, ymm14
...
vmovdqa ymm3, ymm2
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..2和xmm1和0中充满了有价值的数据。如果它们在底部,它们将运行相同的次数,但ymm2将是xmm0和1的副本。跳转到循环入口而不执行第一次迭代上的vmovdqa操作是一个选择。
根据4x magic,这会消耗我们6个端口5的uops(movq + pinsrq),12个vmovdqa(没有执行单元)和1个vinserti128(再次是端口5)。因此,每4个magic需要19个uops,即4.75个uops。
您可以将vmovdqa + vinsert与第一个magic交错,或者在第一个magic之前/之后拆分它。您不能破坏xmm0,直到vinserti128之后,但如果您有一个空闲的整数寄存器,可以延迟vmovq。
更多嵌套
另一个循环嵌套层次或展开循环会大大减少vmovdqa指令的数量。 然而,将数据混洗到YMM寄存器中至少具有最小代价。
从GP regs加载xmm。
AVX512可以为我们提供更便宜的int-> xmm。 (它还允许写入YMM的所有4个元素)。 但我认为它不能避免需要展开或嵌套循环以避免每次都触及所有寄存器的需要。
PS:
对于洗牌累加器,我的第一个想法是将元素向左移动一位。但后来我意识到这样会有5个状态元素,而不是4个,因为我们有两个寄存器中的高低位以及新写入的xmm0。(并且可以使用vpalignr。)
这里留下一个例子,展示了使用vshufpd
可以做什么:在一个寄存器中将低位移动到高位,并合并另一个寄存器中的高位作为新的低位。
vshufpd xmm2, xmm1,xmm2, 01b
vshufpd xmm1, xmm0,xmm1, 01b
vmovq xmm0, rax
AVX512:将向量作为内存索引
对于一般情况下将向量寄存器写入内存,我们可以使用vpbroadcastq zmm0{k1}, rax
,并对其他zmm
寄存器重复此操作,但需要不同的k1
掩码。使用合并掩码(掩码只有一个位被设置)的广播操作可以让我们将数据索引存储到向量寄存器中,但我们需要为每个可能的目标寄存器编写一条指令。
创建掩码:
xor edx, edx
bts rdx, rcx
kmovq k1, rdx
kshiftrq k2, k1, 8
kshiftrq k3, k1, 16
...
从 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
的文档:使用零屏蔽将所有元素清零,包括它所写入的元素以上的所有元素。)
为了隐藏vpcompressq
的延迟,您可以将多个依赖链传递到多个临时向量中,然后在最后执行vpor xmm0, xmm0, xmm1
。(其中一个向量将全为零,另一个将具有所选元素。)
根据此instatx64报告,SKX的延迟为3c,吞吐量为2c。
rax
中的值的代码实际上是使用rdpmc
读取性能计数器的小型微基准测试,对于处理内存读写的基准测试,没有任何与保存结果相关的“多余”内存活动非常好。我正在uarch-bench中使用这个“一次性”模式。@Mysticial - BeeOnRopevpbroadcastq
和旋转掩码寄存器的巨大无分支暴力堆栈更好的选择了。 - Peter Cordes