gpt4 book ai didi

assembly - 使用 ymm 寄存器作为 "memory-like"存储位置

转载 作者:行者123 更新时间:2023-12-02 21:37:20 25 4
gpt4 key购买 nike

考虑 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 提供的充足空间(512 字节) regs 来保存值,但实际上这样做似乎很尴尬,因为您不能“索引”任意寄存器。

一种方法是始终对 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 寄存器的上 channel 置零。它是在 AVX1 中作为 128 位指令的 VEX 版本引入的。 AVX2 和 AVX512 没有将其升级到 YMM/ZMM 目的地。我猜他们不想提供插入高车道的功能,而且提供仍然只查看 imm8 最低位的 YMM 版本会很奇怪。

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

显而易见的解决方案是给自己留一个临时注册用于 vmovq + vpblendd (而不是 vpinsrq y,r,0 )。这仍然是 2 uop,但是 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 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..2 和 xmm1 和 0 充满了有值(value)的数据。如果它们在底部,它们将运行相同的次数,但 ymm2 将是 xmm0 和 1 的副本。A jmp进入循环而不做 vmovdqa第一个迭代器上的东西是一个选项。

每 4x magic ,这花费了我们 6 uop 端口 5 (movq + pinsrq), 12 vmovdqa (无执行单元)和 1x vinserti128(再次端口 5)。所以这是每 4 个 19 uop magic ,或 4.75 uop。

您可以交织 vmovdqa + vinsert与第一 magic ,或者只是在第一个 magic 之前/之后分割它.你不能在 vinserti128 之后破坏 xmm0 , 但如果你有一个空闲的整数 reg 你可以延迟 vmovq .

更多嵌套

另一个循环嵌套级别, 或其他展开 ,会大大减少 vmovdqa的数量指示。不过,将数据混入 YMM regs 的成本最低。 Loading an xmm from GP regs .

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

PS:

我对随机累加器的第一个想法是将元素向左移动。但后来我意识到这最终得到了 5 个状态元素,而不是 4 个,因为我们在两个 reg 中有高和低,加上新编写的 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:索引向量作为内存

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

创建 mask :
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 .

关于assembly - 使用 ymm 寄存器作为 "memory-like"存储位置,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50915381/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com