从 GP regs 加载 xmm

Loading an xmm from GP regs

假设您在 raxrdx 中有值要加载到 xmm 寄存器中。

一种方法是:

movq     xmm0, rax
pinsrq   xmm0, rdx, 1

虽然速度很慢!有没有更好的方法?

最近的 Intel 或 AMD 的延迟或 uop 计数不会做得更好(我主要查看 Agner Fog 的 Ryzen / Skylake 表格)。对于相同的端口,movq+movq+punpcklqdq 也是 3 微指令。

在英特尔/AMD 上,将 GP 寄存器存储到临时位置并使用 16 字节读取重新加载它们可能值得考虑吞吐量,如果 ALU 端口上的整数-> 向量周围代码瓶颈,这是端口5 最近的英特尔。

在 Intel 上,pinsrq x,r,imm 是端口 5 的 2 微指令,movq xmm,r64 也是端口 5 的 1 微指令。

movhps xmm, [mem] 可以微熔断负载,但它仍然需要一个端口 5 ALU uop。所以 movq xmm0,rax / mov [rsp-8], rdx / movhps xmm0, [rsp-8] 是 3 个融合域 uops,其中 2 个在最近的 Intel 上需要端口 5。存储转发延迟使得这个延迟明显高于插入。

用 store / store / movdqa 存储两个 GP regs(读取负载较大的两个较窄存储的长存储转发停顿)也是 3 微指令,但这是避免任何的唯一合理顺序端口 5 微指令。大约 15 个周期的延迟是如此之多,以至于乱序执行很容易难以隐藏它。


对于 YMM and/or 较窄的元素,商店 + 重新加载更值得考虑,因为您可以将摊位分摊到更多的商店/它可以节省更多的洗牌 uops。但它仍然不应该是您处理 32 位元素的首选策略。

对于较窄的元素,如果有一种将 2 个窄整数打包到 64 位整数寄存器中的单 uop 方式,那就太好了,因此设置更宽的传输到 XMM regs。但是没有: shld 在 Intel SnB 系列上是 1 uop,但需要寄存器顶部的输入之一。与 PowerPC 或 ARM 相比,x86 的位域 insert/extract 指令相当薄弱,每次合并需要多条指令(store/reload 除外,每个时钟 1 条的存储吞吐量很容易成为瓶颈。


AVX512F 可以 broadcast to a vector from an integer reg,合并屏蔽允许单 uop 插入。

根据 http://instlatx64.atw.hu/ 的电子表格(从 IACA 获取 uop 数据),将任何宽度的整数寄存器广播到 Skylake-AVX512 上的 x/y/zmm 向量仅需 1 个端口 5 uop。

Agner 似乎没有在 KNL 上测试整数源 regs,但类似的 VPBROADCASTMB2Q v,k(掩码寄存器源)是 1 uop。

已经设置了掩码寄存器:总共只有 2 微指令:

; k1 = 0b0010

vmovq         xmm0, rax           ; 1 uop p5             ; AVX1
vpbroadcastq  xmm0{k1}, rdx       ; 1 uop p5  merge-masking

认为 合并屏蔽是 "free" 即使对于 ALU 微指令也是如此。请注意,我们首先执行 VMOVQ,这样我们就可以避免对其进行更长的 EVEX 编码。但是,如果您在掩码 reg 中使用 0001 而不是 0010,则将其混合到带有 vmovq xmm0{k1}, rax.

的未掩码广播中

设置更多掩码寄存器后,我们可以为每个 uop 执行 1 个 reg:

vmovq         xmm0, rax                         2c latency
vpbroadcastq  xmm0{k1}, rdx   ; k1 = 0b0010     3c latency
vpbroadcastq  ymm0{k2}, rdi   ; k2 = 0b0100     3c latency
vpbroadcastq  ymm0{k3}, rsi   ; k3 = 0b1000     3c latency

(对于完整的 ZMM 向量,可能会启动第二个 dep 链和 vinserti64x4 以组合 256 位的一半。也意味着只有 3 k 个寄存器而不是 7 个。它需要 1 个额外的 shuffle uop,但除非有一些软件流水线,OoO exec 在你对向量做任何事情之前可能无法隐藏 7 merges = 21c 的延迟。)

; high 256 bits: maybe better to start again with vmovq instead of continuing
vpbroadcastq  zmm0{k4}, rcx   ; k4 =0b10000     3c latency
... filling up the ZMM reg

英特尔列出的 vpbroadcastq 在 SKX 上的延迟仍然是 3c,即使目的地只有 xmm,根据引用该数据和其他来源的 Instlatx64 电子表格。 http://instlatx64.atw.hu/

同一文档确实将 vpbroadcastq xmm,xmm 列为 1c 延迟,因此我们在合并依赖链中每一步获得 3c 延迟大概是正确的。不幸的是,合并屏蔽微指令需要目标寄存器与其他输入一样早准备好;所以操作的合并部分不能单独转发


k1 = 2 = 0b0010开始,我们可以用KSHIFT初始化其余部分:

mov      eax, 0b0010 = 2
kmovw    k1, eax
KSHIFTLW k2, k1, 1
KSHIFTLW k3, k1, 2

#  KSHIFTLW k4, k1, 3
# ...

KSHIFT 运行s 仅在端口 5 (SKX) 上,但 KMOV 也是如此;从整数寄存器中移动每个掩码只会花费额外的指令来首先设置整数寄存器。

如果向量的高位字节用广播而不是零填充实际上没问题,所以我们可以使用 0b1110 / 0b1100 等作为掩码。
我们最终写出了所有的元素。我们可以从 KXNOR k0, k0,k0 开始生成一个 -1 并左移它,但那是 2 port5 uops 与 mov eax,2 / kmovw k1, eax 是 p0156 + p5.


没有掩码寄存器:(没有kmov k1, imm,并且从内存加载需要多个微指令,所以一次性使用没有 3-uop 选项合并掩码。但是在一个循环中,如果你可以保留一些掩码规则,那似乎 far 更好。)

VPBROADCASTQ  xmm1, rdx           ; 1 uop  p5      ; AVX512VL (ZMM1 for just AVX512F)
vmovq         xmm0, rax           ; 1 uop p5             ; AVX1
vpblendd      xmm0, xmm0, xmm1, 0b1100    ; 1 uop p015   ; AVX2

; SKX: 3 uops:  2p5 + p015
; KNL: 3 uops: ? + ? + FP0/1

这里唯一的好处是 3 微码之一不需要端口 5。

vmovsd xmm1, xmm1, xmm0 也会混合两半,但在最近的 Intel 端口 5 上只有 运行s,不像整数立即数混合 运行s 在任何向量 ALU 端口上。


关于整数 -> 向量策略的更多讨论

gcc 喜欢 store/reload,这在任何情况下都不是最佳选择,除非在非常罕见的端口 5 绑定情况下,大量延迟无关紧要。我提交了 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80820 and https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80833,更多地讨论了 32 位或 64 位元素在各种体系结构上的最佳选择。

我建议在第一个错误中使用 AVX512 替换上述 vpbroadcastq 插入。

(如果编译 _mm_set_epi64x,一定要使用 -mtune=haswell 或最近的东西,以避免默认 mtune=generic 的糟糕调整。或者如果你的二进制文件将使用 -march=native只有 运行 在本地机器上。)