128 位值 - 从 XMM 寄存器到通用

128-bit values - From XMM registers to General Purpose

我有几个关于将 XMM 值移动到通用寄存器的问题。 SO上查到的所有问题都集中在相反的方向,即将gp寄存器中的值传输到XMM。

  1. 如何将 XMM 寄存器值(128 位)移动到两个 64 位通用寄存器?

    movq RAX XMM1 ; 0th bit to 63th bit
    mov? RCX XMM1 ; 64th bit to 127th bit
    
  2. 同样,如何将 XMM 寄存器值(128 位)移动到四个 32 位通用寄存器?

    movd EAX XMM1 ; 0th bit to 31th bit
    mov? ECX XMM1 ; 32th bit to 63th bit
    
    mov? EDX XMM1 ; 64th bit to 95th bit
    mov? ESI XMM1 ; 96th bit to 127 bit
    

您不能将 XMM 寄存器的高位直接移动到通用寄存器中。
您必须遵循一个两步过程,这可能涉及也可能不涉及内存往返或寄存器销毁。

在寄存器中 (SSE2)

movq rax,xmm0       ;lower 64 bits
movhlps xmm0,xmm0   ;move high 64 bits to low 64 bits.
movq rbx,xmm0       ;high 64 bits.

punpckhqdq xmm0,xmm0 is the SSE2 integer equivalent of movhlps xmm0,xmm0。如果 xmm0 最后由整数指令而非 FP 写入,某些 CPU 可能会避免一两个周期的旁路延迟。

通过内存 (SSE2)

movdqu [mem],xmm0
mov rax,[mem]
mov rbx,[mem+8]

慢,但不破坏xmm寄存器(SSE4.1)

mov rax,xmm0
pextrq rbx,xmm0,1        ;3 cycle latency on Ryzen! (and 2 uops)

混合策略是可能的,例如存储到内存,movd/q e/rax,xmm0 所以它很快就准备好了,然后重新加载更高的元素。 (不过,存储转发延迟并不比 ALU 差多少。)这为不同的后端执行单元提供了微指令的平衡。 Store/reload 当你想要很多小元素时特别好。 (mov / movzx 加载到 32 位寄存器的成本很低,并且具有 2/时钟吞吐量。)


对于32位,代码类似:

在寄存器中

movd eax,xmm0
psrldq xmm0,xmm0,4    ;shift 4 bytes to the right
movd ebx,xmm0
psrldq xmm0,xmm0,4    ; pshufd could copy-and-shuffle the original reg
movd ecx,xmm0         ; not destroying the XMM and maybe creating some ILP
psrlq xmm0,xmm0,4
movd edx,xmm0

通过记忆

movdqu [mem],xmm0
mov eax,[mem]
mov ebx,[mem+4]
mov ecx,[mem+8]
mov edx,[mem+12]

不破坏 xmm 寄存器 (SSE4.1)(像 psrldq / pshufd 版本一样慢)

movd eax,xmm0
pextrd ebx,xmm0,1        ;3 cycle latency on Skylake!
pextrd ecx,xmm0,2        ;also 2 uops: like a shuffle(port5) + movd(port0)
pextrd edx,xmm0,3       

64 位移位变体可以在 2 个周期内 运行。 pextrq 版本最少需要 4 个。对于 32 位,数字分别为 4 和 10。

在 Intel SnB 系列(包括 Skylake)上,shuffle+movqmovd 具有与 pextrq/d 相同的性能。它解码为一个 shuffle uop 和一个 movd uop,所以这并不奇怪。

在 AMD Ryzen 上,pextrq 的延迟显然比 shuffle + movq 低 1 个周期。根据Agner Fog's tablespextrd/q 是3c 延迟,movd/q 也是如此。这是一个巧妙的技巧(如果它是准确的),因为 pextrd/q 确实解码为 2 微指令(相对于 movq 的 1)。

由于 shuffle 具有非零延迟,shuffle+movq 在 Ryzen 上总是比 pextrq 严格差(除了可能的前端解码/uop-cache 影响)。

用于提取所有元素的纯 ALU 策略的主要缺点是吞吐量:它需要大量的 ALU 微指令,并且大多数 CPU 只有一个执行单元/端口可以将数据从 XMM 移动到整数。 Store/reload 第一个元素的延迟更高,但吞吐量更高(因为现代 CPU 每个周期可以执行 2 次加载)。如果周围的代码受到 ALU 吞吐量的瓶颈,store/reload 策略可能会很好。也许用 movdmovq 做低元素,这样乱序执行就可以开始使用它,而其余的矢量数据正在通过存储转发。


将 32 位元素提取到整数寄存器的另一个值得考虑的选项(除了 Johan 提到的)是使用整数移位进行一些“洗牌”:

mov  rax,xmm0
# use eax now, before destroying it
shr  rax,32    

pextrq rcx,xmm0,1
# use ecx now, before destroying it
shr  rcx, 32

shr 可以在 Intel Haswell/Skylake 的 p0 或 p6 上 运行。 p6 没有向量 ALU,所以如果你想要低延迟但又想对向量 ALU 施加低压力,这个序列非常好。


或者如果你想把它们留在身边:

movq  rax,xmm0
rorx  rbx, rax, 32    # BMI2
# shld rbx, rax, 32  # alternative that has a false dep on rbx
# eax=xmm0[0], ebx=xmm0[1]

pextrq  rdx,xmm0,1
mov     ecx, edx     # the "normal" way, if you don't want rorx or shld
shr     rdx, 32
# ecx=xmm0[2], edx=xmm0[3]

以下处理 get 和 set 并且似乎有效(我认为这是 AT&T 语法):

#include <iostream>

int main() {
    uint64_t lo1(111111111111L);
    uint64_t hi1(222222222222L);
    uint64_t lo2, hi2;

    asm volatile (
            "movq       %3,     %%xmm0      ; " // set high 64 bits
            "pslldq     ,     %%xmm0      ; " // shift left 64 bits
            "movsd      %2,     %%xmm0      ; " // set low 64 bits
                                                // operate on 128 bit register
            "movq       %%xmm0, %0          ; " // get low 64 bits
            "movhlps    %%xmm0, %%xmm0      ; " // move high to low
            "movq       %%xmm0, %1          ; " // get high 64 bits
            : "=x"(lo2), "=x"(hi2)
            : "x"(lo1), "x"(hi1)
            : "%xmm0"
    );

    std::cout << "lo1: [" << lo1 << "]" << std::endl;
    std::cout << "hi1: [" << hi1 << "]" << std::endl;
    std::cout << "lo2: [" << lo2 << "]" << std::endl;
    std::cout << "hi2: [" << hi2 << "]" << std::endl;

    return 0;
}