如何正确地将浮点数和整数进行类型转换?

31
下面的代码通过一些比特操作执行快速反平方根运算。该算法可能是由Silicon Graphics在1990年代初开发的,它也出现在Quake 3中。更多信息 然而,我从GCC C++编译器得到以下警告:解引用类型转换后的指针将会违反严格别名规则。
在这种情况下,我应该使用static_castreinterpret_castdynamic_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;
}

该论文在结论中给出了0x5fe6ec85e7de30da作为双精度浮点数的最优常数。 - R. Martinho Fernandes
Lomont指出,64位IEEE754大小类型double的“幻数”为0x5fe6ec85e7de30da,但实际上被证明确切为0x5fe6eb50c7b537a9 (取自维基百科)。也许我会进行一些测试。 - plasmacel
哦,有趣。我不知道McEniry的论文。谢谢 :) - R. Martinho Fernandes
1
假设浮点数和整数位相互兼容,则此技巧完全不会发生未定义行为:union A { float x; int32_t y; }; int32_t value = A{3.14f}.y; 原因是这里从未应用别名规则。然而,此技巧仅适用于C++11及以下版本(我认为C++14将更改规则,以便这变成UB)。 - Johannes Schaub - litb
1
你的C++中的pseudo_cast也应该使用static_assert来检查两种类型是否都是可平凡复制的(参见std::is_trivially_copyable)。 - Ruslan
与https://stackoverflow.com/q/67636231/5740428几乎相同的副本。 - undefined
8个回答

40

忘记使用类型转换。使用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;

原始代码试图通过首先通过int32_t指针访问float对象来初始化int32_t,这就是违反规则的地方。C样式转换等同于reinterpret_cast,因此将其更改为reinterpret_cast不会有太大的区别。

使用memcpy时的重要区别在于,字节从float复制到int32_t,但float对象永远不会通过int32_t lvalue访问,因为memcpy获取void指针并且其内部是“神奇”的,不会违反别名规则。


6
为什么它“显然”更慢?我并不觉得那很明显。 - Konrad Rudolph
3
大多数情况下选择它是因为它有效。我并不认为一个合理的编译器会有明显的性能差异。(是的,我确定 MSVC 会证明我错...) - R. Martinho Fernandes
12
我担心我的上述“编译为同一汇编代码的证明”评论会被某些人误解为编写违反别名规则的代码是合理的。请注意,这依赖于未定义行为,就像在任何其他未定义行为情况下一样,它可能会在不同的编译器或版本或在星期二而不是星期一等情况下产生不同的结果......等等。未定义行为最危险的可能结果是在测试期间达到你所期望的结果。 - Casey
2
@MikeSeymour,C89说:“一个对象的存储值只能被具有以下类型之一的lvalue访问:对象的声明类型,对象的声明类型的限定版本[有符号或无符号变体],包含上述类型之一的聚合或联合类型[],或字符类型”,在6.3表达式中。这基本上与C99措辞相同,并禁止原始代码。 - Jeffrey Yasskin
显示剩余12条评论

14

这里有一些很好的答案,涉及类型转换问题。

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

这些指令都可以通过内联汇编调用,所以使用起来相对容易。在SSE中,您需要使用 rsqrtss (内联函数: _mm_rsqrt_ss( ));在NEON中,您需要使用 vrsqrte (内联函数: vrsqrte_f32( ));而在AltiVec中,您需要使用frsqrte。大多数GPU指令集也有类似的指令。这些估计值可以使用相同的牛顿迭代法进行精细化,并且NEON甚至有vrsqrts指令,可以在不需要加载常量的情况下完成部分精细化。


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;
}

目前只有 MSVC 支持 std::bit_cast。请参见在 Godbolt 上的演示demo on Godbolt

如果您正在等待实现,如果您使用的是Clang,则可以尝试__builtin_bit_cast。只需像这样更改转换即可。

int32_t i = __builtin_bit_cast(std::int32_t, x);
x = __builtin_bit_cast(float, i);

演示


7

更新

由于委员会的反馈,我不再认为这个答案是正确的。但是出于信息目的,我想保留它。并且我有意希望委员会可以使这个答案变得正确(如果它选择这样做)。也就是说,没有任何关于底层硬件的东西使这个答案不正确,只是委员会的判断使其如此或不如此。


