【问题标题】:Intel SSE: Why does `_mm_extract_ps` return `int` instead of `float`?英特尔 SSE:为什么 `_mm_extract_ps` 返回 `int` 而不是 `float`?
【发布时间】:2011-07-28 10:52:44
【问题描述】:

为什么_mm_extract_ps 返回int 而不是float

从 C 中的 XMM 寄存器读取单个 float 的正确方法是什么?

或者更确切地说,问它的另一种方式是:_mm_set_ps 指令的反面是什么?

【问题讨论】:

标签: c sse simd


【解决方案1】:

尝试_mm_storeu_ps,或SSE store operations 的任何变体。

【讨论】:

    【解决方案2】:

    MSDN docs,我相信您可以将结果转换为浮点数。

    请注意,从他们的示例中,0xc0a40000 值相当于 -5.125 (a.m128_f32[1])。

    更新:我强烈推荐 @doug65536 和 @PeterCordes(下)的答案来代替我的答案,这显然会在许多编译器上生成性能不佳的代码。

    【讨论】:

    • 那会是怎样的演员阵容呢? C 风格的演员表行不通,不是吗?
    • 当然 - 只要存储大小匹配。顺便说一句 - 看起来有一个宏可能会有所帮助:_MM_EXTRACT_FLOAT(请参阅stackoverflow.com/questions/3130169/…
    • Ahhhhhhh 这正是我面临的问题的解决方案!很难搜索;谢谢您的帮助! :)
    • 不要将_mm_extract_ps 的结果键入回float。这不是内在函数的用途,当你这样做时,ICC 或 GCC 最终会生成从糟糕到可怕的代码。有关 Godbolt 上一些简单测试用例的 asm 输出,请参阅 my answer to this question。但是,通过@Apriori 建议的正确且安全的基于联合的类型双关语,gcc 在这两种情况下都可以生成良好的代码。只有 ICC 有问题。
    【解决方案3】:

    似乎没有一个答案能真正回答这个问题,为什么它会返回int

    原因是,extractps 指令实际上将向量的一个分量复制到一个通用寄存器。返回一个 int 看起来确实很愚蠢,但这就是实际发生的事情 - 原始浮点值最终在一个通用寄存器中(它保存整数)。

    如果您的编译器配置为为所有浮点运算生成 SSE,那么最接近将值“提取”到寄存器的方法是将值混洗到向量的低分量中,然后将其转换为标量漂浮。这应该会导致向量的该分量保留在 SSE 寄存器中:

    /* returns the second component of the vector */
    float foo(__m128 b)
    {
        return _mm_cvtss_f32(_mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 2)));
    }
    

    _mm_cvtss_f32 内在函数是免费的,它不生成指令,它只会让编译器将 xmm 寄存器重新解释为 float,因此可以这样返回。

    _mm_shuffle_ps 将所需值放入最低组件中。 _MM_SHUFFLE 宏为生成的 shufps 指令生成立即操作数。

    示例中的2 从 127:0 寄存器的第 95:64 位(按内存顺序从头开始的第 3 个 32 位分量)获取浮点数并将其放入寄存器的 31:0 分量(开头,按记忆顺序)。

    生成的代码很可能会自然地在寄存器中返回值,就像任何其他浮点值返回一样,不会低效地写入内存并将其读回。

    如果您生成的代码使用 x87 FPU 进行浮点运算(对于未经 SSE 优化的普通 C 代码),这可能会导致生成的代码效率低下 - 编译器可能会存储SSE 向量然后使用fld 将其读回 x87 寄存器堆栈。通常 64 位平台不使用 x87(它们对所有浮点使用 SSE,主要是标量指令,除非编译器正在矢量化)。

    我应该补充一点,我总是使用 C++,所以我不确定在 C 中通过值还是通过指针传递 __m128 是否更有效。在 C++ 中,我将使用 const __m128 &,这种代码将是在头文件中,因此编译器可以内联。

    【讨论】:

    • movss xmm, xmm 不会将上部组件归零。它使它们保持不变。也许你会想到整数movd xmm, r32movd xmm, m32。此外,_MM_SHUFFLE 将{ d c b a } 的向量转换为{ a a a c }。它将第 3 个元素放入 low32,而不是第 2 个,除非您从高端数...
    • @PeterCordes 修正并澄清了洗牌,谢谢。
    • _mm_cvtss_f32“是免费的(没有asm指令)”可能是一种更清晰的方式来表达它的效果就像是XMM寄存器的reinterpret_cast。
    • @PeterCordes 好主意。谢谢。
    • 我添加了我自己的答案以及关于英特尔设计选择的更多想法(并警告不要将_mm_extract_ps 打回浮动,因为这不是它的用途)。您的 shuffle 是表达这一点的正确方式,编译器(ICC 除外)会在适当时将其优化为 extractps [mem], xmm0, 2
    【解决方案4】:

    令人困惑的是,int _mm_extract_ps() 不是用于从向量中获取标量 float 元素。 内在函数不会公开指令的内存目标形式(这对此很有用目的)。这不是内在函数无法直接表达指令有用的所有内容的唯一情况。 :(

    gcc 和 clang 知道 asm 指令是如何工作的,并且会在编译其他 shuffle 时为您使用它;将_mm_extract_ps 结果键入到float 通常会导致来自 gcc (extractps eax, xmm0, 2 / mov [mem], eax) 的可怕 asm。

    如果您认为_mm_extract_psIEEE 754 binary32 float bit pattern 从 CPU 的 FP 域提取到整数域(作为 C 标量 int),而不是使用整数操作 FP 位模式,那么这个名称是有意义的矢量操作。 根据我对 gcc、clang 和 icc(见下文)的测试,这是 _mm_extract_ps 在所有编译器中编译成良好 asm 的唯一“可移植”用例。其他任何东西都只是一个特定于编译器的 hack 来获得你想要的 asm。


    对应的asm指令为EXTRACTPS r/m32, xmm, imm8。请注意,目标可以是内存或 整数 寄存器,但不能是另一个 XMM 寄存器。它是PEXTRD r/m32, xmm, imm8 的FP 等价物(也在SSE4.1 中),其中整数寄存器目标形式更明显有用。 EXTRACTPS 不是INSERTPS xmm1, xmm2/m32, imm8 的反面。

    也许与 PEXTRD 的这种相似性使内部实现更简单,而不会损害提取到内存的用例(对于 asm,而不是内在函数),或者英特尔的 SSE4.1 设计人员认为它实际上比这种方式更有用作为非破坏性的 FP 域复制和洗牌(没有 AVX,x86 严重缺乏)。有些 FP 向量指令具有 XMM 源和内存或 xmm 目标,例如 MOVSS xmm2/m32, xmm,因此这种指令不会是新的。有趣的事实:PEXTRD 和 EXTRACTPS 的操作码仅在最后一位不同。


    在汇编中,标量 float 只是 XMM 寄存器的低位元素(或内存中的 4 个字节)。 XMM 的上层元素甚至不必为 ADDSS 之类的指令归零即可工作,而不会引发任何额外的 FP 异常。在 XMM 寄存器中传递/返回 FP 参数的调用约定(例如,所有常见的 x86-64 ABI)中,float foo(float a) 必须假定 XMM0 的高位元素在入口处保存垃圾,但可以在 XMM0 的高位元素中留下垃圾返回。 (More info)。

    As @doug points out,其他 shuffle 指令可用于将向量的浮点元素放入 xmm 寄存器的底部。 这已经是 SSE1/SSE2 中大部分已解决的问题,似乎 EXTRACTPS 和 INSERTPS 并没有尝试解决寄存器操作数的问题。


    SSE4.1 INSERTPS xmm1, xmm2/m32, imm8 是编译器实现 _mm_set_ss(function_arg) 的最佳方法之一,当标量浮点数已经在寄存器中并且它们不能/不优化将上部元素归零时。 (Which is most of the time for compilers other than clang)。该链接问题还进一步讨论了内在函数未能公开加载或存储指令的版本,例如 EXTRACTPS、INSERTPS 和 PMOVZX,其内存操作数小于 128b(因此即使没有 AVX 也不需要对齐)。编写安全的代码是不可能像在 asm 中那样高效地编译的。

    没有 AVX 3 操作数 SHUFPS,x86 无法像整数 PSHUFD 那样提供一种完全有效且通用的方法来复制和混洗 FP 向量。 SHUFPS 是一个不同的野兽,除非在 src=dst 就地使用。保留原始文件需要 MOVAPS,这会在 IvyBridge 之前在 CPU 上花费 uop 和延迟,并且总是会花费代码大小。在 FP 指令之间使用 PSHUFD 会产生延迟(旁路延迟)。 (请参阅 this horizontal-sum answer 了解一些技巧,例如使用 SSE3 MOVSHDUP)。

    SSE4.1 INSERTPS 可以将一个元素提取到单独的寄存器中,但 AFAIK 即使替换了所有原始值,它仍然依赖于目标的先前值。像这样的错误依赖不利于乱序执行。 xor-zeroing 一个寄存器作为 INSERTPS 的目标仍然是 2 微秒,并且在 SSE4.1 CPU 上具有比 MOVAPS+SHUFPS 更低的延迟,而没有针对零延迟 MOVAPS 的 mov 消除(仅限 Penryn、Nehalem、Sandybridge。如果你包括低功耗 CPU)。不过,代码大小稍差。


    使用_mm_extract_ps 然后将结果键入回浮动(如建议的in the currently-accepted answer 及其cmets)是一个坏主意。在 gcc 或 icc 上,您的代码很容易编译为可怕的东西(例如 EXTRACTPS 到内存,然后加载回 XMM 寄存器)。 Clang 似乎对脑死亡行为免疫,并使用自己选择的随机指令(包括适当使用 EXTRACTPS)进行通常的随机编译。

    我使用 gcc5.4 -O3 -msse4.1 -mtune=haswell、clang3.8.1 和 icc17、on the Godbolt compiler explorer 尝试了这些示例。我使用的是 C 模式,而不是 C++,但在 GNU C++ 中允许基于联合的类型双关语作为 ISO C++ 的扩展。类型双关的指针转换违反了 C99 和 C++ 中的严格别名,即使使用 GNU 扩展也是如此。

    #include <immintrin.h>
    
    // gcc:bad  clang:good  icc:good
    void extr_unsafe_ptrcast(__m128 v, float *p) {
      // violates strict aliasing
      *(int*)p = _mm_extract_ps(v, 2);
    }
    
      gcc:   # others extractps with a memory dest
        extractps       eax, xmm0, 2
        mov     DWORD PTR [rdi], eax
        ret
    
    
    // gcc:good  clang:good  icc:bad
    void extr_pun(__m128 v, float *p) {
      // union type punning is safe in C99 (and GNU C and GNU C++)
      union floatpun { int i; float f; } fp;
      fp.i = _mm_extract_ps(v, 2);
      *p = fp.f;     // compiles to an extractps straight to memory
    }
    
       icc:
        vextractps eax, xmm0, 2
        mov       DWORD PTR [rdi], eax
        ret       
    
    
    // gcc:good  clang:good  icc:horrible
    void extr_gnu(__m128 v, float *p) {
      // gcc uses extractps with a memory dest, icc does extr_store
      *p = v[2];
    }
    
     gcc/clang:
        extractps       DWORD PTR [rdi], xmm0, 2
     icc:
        vmovups   XMMWORD PTR [-24+rsp], xmm0
        mov       eax, DWORD PTR [-16+rsp]      # reload from red-zone tmp buffer
        mov       DWORD PTR [rdi], eax
    
    // gcc:good  clang:good  icc:poor
    void extr_shuf(__m128 v, float *p) {
      __m128 e2 = _mm_shuffle_ps(v,v, 2);
      *p = _mm_cvtss_f32(e2);  // gcc uses extractps
    }
    
     icc:   (others: extractps right to memory)
        vshufps   xmm1, xmm0, xmm0, 2
        vmovss    DWORD PTR [rdi], xmm1
    

    当您希望在 xmm 寄存器中获得最终结果时,由编译器来优化您的提取并做一些完全不同的事情。 Gcc 和 clang 都成功了,但 ICC 没有。

    // gcc:good  clang:good  icc:bad
    float ret_pun(__m128 v) {
      union floatpun { int i; float f; } fp;
      fp.i = _mm_extract_ps(v, 2);
      return fp.f;
    }
    
      gcc:
        unpckhps        xmm0, xmm0
      clang:
        shufpd  xmm0, xmm0, 1
      icc17:
        vextractps DWORD PTR [-8+rsp], xmm0, 2
        vmovss    xmm0, DWORD PTR [-8+rsp]
    

    请注意,icc 对 extr_pun 的表现也很差,因此它不喜欢基于联合的类型双关语。

    这里明显的赢家是使用_mm_shuffle_ps(v,v, 2)_mm_cvtss_f32“手动”进行随机播放。我们从每个编译器中获得了针对寄存器和内存目标的最佳代码,除了 ICC,它未能将 EXTRACTPS 用于 memory-dest 情况。使用 AVX,SHUFPS + 独立存储在 Intel CPU 上仍然只有 2 微秒,只是更大的代码大小并且需要一个 tmp 寄存器。但是,如果没有 AVX,不破坏原始向量会花费 MOVAPS:/


    根据Agner Fog's instruction tables,除 Nehalem 之外的所有英特尔 CPU 都使用多个微指令实现 PEXTRD 和 EXTRACTPS 的寄存器目标版本:通常只需一个 shuffle uop + 一个 MOVD uop 即可将数据从向量域移动到 gp 整数。 Nehalem register-destination EXTRACTPS 对于端口 5 为 1 uop,具有 1+2 周期延迟(1 + 旁路延迟)。

    我不知道他们为什么设法将 EXTRACTPS 实现为单个 uop 而不是 PEXTRD(这是 2 uop,并以 2+1 周期延迟运行)。 Nehalem MOVD 为 1 uop(并在任何 ALU 端口上运行),具有 1+1 周期延迟。 (我认为 +1 是用于 vec-int 和通用整数 regs 之间的绕过延迟)。

    Nehalem 非常关心向量 FP 与整数域; SnB 系列 CPU 在域之间的旁路延迟延迟更小(有时为零)。

    PEXTRD 和 EXTRACTPS 的内存目标版本都是 Nehalem 上的 2 微指令。

    在 Broadwell 及更高版本上,内存目标 EXTRACTPS 和 PEXTRD 为 2 微指令,但在 Sandybridge 通过 Haswell 上,内存目标 EXTRACTPS 为 3 微指令。 Memory-destination PEXTRD 在除了 Sandybridge 之外的所有东西上都是 2 微指令,它是 3。这看起来很奇怪,而且 Agner Fog 的表有时确实有错误,但这是可能的。微融合不适用于某些微架构上的某些指令。

    如果任何一条指令被证明对任何重要的事情都非常有用(例如,在内部循环中),CPU 设计人员将构建可以将整个事情作为一个 uop 来完成的执行单元(或者对于内存目标来说可能是 2 个)。但这可能需要更多内部 uop 格式的位(Sandybridge 对此进行了简化)。

    有趣的事实:_mm_extract_epi32(vec, 0) 编译(在大多数编译器上)为movd eax, xmm0,它比pextrd eax, xmm0, 0 更短、更快。

    有趣的是,they perform differently on Nehalem(它非常关心向量 FP 与整数域,并且在 Penryn(45nm Core2)中引入 SSE4.1 后不久就出来了)。具有寄存器目标的 EXTRACTPS 为 1 uop,具有 1+2 周期延迟(来自 FP 和整数域之间的旁路延迟的 +2)。 PEXTRD 是 2 uop,以 2+1 周期延迟运行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-07-19
      • 1970-01-01
      • 2023-01-16
      • 1970-01-01
      • 2011-07-01
      • 1970-01-01
      • 2020-04-26
      • 1970-01-01
      相关资源
      最近更新 更多