英特尔SSE:为什么`_mm_extract_ps`返回`int`而不是`float`?

时间:2011-04-02 23:52:14

标签: c sse simd

为什么_mm_extract_ps会返回int而不是float

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

或者更确切地说,问一个不同的方式是: _mm_set_ps指令的反义词是什么?

4 个答案:

答案 0 :(得分:17)

这些答案似乎都没有真正回答问题,为什么会返回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代码),这可能会导致生成低效的代码 - 编译器可能会存储该代码的组件然后使用fld将SSE向量读回x87寄存器堆栈。通常,64位平台不使用x87(它们对所有浮点使用SSE,主要是标量指令,除非编译器正在向量化)。

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

答案 1 :(得分:6)

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

gcc和clang知道asm指令是如何工作的,并且在编译其他shuffle时会以这种方式使用它;将_mm_extract_ps结果标记为float通常会导致gcc(extractps eax, xmm0, 2 / mov [mem], eax)出现可怕的asm。

如果您认为_mm_extract_ps将CPU的FP域中的IEEE 754 binary32 float bit pattern提取到整数域(作为C标量int),而不是用整数向量ops操纵FP位模式。 根据我对gcc,clang和icc(见下文)的测试,这是唯一的"便携式"用例_mm_extract_ps在所有编译器中编译好的asm 。其他任何东西只是一个特定于编译器的黑客来获取你想要的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-vector指令有一个XMM源和一个memory-or-xmm目的地,比如MOVSS xmm2/m32, xmm,所以这种指令不是新的。有趣的事实:PEXTRD和EXTRACTPS的操作码只在最后一位有所不同。

在汇编中,标量float只是XMM寄存器的低位元素(或内存中的4个字节)。对于像ADDSS这样的指令,XMM的上层元素甚至不必归零,而不会产生任何额外的FP异常。在调用XMM寄存器中传递/返回FP args的约定(例如所有通常的x86-64 ABI)时,float foo(float a)必须假设XMM0的上层元素在入口处保留垃圾,但是可以在高级元素中保留垃圾。返回XMM0。 (More info)。

As @doug points out,其他shuffle指令可用于将向量的float元素放入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 uop,并且在SSE4.1 CPU上具有比MOVAPS + SHUFPS更低的延迟,而没有用于零延迟MOVAPS的移动消除(仅Penryn,Nehalem,Sandybridge。还有Silvermont如果你包括低功耗CPU)。但是,代码大小稍微差一点。

使用_mm_extract_ps然后将结果返回到浮动状态(如建议的in the currently-accepted answer及其注释)是一个坏主意。在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也做得不好,所以它不喜欢基于联合的类型惩罚。

这里的明显赢家正在进行洗牌&#34;手动&#34;使用_mm_shuffle_ps(v,v, 2)并使用_mm_cvtss_f32我们从寄存器和内存目标的每个编译器获得最佳代码,但ICC未能将exTRACTPS用于memory-dest情况。使用AVX,SHUFPS +独立存储在Intel CPU上仍然只有2 uop,只是更大的代码大小,需要一个tmp寄存器。但是,如果没有AVX,则需要MOVAPS才能销毁原始矢量:/

根据Agner Fog's instruction tables,除了Nehalem之外的所有Intel CPU都实现了具有多个uop的PEXTRD和EXTRACTPS的寄存器目的地版本:通常只是一个shuffle uop +一个MOVD uop,用于将数据从向量域移动到gp-整数。 Nehalem寄存器目的地EXTRACTPS端口5为1 uop,1 + 2周期延迟(1 +旁路延迟)。

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

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

在Nehalem上,PEXTRD和EXTRACTPS的memory-dest版本都是2 uops。

在Broadwell以及之后,内存目的地EXTRACTPS和PEXTRD是2 uop,但是在Sandybridge上通过Haswell,内存目标EXTRACTPS是3 uops。内存目的地PEXTRD在除Sandybridge之外的所有东西上都是2 uops,它是3。这看起来很奇怪,而且Agner Fog的表有时会出错,但这是可能的。微融合并不适用于某些微体系结构的一些指令。

如果任何一条指令对于任何重要的事情(例如内部循环内部)都非常有用,那么CPU设计人员就会构建执行单元,可以将整个事务作为一个uop(或者对于memory-dest来说可能为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 uops,并且以2 + 1周期延迟运行。

答案 2 :(得分:1)

尝试_mm_storeu_psSSE store operations的任何变体。

答案 3 :(得分:1)

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

从他们的例子中可以看出,0xc0a40000的值相当于-5.125(a.m128_f32 [1])。

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