使用(float&)int工作类型punning,(float const&)int转换为(float)int而不是?

tru*_*der 6 c++ assembly sse intrinsics visual-c++

VS2019,发布,x86.

template <int i> float get() const {
    int f = _mm_extract_ps(fmm, i);
    return (float const&)f;
}
Run Code Online (Sandbox Code Playgroud)

使用return (float&)f;编译器时使用

extractps m32, ...
movss xmm0, m32
Run Code Online (Sandbox Code Playgroud)

.正确的结果

使用return (float const&)f;编译器时使用

extractps eax, ...
movd xmm0, eax
Run Code Online (Sandbox Code Playgroud)

.错误的结果

T&和T const&首先是T然后是const的主要思想.Const只是程序员的某种协议.你知道你可以解决它.但汇编代码中没有任何const,但是类型为float IS.我认为对于float和float const而言它必须是汇编中的浮点表示(cpu寄存器).我们可以使用中间int reg32,但最终解释必须是float.

而此时它看起来像回归,因为这之前工作正常.并且在这种情况下使用float也绝对是奇怪的,因为我们不应该考虑浮动const和安全性而是浮动的临时变量并且确实值得怀疑.

微软回答:

嗨Truthfinder,感谢自成一体的复制品.碰巧,这种行为实际上是正确的.正如我的同事@Xiang Fan [MSFT]在内部电子邮件中所述:

由[a c-style cast]执行的转换尝试以下序列:(4.1) - const_cast(7.6.1.11),(4.2) - static_cast(7.6.1.9),(4.3) - static_cast后跟const_cast ,(4.4) - reinterpret_cast(7.6.1.10)或(4.5) - reinterpret_cast后跟const_cast,

如果转换可以用上面列出的多种方式解释,则使用列表中首先出现的解释.

所以在你的情况下,(const float&)被转换为static_cast,其效果是"初始化表达式被隐式转换为类型为"cv1 T1"的prvalue.应用临时实现转换并将引用绑定到结果".

但在另一种情况下,(float&)被转换为reinterpret_cast,因为static_cast无效,这与reinterpret_cast(&operand)相同.

您正在观察的实际"错误"是一个强制转换:"将浮点型值"1.0"转换为等效的int-typed值"1"",而另一个强制转换说"将1.0的位表示形式转换为一个浮点数,然后将这些位解释为int".

出于这个原因,我们建议不要使用c风格的演员表.

谢谢!

MS论坛链接:https://developercommunity.visualstudio.com/content/problem/411552/extract-ps-intrinsics-bug.html

有任何想法吗?

PS我真正想要的是什么:

float val = _mm_extract_ps(xmm, 3);
Run Code Online (Sandbox Code Playgroud)

在手动汇编中我可以写:extractps val, xmm0, 3其中val是float 32内存变量.只有一个!指令.我希望在编译器生成的汇编代码中看到相同的结果.没有洗牌或任何其他过度指示.最不可接受的案例是:extractps reg32, xmm0, 3; mov val, reg32.

关于T&和T const&的观点:对于两种情况,变量的类型必须是相同的.但现在float&将m32解释为float32并将float const&m32解释为int32.

