什么是将一个浮点数类型化为int的正确方法,反之亦然?

pla*_*cel 24 c++ strict-aliasing gcc-warning undefined-behavior type-punning

下面的代码通过一些位攻击执行快速反平方根操作.该算法可能是由Silicon Graphics在1990年代早期开发的,它也出现在Quake 3中. 更多信息

但是我从GCC C++编译器收到以下警告:解除引用类型惩罚指针将破坏严格别名规则

我应该使用static_cast,reinterpret_cast还是dynamic_cast在这种情况下使用?

float InverseSquareRoot(float x)
{
    float xhalf = 0.5f*x;
    int32_t i = *(int32_t*)&x;
    i = 0x5f3759df - (i>>1);
    x = *(float*)&i;
    x = x*(1.5f - xhalf*x*x);
    return x;
}
Run Code Online (Sandbox Code Playgroud)

R. *_*des 36

忘记演员.使用memcpy.

float xhalf = 0.5f*x;
uint32_t i;
assert(sizeof(x) == sizeof(i));
std::memcpy(&i, &x, sizeof(i));
i = 0x5f375a86 - (i>>1);
std::memcpy(&x, &i, sizeof(i));
x = x*(1.5f - xhalf*x*x);
return x;
Run Code Online (Sandbox Code Playgroud)

原始代码尝试int32_t通过首先float通过int32_t指针访问对象来初始化,这是规则被破坏的地方.C风格的演员阵容相当于a reinterpret_cast,因此改变它reinterpret_cast不会产生太大的影响.

使用memcpy时的重要区别在于字节是从中复制float到的int32_t,但是float对象永远不会通过int32_t左值访问,因为memcpy它指向void并且其内部是"神奇的"并且不会破坏别名规则.

  • 我担心我的"证明它编译成同一个程序集"上面的评论将被某人解释为表明编写打破别名规则的代码是合理的.意识到这依赖于未定义的行为,就像在任何其他未定义行为的情况下一样,它可能会产生不同的编译器或不同的编译器版本,或周二而不是星期一......等等.*最危险未定义行为的可能结果是在测试期间做您期望的事情.* (11认同)
  • 如果编译器在优化它时做得太糟糕,那就慢一点.[这里证明gcc 4.8将cast和memcpy版本编译成同一个程序集.](http://goo.gl/atXuLe) [和clang的未知版本](http://coliru.stacked-crooked.com/view ?ID = 11c2a965452fccaf7f7f2816953750fb-718da30730c3c592fdabec0dec6148ca). (8认同)
  • @Jan为什么它"明显"慢?我根本没有发现那么明显. (6认同)
  • @plasmacel主要是因为它有效.我不希望合理的编译器在性能上有任何明显的差异.(是的,我相信MSVC会证明我错了......) (3认同)
  • @MikeSeymour,C89说,"一个对象的存储值只能由具有以下类型之一的左值访问:对象的声明类型,对象的声明类型的限定版本,[有符号或无符号变体] ],一种聚合或联合类型,包括其成员[]中的上述类型之一,或6.3表达式中的字符类型.这与C99措辞基本相同,并禁止原始代码. (2认同)
  • @rustyx 从某种意义上说它很神奇,因为它是一个原语,其行为方式与用户实现的方式不同。通过将其视为原始类型,编译器可以使用它来做各种神奇的事情,例如在上面的 Casey 代码中。它并不一定要实现;事实上,在许多编译器中,它是一个编译器内在的;自己尝试实现它很可能最终变成了内在本身,例如 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=56888 (2认同)

Ste*_*non 12

这里有一些很好的答案可以解决打字问题.

我想解决"快速反平方根"部分.不要在现代处理器上使用这个"技巧".每个主流矢量ISA都有一个专用的硬件指令,为您提供快速的反平方根.它们中的每一个都比这个经常复制的小黑客更快更准确.

这些说明都可以通过内在函数获得,因此它们相对容易使用.在SSE中,你想使用rsqrtss(intrinsic :) _mm_rsqrt_ss( ); 在NEON中你想要使用vrsqrte(内在:) vrsqrte_f32( ); 在AltiVec中你想要使用frsqrte.大多数GPU ISA具有类似的指令.可以使用相同的牛顿迭代来细化这些估计,并且NEON甚至具有vrsqrts在单个指令中执行部分细化而无需加载常量的指令.


phu*_*clv 9

如果您可以访问 C++20 或更高版本,那么您可以使用std::bit_cast

float InverseSquareRoot(float x)
{
    float xhalf = 0.5f*x;
    int32_t i = std::bit_cast<int32_t>(x);
    i = 0x5f3759df - (i>>1);
    x = std::bit_cast<float>(i);
    x = x*(1.5f - xhalf*x*x);
    return x;
}
Run Code Online (Sandbox Code Playgroud)

目前,所有主要编译器都std::bit_cast支持。请参阅Godbolt 上的演示

如果您使用的是旧版本的 Clang,您可以尝试__builtin_bit_cast。像这样改变演员阵容

int32_t i = __builtin_bit_cast(std::int32_t, x);
x = __builtin_bit_cast(float, i);
Run Code Online (Sandbox Code Playgroud)

演示


How*_*ant 5

更新资料

由于我从委员会得到的反馈,我不再相信这个答案是正确的。但我想把它留作参考。我有目的地希望委员会能够纠正这个答案(如果它选择这样做的话)。也就是说,没有什么基础的硬件可以使这个答案不正确,这仅仅是委员会的判断是正确的还是错误的。


我添加的答案不是反驳已接受的答案,而是对其进行扩充。我相信公认的答案既正确又有效(我刚刚对此表示赞同)。但是,我想演示另一种同样正确有效的技术:

float InverseSquareRoot(float x)
{
    union
    {
        float as_float;
        int32_t as_int;
    };
    float xhalf = 0.5f*x;
    as_float = x;
    as_int = 0x5f3759df - (as_int>>1);
    as_float = as_float*(1.5f - xhalf*as_float*as_float);
    return as_float;
}
Run Code Online (Sandbox Code Playgroud)

使用在-O3处进行优化的clang ++,我编译了plasmacel的代码,R。Martinho Fernandes代码以及此代码,并逐行比较了组装。这三个都是相同的。这是由于编译器选择像这样进行编译。对于编译器生成不同的,已损坏的代码,同样有效。

  • 据我所知,`memcpy`是在C ++中进行类型修剪的推荐方法。C ++编译器可以维护C99联合保证,但是在C ++中,它仍然是未定义的行为。 (4认同)
  • 对于C ++ 11之前的版本,您可以使用我的上述联合技巧避免出现别名问题:`union A {float x; int32_t y; }; int32_t value = A {3.14f} .y;`(我真的不认为这比没有临时:D的方法更安全)。此“有效”的原因是初始化器是prvalue,因此不受别名规则的限制。但是,这将在C ++ 14中更改,因为初始化器将是xvalue :) (2认同)
  • @ JohannesSchaub-litb我不相信严格的别名规则是C ++中对联合类型进行类型修剪的唯一限制。例如,我相信您的示例与_8.5 [dcl.init] / 16_发生冲突,其中说“正在初始化的对象的初始值为初始化器表达式的(可能是转换的)值”。因为由A {3.14f} .y指定的对象没有任何值。在这种情况下,C ++省略了行为规范,因此行为未定义。 (2认同)