【问题标题】:128-bit values - From XMM registers to General Purpose128 位值 - 从 XMM 寄存器到通用
【发布时间】:2017-10-16 13:06:57
【问题描述】:

我有几个关于将 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
    

【问题讨论】:

    标签: assembly x86 sse


    【解决方案1】:

    您不能将 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,xmm0movhlps xmm0,xmm0 的 SSE2 整数等价物。如果 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 差多少。)这为您提供了不同后端执行单元的微指令平衡。当您需要大量小元素时,存储/重新加载特别好。 (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。

    【讨论】:

    • 嗨,约翰,感谢您的回复。为了完整起见,您能否在答案中也包含 32 位版本。
    • FWIW,对于 SSE4,您还可以使用 pextrq,为 64 位情况提供两条指令解决方案(类似地,使用 pextrd 为 32 位情况提供 4 指令解决方案)。跨度>
    • pextrq 的好处是它不会破坏寄存器。但它有点慢。
    • pextrq 与 Ryzen 上的 movq 延迟相同(均为 3c),因此 shuffle+movq 严格来说比 pextrq 差! Shuffle+movq 与 Intel SnB 系列上的pextrq 的延迟相同(包括 Skylake,movq 是 2c 延迟)。我几乎不会称之为“慢”。它仍然比内存往返延迟更低,特别是对于可以在准备好后立即开始使用eax 的代码,这要归功于乱序执行。不过,存储/重新加载可以提高吞吐量,尤其是对于 32 位或更小的元素,因为大量的提取 / movq 很容易成为特定 ALU 端口的瓶颈。
    【解决方案2】:

    在 Intel SnB 系列(包括 Skylake)上,shuffle+movqmovdpextrq/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 具有非零延迟,因此在 Ryzen 上,shuffle+movq 总是比pextrq 差(除了可能的前端解码/uop-cache 影响)。

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


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

    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 的压力也较低,那么这个序列非常好。


    或者,如果您想保留它们:

    mov  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]
    

    【讨论】:

      【解决方案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     $8,     %%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;
      }
      

      【讨论】:

      • 如果你的 asm 约束是正确的,你就不需要volatile。但更重要的是,您绝对不需要内联 asm,也不应该使用它 (gcc.gnu.org/wiki/DontUseInlineAsm)。尤其是这种优化不佳的代码,它需要编译器为您将整数转换为 xmm regs,而不是自己使用 movq integer -> xmm。提示,punpcklqdq 将 2 个寄存器的低半部分合二为一。但即使你完美地优化了 asm,它仍然会破坏持续传播。
      • 在 C 中,最佳方式是_mm_set1_epi64x(a, b)。理论上,编译器会为目标机器选择最优序列,这取决于 ab 中的一个或两者在此内联时是编译时常量,SSE4.1 是否可用(对于 pinsrq)和调整选项。在实践中,gcc 经常做出糟糕的选择,例如即使使用 -march=haswell 也可以存储/重新加载(我报告了 gcc bug 80820 关于此问题)。
      • 您似乎对“内在”是什么感到困惑。它们是通常可以编译为单个指令的函数,在输入为常量时更好地优化为无。 movq %r64, %xmm 的内在函数是 __m128i _mm_cvtsi64x_si128(__int64),如果向下滚动到 Intel's insn manual entry for movd/movq 的底部,您可以看到。
      • 如果你使用内在函数而不是内联 asm,所有向量的东西都会被优化掉,因为你的输入是编译时常量。经常发生内联(尤其是链接时优化)使某些函数 args 成为常量的情况,因此使用内联 asm 会使您的代码变得更糟。内联汇编的短 sn-ps 仍然依赖于优化器来获得良好的周边代码,因此像这样拼凑内联汇编的短块是一个糟糕的主意。要么在 asm 中编写整个函数(或至少在主循环中),要么使用编译器理解并可以优化的内在函数。
      • 如果你想为特定循环手动调整一些代码,那么当然,用 asm.xml 编写它。 (或者如果你只关心用一个特定版本的 gcc 让它生成你想要的 asm,则可以混合使用 C 和 inline-asm,但不保证它将来会如何优化。)纯手写 asm 完全有意义当一个函数完全控制了整个任务的运行时,并且您不必担心灵活地针对不同的调用者/周围代码进行优化。不过,IDK 与您的答案中的任何内容有什么关系。 inline-asm 还是不好用,asm 效率低。
      猜你喜欢
      • 2011-01-14
      • 2012-01-30
      • 1970-01-01
      • 2011-10-03
      • 2023-03-27
      • 1970-01-01
      • 1970-01-01
      • 2019-11-16
      • 2014-04-21
      相关资源
      最近更新 更多