我添加一个答案不是为了驳斥已接受的答案,而是为了补充它。我认为已接受的答案既正确又有效率(我刚刚点赞了它)。然而,我想演示另一种同样正确和有效的技术:

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;
}

使用带有-O3优化的clang++编译器,我编译了plasmacel的代码、R. Martinho Fernandes的代码和这段代码,并逐行比较了汇编代码。三者完全相同。这是由于编译器选择以这种方式进行编译。编译器产生不同且错误的代码同样有效。


1
@jogojapan:好评论。我认为展示代码很重要,因为英语可以非常模糊和含糊不清。关于你的第二点,我很高兴你提出了这个问题。我认为C++委员会需要解决这个问题,并推荐安全的类型转换技术。在库工作组中已经进行了讨论,特别是请求核心或演化工作组就如何最好地完成此任务提供指导。据我所知,这些问题到目前为止尚未得到回答。然而,我目前的信念是编译器编写者不会违反C99合同。 - Howard Hinnant
4
据我所知,在C++中,memcpy是推荐的类型转换方法。虽然C++编译器可能会保持C99联合保证,但在C++中仍然是未定义的行为。 - bames53
2
对于C++11及以下版本,您可以使用我上面的联合技巧来避免别名问题:union A { float x; int32_t y; }; int32_t value = A{3.14f}.y;(我并不认为这比不使用临时变量更安全 :D)。这个“技巧”的原因是初始化程序是一个prvalue,因此不受别名规则限制。然而,在C++14中,这将会改变,因为初始化程序将是一个xvalue :) - Johannes Schaub - litb
2
@JohannesSchaub-litb 我不相信在C++中,严格别名规则是联合类型转换的唯一限制。例如,我认为您的示例违反了_8.5 [dcl.init] / 16_,其中它说“正在初始化的对象的初始值是初始化表达式的(可能转换的)值。”因为由A {3.14f} .y指定的对象没有任何值。C ++在这种情况下省略了行为规范,因此行为是未定义的。 - bames53
1
@JohannesSchaub-litb,“C99保证”是指读取非活动成员将重新解释活动成员的存储表示的保证。在C11中,他们添加了一个明确的注释,但我还没有仔细查看C规范,不知道C99规范的规范语言是否真正意味着C11注释所声称的内容。 - bames53
显示剩余12条评论

1

类型转换会引起未定义行为。无论使用何种类型的转换,都将引起未定义行为。

大多数编译器会按照你的预期执行,但gcc喜欢捣乱,可能会认为你没有分配指针,重新排列操作以产生一些奇怪的结果。

