考虑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


这很简单:有些东西会在rax中计算结果(未显示),然后我们将结果存储在数组中,并以与rdi索引相反的顺序存储。

我想转换上面的循环,不对内存进行任何写操作(我们可以假设未显示的计算结果不写入内存)。

只要rdi中的循环计数受到限制,我就可以使用ymm regs提供的足够的空间(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)


这仅显示了16个寄存器中的4个,因此显然要全部处理16个寄存器将需要大量代码(32条指令)。

有没有更好的办法?

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

最佳答案

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

您将需要一个暂存器,然后使用vpblendd混合到YMM中。或者(在Skylake或AMD上)使用旧版SSE版本使高字节保持不变!在Skylake上,用旧版SSE指令编写XMM reg对完整寄存器有错误的依赖性。您需要这种错误的依赖关系。 (我尚未测试过;它可能会触发某种合并的uop)。但是您不希望在Haswell上保存所有YMM reg的上半部分,进入“状态C”。

显而易见的解决方案是让自己成为vmovq + vpblendd(而不是vpinsrq y,r,0)使用的临时寄存器。那仍然是2微妙,但是vpblendd在Intel CPU上不需要端口5,以防万一。 (movq uses port 5). If you're really hard up for space, the 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个qword的存储空间,而不是16 x4。无论如何这是一个任意数字。

展开受限

我们可以展开内部循环,如下所示:

.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..2和xmm1和0充满了有价值的数据。如果它们在最底端,它们将运行相同的次数,但是ymm2将是xmm0和1的副本。可以选择jmp进入循环而无需在第一次迭代中执行vmovdqa的操作。

每4x magic,端口5(movq + pinsrq),12 vmovdqa(无执行单元)和1x vinserti128(端口5)的成本为6 uops。因此,每4个magic等于19微克,即4.75微克。

您可以将vmovdqa + vinsert与第一个magic交错,或仅在第一个magic之前/之后对其进行分割。只有在vinserti128之后才可以破坏xmm0,但是如果您有备用整数reg,则可以延迟vmovq

更多嵌套

另一个循环嵌套级别或另一个展开将大大减少vmovdqa指令的数量。不过,仅将数据改组为YMM registry的成本最低。 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
...


要从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延迟,您可以将多个dep链分为多个tmp向量,然后在最后加上vpor xmm0, xmm0, xmm1。 (一个向量全为零,另一个向量具有所选元素。)

在SKX上,它具有3c的延迟和2c的吞吐量,according to this instatx64 report

09-09 20:16