将浮点数从高 xmm 四字移动到低 xmm 四字

Posted

技术标签:

【中文标题】将浮点数从高 xmm 四字移动到低 xmm 四字【英文标题】:Move float from high xmm quadword to low xmm quadword 【发布时间】:2019-04-19 16:46:18 【问题描述】:

MOVHPD 将 xmm 寄存器的高位四字提取到内存中。

PEXTRQ 提取 xmm 寄存器的高位四字并将其放入整数寄存器(仅限整数)。

SHUFPD 随机播放。

VPSLLDQ 使高位四字清零。

是否有将浮点值从 xmm 寄存器的高位四字移动到同一 xmm 寄存器或另一个 xmm 寄存器的低位四字的指令?还是我总是必须通过记忆(添加额外的周期)?

更新: 基于@fuz 和@Peter Cordes 下面的 cmets,这就是我所做的。这会分别为 xmm0 的低位和高位四字调用舍入函数;由于特殊的舍入参数,必须为每个 qword 单独调用该函数,因此它不能是 SIMD 指令。目标是对 xmm0 中的每个 qwords 进行四舍五入,并将结果放入 xmm11。

movapd xmm2,xmm0 ;preserve both qwords of xmm0
call Round
movsd [scratch_register+0],xmm0 ; write low qword to memory
movhlps xmm0,xmm2
call Round
movsd [scratch_register+8],xmm0 ; write low qword to memory
movupd xmm11,[scratch_register]

更新#2: @Peter Cordes 展示了如何在没有记忆的情况下做到这一点:

movhlps  xmm2, xmm0   ; extract high qword for later
call Round            ; round the low qword
movaps   xmm3, xmm0   ; save the result
movaps   xmm0, xmm2   ; set up the arg
call Round            ; round the high qword
movlhps  xmm3, xmm0   ; re-combine into xmm3

【问题讨论】:

我认为unpckhpd 应该可以解决问题。 谢谢——我现在就去看看。 你确定这段代码是正确的? vunpckhpd 有三个操作数,所以看起来很奇怪。此外,这绝对看起来不正确。 unpckhpdvunpckhpd 应该做你期望他们做的事情。也许你想要vunpcklpdunpcklpd 代替? 您可以使用movhlps 了解这些指令只是移动字节。它们代表什么并不重要。 【参考方案1】:

请参阅Agner Fog's asm optimization guide,他关于 SIMD 的章节有一张 shuffle 指令表他们这样做),看看他们是否是你想要的。


将寄存器的高位 qword 广播到两个元素的最便宜的方法是 movhlps xmm0,xmm0(或者对于整数数据,如果您的代码可能在 Nehalem 上运行,请使用 punpckhqdq xmm0,xmm0 以避免 FPvec-int 绕过延迟。)

没有 AVX,movhlps 很好,因为它的 shuffle 与 unpckhpd 略有不同。

movhlps xmm3, xmm4 执行 xmm3[0] = xmm4[1];,保持 xmm3[1] 不变。 unpckhpd xmm3, xmm4 从 xmm3 和 xmm4 中取出高位 qwords,并按顺序将它们放入 xmm3 中。所以在目的地,高位qword移动到低位,然后从src复制高位qword。 xmm3[0] = xmm3[1]; xmm3[1] = xmm4[1]

但是unpcklpd 没用,它长了 1 个字节,并且和 SSE1 movlhps 做同样的事情。 (将 src 中的低 qword 复制到目标的高 qword,保持目标的低 qword 不变。)movapd 相同,始终使用 movaps 代替。

还有:代码大小:使用 xmm8..15 需要 REX 前缀,因此请选择您的寄存器分配以在尽可能少的指令中使用 xmm8..15(或已经需要 REX 前缀的指令,例如对于 r8..15 中的指针)。代码大小通常没什么大不了的,但其他一切都一样小通常是最好的。较小的指令通常会更好地打包到 uop 缓存中。


使用 AVX,您可以将vunpckhpd 与源操作数的任一顺序一起使用,第一个 src 的高位 qword 转到目标的低位 qword。 vmovhlps 没有代码大小优势(或其他性能优势),它们都可以使用 2 字节 VEX 前缀来实现 4 字节的最小指令大小。

例如vunpckhpd xmm0, xmm1, xmm0 就像 vmovhlps xmm0, xmm0,xmm1


您可以使用shufpd or vpshufd 来解决您要解决的问题。这是浪费代码大小,因为它需要立即数,但显然您没有意识到您可以使用shufpd xmm0, xmm0, 0b11 来获取(按此顺序):

xmm0[1] 的低位 qword(第一个 src 操作数,立即数的低位) 来自xmm0[1] 的高位 qword(第二个 src 操作数,立即数的高位)。

随机播放控件可以多次读取同一个输入元素。