将指针强制转换为不兼容的类型并对其进行解引用是未定义行为。唯一的例外是将其转换为或从char,因此唯一的解决方法是使用 std::memcpy(根据R. Martinho Fernandes'的回答)。 (我不确定使用联合定义了多少;尽管如此,它仍然有更好的工作机会)。

话虽如此,你在C++中不应该使用C风格的转换。在这种情况下,static_cast不会编译,dynamic_cast也不会,迫使你使用reinterpret_cast,而reinterpret_cast强烈建议你可能会违反严格别名规则。


2
有不同的原因导致未定义行为。当然,在可能出现float(通常)或int(罕见但可能存在)的陷阱表示的情况下,标准委员会无法定义它。另一方面,意图显然是它应该具有熟悉架构的人所期望的行为。当强制转换立即可见时,违反此行为的编译器就是有问题的。 - James Kanze
3
当强制类型转换立即可见时,打破此规则的编译器符合规范。GCC就是这样做的(事实上,我认为它不会将其破坏,因为整数指针被转换回来,但它几乎可以肯定从原始变量中读取原始值,因为它删除了依赖关系,并且认为在浮点寄存器中留下的值是有效的)。 - Jan Hudec
2
@JamesKanze:最好在所有情况下立即停止程序,而不是有时工作,有时不工作。如果您将转换操作移出函数并且它悄悄地失败了,您会感到满意吗? - Zan Lynx
1
@JamesKanze:不是的。您混淆了两个非常不同的未定义行为。reinterpret_cast的行为是“未定义的”。这意味着编译器可以自由地做任何事情,从您期望的到格式化硬盘并使恶魔从您的鼻子飞出。但是memcpy的行为是“实现定义的”。规范说实现必须复制实际字节,并且仅表示相同位所表示的值不会被指定。是的,如果该值是信号NaN,则可能会导致故障,但可能不涉及任何恶魔。并且必须是确定性的。 - Jan Hudec
1
@JamesKanze:当访问未初始化为float的内容时,将其视为float也是实现定义的。未定义的是访问同一内存位置作为两个不兼容类型之一的别名规则(称为char)。char异常允许您通过memcpy或其他标准库函数来初始化某些不是float的内容,并将其视为float进行访问,仍然是(实现)定义的。 - Jan Hudec
显示剩余11条评论

1

查看this以获取有关类型玩弄和严格别名的更多信息。

将类型转换为数组的唯一安全转换是转换为char数组。如果您想要一个数据地址可以切换到不同的类型,您需要使用一个union


1
使用联合体进行类型转换也不符合标准(尽管通常可以正常工作)。 - jogojapan
1
@jogojapan - 严格来说,类型转换联合体确实违反了严格别名规则并且没有定义,但它们是足够常见的习语,以至于GCC支持它们(而且不会生成警告)。 - doron
2
严格别名规则没有提到它们(至少没有明确提到)。但是联合的规则指出,您只能从最后设置的联合成员中读取。对于类型转换,您必须先写入一个成员,然后再读取另一个成员。 - jogojapan
1
@jogojapan 3.10/10在第6点明确提到了union。通过包含对象动态类型作为成员的union的glvalue访问存储在对象中的值是定义良好的行为。您可能会违反“最后一个成员设置”规则,但这不会违反别名。 </nitpick> - Casey
4
我已在核心工作组邮件列表上发起了一个关于此事的查询。 - Howard Hinnant
显示剩余11条评论

1
基于这里的答案,我做了一个现代化的“伪装”函数,以便更容易地应用。
C99版本 (虽然大多数编译器都支持它,但在某些情况下可能会出现未定义的行为)
template <typename T, typename U>
inline T pseudo_cast(const U &x)
{
    static_assert(std::is_trivially_copyable<T>::value && std::is_trivially_copyable<U>::value, "pseudo_cast can't handle types which are not trivially copyable");

    union { U from; T to; } __x = {x};
    return __x.to;
}

通用版本 (基于被接受的答案)

将大小相同的类型强制转换:

#include <cstring>

template <typename T, typename U>
inline T pseudo_cast(const U &x)
{
    static_assert(std::is_trivially_copyable<T>::value && std::is_trivially_copyable<U>::value, "pseudo_cast can't handle types which are not trivially copyable");
    static_assert(sizeof(T) == sizeof(U), "pseudo_cast can't handle types with different size");
    
    T to;
    std::memcpy(&to, &x, sizeof(T));
    return to;
}

使用任意大小的类型转换:

#include <cstring>

template <typename T, typename U>
inline T pseudo_cast(const U &x)
{
    static_assert(std::is_trivially_copyable<T>::value && std::is_trivially_copyable<U>::value, "pseudo_cast can't handle types which are not trivially copyable");

    T to = T(0);
    std::memcpy(&to, &x, (sizeof(T) < sizeof(U)) ? sizeof(T) : sizeof(U));
    return to;
}

使用方法如下:
float f = 3.14f;
uint32_t u = pseudo_cast<uint32_t>(f);

C++20更新

C++20在头文件<bit>中引入了constexprstd::bit_cast,对于大小相同的类型功能上等价。然而,如果您想要自己实现此功能(假设不需要constexpr),或者想要支持具有不同大小的类型,则仍然可以使用上述版本。


C++20在#include <bit>中使用std::bit_cast<T>实现。 - Peter Cordes
@PeterCordes 是的,这使得它有些过时了。然而,如果你想自己实现它,它仍然是有用的 - 假设不需要 constexpr。我会更新答案。 - plasmacel

0
这里唯一可行的类型转换是 reinterpret_cast。(甚至有些编译器会尽力确保它不起作用。)
但你实际上想做什么?肯定有更好的解决方案,不涉及类型切换。几乎没有什么情况下可以使用类型切换,它们都在非常低级别的代码中使用,例如序列化或实现C标准库(例如modf函数)。否则(甚至在序列化中),ldexpmodf等函数可能会更好地工作,并且肯定更可读。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接