int main() {
    int z = 1;
    float x = (float&)z;
    float y = (float const&)z;
    printf("%f %f %i", x, y, x==y);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

出:0.000000 1.000000 0

那真的好吗?

最好的问候,Truthfinder

Pet*_*des 10

有一个关于C++强制语义的一个有趣的问题(微软已经为你简要回答了这个问题),但它与你的滥用相混淆,_mm_extract_ps导致首先需要一个类型双关语. (并且只显示相同的asm,省略int-> float转换.)如果有人想在另一个答案中扩展标准ese,那就太好了.

TL:DR:改用它:它是零或一个shufps.没有提取物,没有类型的惩罚.

template <int i> float get(__m128 input) {
    __m128 tmp = input;
    if (i)     // constexpr i means this branch is compile-time-only
        tmp = _mm_shuffle_ps(tmp,tmp,i);  // shuffle it to the bottom.
    return _mm_cvtss_f32(tmp);
}
Run Code Online (Sandbox Code Playgroud)

如果你实际上有一个内存目的地用例,那么你应该看一个asm来获取一个float*输出arg的函数,而不是一个需要结果的函数xmm0.(是的,这是extractps指令的用例,但可以说不是_mm_extract_ps内在函数.extractps在优化时使用gcc和clang *out = get<2>(in),尽管MSVC错过了它并且仍然使用shufps + movss.)


您显示的两个asm块都只是在某处复制xmm0的低32位,而没有转换为int.你忽略了重要的不同,只显示了那些只是无用地将float位模式从xmm0 复制出来然后以2种不同方式(寄存器或存储器)复制的部分. movd是未经修改的比特的纯副本,就像movss加载一样.

这是编译器的选择,在你强行使用它之后使用它extractps.通过寄存器返回比存储/重新加载更低的延迟,但更多的ALU uops.

(float const&)类型双关语的尝试确实包括从FP到整数的转换,您没有显示.好像我们需要更多的理由来避免指针/引用转换为类型惩罚,这确实意味着不同的东西:(float const&)f将整数位模式(from _mm_extract_ps)作为aint并将其转换为float.

我把你的代码放在Godbolt编译器浏览器上,看看你遗漏了什么.

float get1_with_extractps_const(__m128 fmm) {
    int f = _mm_extract_ps(fmm, 1);
    return (float const&)f;
}

;; from MSVC -O2 -Gv  (vectorcall passes __m128 in xmm0)
float get1_with_extractps_const(__m128) PROC   ; get1_with_extractps_const, COMDAT
    extractps eax, xmm0, 1   ; copy the bit-pattern to eax

    movd    xmm0, eax      ; these 2 insns are an alternative to pxor xmm0,xmm0 + cvtsi2ss xmm0,eax to avoid false deps and zero the upper elements
    cvtdq2ps xmm0, xmm0    ; packed conversion is 1 uop
    ret     0
Run Code Online (Sandbox Code Playgroud)

GCC以这种方式编译它:

get1_with_extractps_const(float __vector(4)):    # gcc8.2 -O3 -msse4
        extractps       eax, xmm0, 1
        pxor    xmm0, xmm0            ; cvtsi2ss has an output dependency so gcc always does this
        cvtsi2ss        xmm0, eax     ; MSVC's way is probably better for float.
        ret
Run Code Online (Sandbox Code Playgroud)

显然,MSVC确实为类型惩罚定义了指针/引用转换的行为.普通的ISO C++没有(严格别名UB),其他编译器也没有.使用memcpy输入-双关语,或联合(其GNU C和用C MSVC支持++作为扩展名).当然,在这种情况下,对你想要的整数和向后的向量元素进行打字是非常糟糕的.

只有(float &)fgcc警告严格别名违规. 并且GCC/clang同意MSVC,只有这个版本是一个类型双关语,而不是float从隐式转换中实现. C++很奇怪!

float get1_with_extractps_nonconst(__m128 fmm) {
    int f = _mm_extract_ps(fmm, 1);
    return (float &)f;
}

<source>: In function 'float get_with_extractps_nonconst(__m128)':
<source>:21:21: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
     return (float &)f;
                     ^
Run Code Online (Sandbox Code Playgroud)

gcc extractps完全优化了.

# gcc8.2 -O3 -msse4
get1_with_extractps_nonconst(float __vector(4)):
    shufps  xmm0, xmm0, 85    ; 0x55 = broadcast element 1 to all elements
    ret
Run Code Online (Sandbox Code Playgroud)

Clang使用SSE3 movshdup将元素1复制到0.(和元素3到2).但是MSVC没有,这是永远不会使用它的另一个原因:

float get1_with_extractps_nonconst(__m128) PROC
    extractps DWORD PTR f$[rsp], xmm0, 1     ; store
    movss   xmm0, DWORD PTR f$[rsp]          ; reload
    ret     0
Run Code Online (Sandbox Code Playgroud)

不要_mm_extract_ps用于此

你的两个版本都很糟糕,因为这不是什么_mm_extract_psextractps不适合. 英特尔SSE:为什么`_mm_extract_ps`返回`int`而不是`float`?

float寄存器中的A 与向量的低元素相同.高元素不需要归零.如果他们这样做,你会想要使用insertps哪个可以根据立即做xmm,xmm和零元素.

使用_mm_shuffle_ps给你带来想要一个寄存器的低位置的元素,然后一个标量浮动.(你可以告诉C++编译器_mm_cvtss_f32).这应该编译只是shufps xmm0,xmm0,2,没有一个extractps或任何mov.

template <int i> float get() const {
    __m128 tmp = fmm;
    if (i)                               // i=0 means the element is already in place
        tmp = _mm_shuffle_ps(tmp,tmp,i);  // else shuffle it to the bottom.
    return _mm_cvtss_f32(tmp);
}
Run Code Online (Sandbox Code Playgroud)

(我跳过使用,_MM_SHUFFLE(0,0,0,i)因为它等于i.)

如果你fmm在内存中,而不是一个寄存器,那么希望编译器能够优化掉洗牌movss xmm0, [mem].MSVC 19.14确实设法做到这一点,至少对于堆栈案例中的函数arg.我没有测试过其他编译器,但clang应该设法优化掉_mm_shuffle_ps; 通过洗牌非常擅长.

测试用例证明了这有效地编译

例如,具有函数的非类成员版本的测试用例,以及为特定情况内联它的调用者i:

#include <immintrin.h>

template <int i> float get(__m128 input) {
    __m128 tmp = input;
    if (i)                  // i=0 means the element is already in place
        tmp = _mm_shuffle_ps(tmp,tmp,i);  // else shuffle it to the bottom.
    return _mm_cvtss_f32(tmp);
}

// MSVC -Gv (vectorcall) passes arg in xmm0
// With plain dumb x64 fastcall, arg is on the stack, and it *does* just MOVSS load without shuffling
float get2(__m128 in) {
    return get<2>(in);
}
Run Code Online (Sandbox Code Playgroud)

来自Godbolt编译器资源管理器,来自MSVC,clang和gcc的asm输出:

;; MSVC -O2 -Gv
float get<2>(__m128) PROC               ; get<2>, COMDAT
        shufps  xmm0, xmm0, 2
        ret     0
float get<2>(__m128) ENDP               ; get<2>

;; MSVC -O2  (without Gv, so the vector comes from memory)
input$ = 8
float get<2>(__m128) PROC               ; get<2>, COMDAT
        movss   xmm0, DWORD PTR [rcx+8]
        ret     0
float get<2>(__m128) ENDP               ; get<2>
Run Code Online (Sandbox Code Playgroud)
# gcc8.2 -O3 for x86-64 System V (arg in xmm0)
get2(float __vector(4)):
        shufps  xmm0, xmm0, 2   # with -msse4, we get unpckhps
        ret
Run Code Online (Sandbox Code Playgroud)
# clang7.0 -O3 for x86-64 System V (arg in xmm0)
get2(float __vector(4)):
        unpckhpd        xmm0, xmm0      # xmm0 = xmm0[1,1]
        ret
Run Code Online (Sandbox Code Playgroud)

clang的shuffle优化器简化为unpckhpd,在某些旧CPU上更快.不幸的是,它没有注意到它可以使用movhlps xmm0,xmm0,这也很快,1个字节更短.

  • @truthfinder:`extractps`比Skylake上的`shufps`慢; 它花费2 uop,其中一个需要端口5(https://agner.org/optimize/).然后要将值重新放回XMM寄存器,您需要第3个uop.我的版本是1 uop*total*,不涉及`extractps`.你有没有看过[英特尔SSE:为什么\`\ _mm\_extract\_ps \`return \`int \`而不是\`float \`?](// stackoverflow.com/q/5526658)这不是指令你想得到一个矢量元素作为标量浮点数. (3认同)
  • @rustyx:刚试过Godbolt上的源码,是的,一个版本确实包括将位模式作为整数并将其转换为"float".OP令人困惑地离开了这个问题!更新了我的回答. (2认同)
  • @truthfinder:将一个向量元素作为标量提取最多需要1个指令,但是使用`extractps`一定意味着你需要至少2.这与C++引用强制转换的含义完全不同.也许其他人想要引导你完成微软对C++语言规则的解释(在这种情况下会导致非常令人惊讶的行为,但gcc/clang在这里同意MSVC).你好像错过了我的回答:效率. (2认同)

rus*_*tyx 8

我的观点T&T const&:对于两种情况,变量的类型必须是相同的.

正如微软的支持试图解释的那样,这些并不相同.这就是C++的工作原理.

您正在使用C风格的强制转换( ... ),在C++中分解为一系列尝试以降低安全性的顺序使用不同的C++强制转换:

  • (4.1) - a const_cast
  • (4.2) - a static_cast
  • (4.3) - a static_cast后跟aconst_cast
  • (4.4) - a reinterpret_cast
  • (4.5) - a reinterpret_cast后跟aconst_cast

在的情况下(float const&) b(其中,bint):

  • 我们尝试const_cast<float const&>(b);- 没有运气(float对比int)
  • 我们试试static_cast<float const&>(b);- 瞧!(在隐式标准转换为b临时标记之后float- 记住C++允许自己每个表达式隐式执行两个标准和一个用户定义的转换)

在的情况下(float&) b(其中,再次bint):

  • 我们尝试const_cast<float&>(b);- 没有运气
  • 我们尝试static_cast<float&>(b);- 没有运气(在隐式标准转换为b临时之后float,它不会绑定到非const左值引用)
  • 我们尝试const_cast<float&>(static_cast<float&>(b));- 没有运气
  • 我们试试reinterpret_cast<float&>(b);- 瞧!

严格别名规则1,这是一个演示此行为的示例:

#include <iostream>

int main() {
    float a = 1.2345f;
    int b = reinterpret_cast<int&>(a); // this type-pun is built into _mm_extract_ps
    float nc = (float&)b;
    float cc = (float const&)b;
    float rc = reinterpret_cast<float&>(b);
    float sc = static_cast<float const&>(b);
    std::cout << "a=" << a << " b=" << b << std::endl;
    std::cout << "nc=" << nc << " cc=" << cc << std::endl;
    std::cout << "rc=" << rc << " sc=" << sc << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

打印:

a=1.2345 b=1067320345
nc=1.2345 cc=1.06732e+09
rc=1.2345 sc=1.06732e+09
Run Code Online (Sandbox Code Playgroud)

现场演示

这就是为什么你不应该在C++中使用C风格的强制转换.少打字,但很多更头痛.

也不要使用_mm_extract_ps-为什么它返回一个原因int是因为extractps指令拷贝float到通用寄存器-这是不是你想要的,因为要使用一个float必须被复制回一个浮点寄存器.这样做是浪费时间.正如Peter Cordes所解释的那样,使用_mm_cvtss_f32(_mm_shuffle_ps())它来编译一条指令.


1从技术上讲,reinterpret_cast用于规避C++类型系统(又称类型惩罚)是ISO C++中未定义的行为.但是,MSVC将此规则放宽为编译器扩展.因此代码是正确的,只要它是用MSVC或其他可以关闭严格别名规则的地方编译的(例如-fno-strict-aliasing).在没有陷入严格混叠陷阱的情况下,标记双关语的方法是通过memcpy().

  • 如果你关心其他编译器,只需使用`memcpy(&b,&a,sizeof(b))`.现代编译器知道如何完全内联并优化它与演员相同.或者使用工会; MSVC和gcc/clang/icc都支持union type-punning作为C++的扩展,就像在ISO C99中一样.使用`-fno-strict-aliasing'可能会减慢代码的其他部分,这似乎不值得.(在循环中重新加载内容和/或停止自动向量化,除非你使用`float*__ restrict foo`来保证没有别名.) (3认同)