有趣的是,NASM 编译器将只用两个操作数编译 VUNPCKHPD

NASM 允许您将 vaddps xmm0, xmm0, xmm1 之类的指令编写为 vaddps xmm0, xmm1,当它与第一个源相同时省略单独的目标操作数。

我很困惑,因为这些值是双精度的,而不是单精度的,但它确实有效。

一切都只是要复制的位/字节。除非您使用 FP 计算指令(例如 addpd / addps),否则“类型”无关紧要。 (您可以通过手册条目中是否存在“SIMD 浮点异常”部分来判断它是否关心作为 FP 位模式的位的含义。例如addps: https://www.felixcloutier.com/x86/addps#simd-floating-point-exceptions。 (但没有任何意外。唯一关心的指令是出于非常明显的原因,比如进行 FP 计算或类型转换,而不仅仅是复制数据。)

没有真正的 CPU 关心 PS 与 PD 指令的性能,但有些人关心 vec-int 与 vec-FP,所以不幸的是,使用pshufd 复制和洗牌 FP 数据并不总是一种胜利。或者使用 shufps 作为 2 源整数随机播放。

不幸的是,在 AVX512 之前没有通用的 2 源“整数”洗牌,只有 palignrpunpck 指令。在 AVX 之前,没有 FP copy-and-shuffle 指令。 (具有讽刺意味的是,vpermilpsvshufps dst, same,same, imm8 相比,除了内存源加载+shuffle 之外,带有立即数是多余的,并且出于代码大小的原因应避免使用。What's the point of the VPERMILPS instruction (_mm_permute_ps)?)


  movapd xmm2,xmm0 ;preserve both qwords of xmm0
  call Round
     movsd [scratch_register+0],xmm0 ; write low qword to memory
  movhlps xmm0,xmm2
  call Round

这是有效的洗牌,但不幸的是,它在第一轮的输出和第二轮的输入之间创建了错误的依赖关系。所以这两个调用不能并行工作。取而代之的是,在第一次调用之前复制时随机播放,最好放入一个您知道已经“死”一段时间的寄存器,或者是 xmm0 中值的依赖链的一部分,因此必须在它之前准备好。

  movhlps  xmm2, xmm0   ; extract high qword for later
  call Round                ; round the low qword
  movaps   xmm3, xmm0   ; save the result
  movaps   xmm0, xmm2   ; set up the arg
  call Round                ; round the high qword
  movlhps  xmm3, xmm0    ; re-combine into xmm3

除非您的手写 Round 函数不会触及的寄存器数量不足,否则您并不特别需要内存,而且它的效率并不高。

作为奖励,所有这些 movapsmovhlps 指令都只有 3 个字节长,并且它们的数量与您的版本中的指令数量相同。

另一种选择(尤其是如果您的输入在不同的寄存器中开始)是先到Round 高半部分,然后您可以使用movlhps 将高半部分放回xmm0。

顺便说一句,如果您有 SSE4.1,roundpd 可以使用 Nearest、向 +-Inf(ceil/floor)或向 0(截断)四舍五入到最接近的整数。


movsd [scratch_register+8],xmm0 ; write low qword to memory
movupd xmm11,[scratch_register]

永远不要这样做,窄存储 + 宽重载是有保证的存储转发停顿。 (约 10 个周期的额外延迟)。

使用 16 字节对齐的存储位置(例如,在堆栈上 [rsp+8] 或其他位置),以及unpckhpd xmm0, [scratch_register] 进行加载+随机播放

不幸的是,英特尔糟糕地设计了内存源 unpck 指令,因此它们需要一个 16 字节的内存源,而不仅仅是它们实际加载/使用的 8 个字节。有几种情况

【讨论】:

感谢您对选项的精彩总结。我编辑了我的问题,以显示我对你和 fuz 的信息做了什么。 非常感谢您的更新。我知道我需要避免记忆,你在我读完你的更新之前向我展示了如何(unf。我在此期间被叫去开会)。删除内存应该会更有效。 @RTC222:L1d 缓存和存储转发速度很快。存储/加载指令是单微指令。如果其他工作隐藏了 5 或 6 个周期的存储/重新加载延迟,那很好。在某些情况下,使用 movhps [mem], xmm 存储高半部分以供以后重新加载而不是 ALU shuffle 实际上可能很好。

以上是关于将浮点数从高 xmm 四字移动到低 xmm 四字的主要内容,如果未能解决你的问题,请参考以下文章

如何在 xmm 寄存器中旋转压缩四字?

比较 xmm 中的四字

如何将两个打包的 64 位四字加载到 128 位 xmm 寄存器中

如何将一个 XMM 128 位寄存器拆分为两个 64 位整数寄存器?

将单个浮点数移动到 xmm 寄存器

如何将单精度浮点数的 XMM 寄存器转换为整数?