使用ymm寄存器作为"类似内存"的存储位置

Bee*_*ope 7 x86 assembly sse avx

考虑x86中的以下循环:

; on entry, rdi has the number of iterations
.top:
; some magic happens here to calculate a result in rax
mov [array + rdi * 8], rax ; store result in output array
dec rdi
jnz .top
Run Code Online (Sandbox Code Playgroud)

它很简单:有些东西计算结果rax(未显示)然后我们将结果存储到数组中,与我们索引时的顺序相反rdi.

我想转换上面的循环而不是对内存进行任何写入(我们可以假设未显示的计算不会写入内存).

只要循环计数rdi有限,我就可以使用ymmregs 提供的充足空间(512字节)来保存值,但实际上这样做似乎很尴尬,因为你不能"索引"任意寄存器.

一种方法是始终将ymm一个元素的整个"数组" 寄存器混洗,然后将元素插入新释放的位置.

像这样的东西:

vpermq  ymm3, ymm3, 10_01_00_11b ; left rotate ymm by qword
vpermq  ymm2, ymm2, 10_01_00_11b ; left rotate ymm by qword
vpermq  ymm1, ymm1, 10_01_00_11b ; left rotate ymm by qword
vpermq  ymm0, ymm0, 10_01_00_11b ; left rotate ymm by qword

vblenddd ymm3, ymm3, ymm2, 3     ; promote one qword of ymm2 to ymm3
vblenddd ymm2, ymm2, ymm1, 3     ; promote one qword of ymm1 to ymm2
vblenddd ymm1, ymm1, ymm0, 3     ; promote one qword of ymm0 to ymm1

pinsrq   xmm0, rax, 0  ; playing with mixed-VEX mode fire (see Peter's answer)
Run Code Online (Sandbox Code Playgroud)

这显示只处理16个寄存器中的4个,所以显然要做全部16个,这将是很多代码(32个指令).

有没有更好的办法?

不可预测的分支是不可取的,但我们仍然可以考虑使用它们的解决方案.

Pet*_*des 4

你不能vpinsrq进入 YMM 寄存器。只有 xmm 目标可用,因此它不可避免地将整个 YMM 寄存器的上通道清零。它是随 AVX1 作为 128 位指令的 VEX 版本一起引入的。AVX2 和 AVX512 未将其升级到 YMM/ZMM 目的地。我猜他们不想提供插入到高通道的功能,并且提供仍然只查看 imm8 最低位的 YMM 版本会很奇怪。

您将需要一个临时寄存器,然后使用 . 混合到 YMM 中vpblendd或者(在 Skylake 或 AMD 上)使用旧版 SSE 版本保持高位字节不变! 在 Skylake 上,使用旧版 SSE 指令编写 XMM 寄存器会对完整寄存器产生错误依赖。你想要这种虚假的依赖。(我还没有测试过这个;它可能会触发某种合并微操作)。但你不希望在 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
Run Code Online (Sandbox Code Playgroud)

额外的好处:这只需要 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
Run Code Online (Sandbox Code Playgroud)

当我们离开循环时,ymm15..2xmm1 和 0 充满了有价值的数据。如果它们位于底部,它们将运行相同的次数,但 ymm2 将是 xmm0 和 1 的副本。Ajmp进入循环而不执行vmovdqa第一个迭代器上的操作是一种选择。

每 4x magic,端口 5(movq + pinsrq)、12 vmovdqa(无执行单元)和 1x vinserti128(再次是端口 5)将花费 6 uops。因此,每 4 19 个微指令magic,即 4.75 个微指令。

您可以将vmovdqa+vinsert与第一个交错magic,或者在第一个之前/之后将其分开magic。在 之前你不能破坏 xmm0 vinserti128,但是如果你有一个空闲的整数寄存器,你可以延迟vmovq.

更多嵌套

另一个循环嵌套级别或另一个展开将大大减少指令量vmovdqa。不过,将数据重新整理到 YMM 规则中的成本是最低的。 从 GP regs 加载 xmm

AVX512可以给我们更便宜的int->xmm。(它允许写入 YMM 的所有 4 个元素)。但我不认为它避免了展开或嵌套循环的需要,以避免每次都触及所有寄存器。


附:

我对洗牌累加器的第一个想法是将元素向左洗牌。但后来我意识到这最终得到了 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
Run Code Online (Sandbox Code Playgroud)

AVX512:将向量索引为内存

对于将向量寄存器写入内存的一般情况,我们可以使用不同的掩码vpbroadcastq zmm0{k1}, rax对其他寄存器进行重复。具有合并掩码(其中掩码具有单个位集)的广播为我们提供了向量寄存器中的索引存储,但我们需要针对每个可能的目标寄存器的一条指令。zmmk1

创建蒙版

xor      edx, edx
bts      rdx, rcx          #  rdx = 1<<(rcx&63)
kmovq     k1, rdx
kshiftrq  k2, k1, 8
kshiftrq  k3, k1, 16
...
Run Code Online (Sandbox Code Playgroud)

从 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
Run Code Online (Sandbox Code Playgroud)

(请参阅文档vpcompressq:使用零掩码会将其写入的元素上方的所有元素归零)

要隐藏 vpcompressq 延迟,您可以将多个 dep 链放入多个 tmp 向量,然后vpor xmm0, xmm0, xmm1放在最后。(其中一个向量将全为零,另一个向量将具有选定的元素。)

根据此 instatx64 报告,在 SKX 上,它具有 3c 延迟和 2c 吞吐量。