在这种情况下,我应该使用
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;
}
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;
}
忘记使用类型转换。使用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指针并且其内部是“神奇”的,不会违反别名规则。
这里有一些很好的答案,涉及类型转换问题。
我想解决的是"快速反平方根"部分。在现代处理器上不要使用这个"技巧"。每个主流矢量指令集都有专门的硬件指令来提供快速反平方根。它们中的每一个都比这个经常被复制的小技巧更快且更准确。
这些指令都可以通过内联汇编调用,所以使用起来相对容易。在SSE中,您需要使用 rsqrtss
(内联函数: _mm_rsqrt_ss( )
);在NEON中,您需要使用 vrsqrte
(内联函数: vrsqrte_f32( )
);而在AltiVec中,您需要使用frsqrte
。大多数GPU指令集也有类似的指令。这些估计值可以使用相同的牛顿迭代法进行精细化,并且NEON甚至有vrsqrts
指令,可以在不需要加载常量的情况下完成部分精细化。
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);
更新
由于委员会的反馈,我不再认为这个答案是正确的。但是出于信息目的,我想保留它。并且我有意希望委员会可以使这个答案变得正确(如果它选择这样做)。也就是说,没有任何关于底层硬件的东西使这个答案不正确,只是委员会的判断使其如此或不如此。
我添加一个答案不是为了驳斥已接受的答案,而是为了补充它。我认为已接受的答案既正确又有效率(我刚刚点赞了它)。然而,我想演示另一种同样正确和有效的技术:
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的代码和这段代码,并逐行比较了汇编代码。三者完全相同。这是由于编译器选择以这种方式进行编译。编译器产生不同且错误的代码同样有效。
memcpy
是推荐的类型转换方法。虽然C++编译器可能会保持C99联合保证,但在C++中仍然是未定义的行为。 - bames53union A { float x; int32_t y; }; int32_t value = A{3.14f}.y;
(我并不认为这比不使用临时变量更安全 :D)。这个“技巧”的原因是初始化程序是一个prvalue,因此不受别名规则限制。然而,在C++14中,这将会改变,因为初始化程序将是一个xvalue :) - Johannes Schaub - litbA {3.14f} .y
指定的对象没有任何值。C ++在这种情况下省略了行为规范,因此行为是未定义的。 - bames53类型转换会引起未定义行为。无论使用何种类型的转换,都将引起未定义行为。
大多数编译器会按照你的预期执行,但gcc喜欢捣乱,可能会认为你没有分配指针,重新排列操作以产生一些奇怪的结果。
将指针强制转换为不兼容的类型并对其进行解引用是未定义行为。唯一的例外是将其转换为或从char,因此唯一的解决方法是使用 std::memcpy
(根据R. Martinho Fernandes'的回答)。 (我不确定使用联合定义了多少;尽管如此,它仍然有更好的工作机会)。
话虽如此,你在C++中不应该使用C风格的转换。在这种情况下,static_cast
不会编译,dynamic_cast
也不会,迫使你使用reinterpret_cast
,而reinterpret_cast
强烈建议你可能会违反严格别名规则。
float
(通常)或int
(罕见但可能存在)的陷阱表示的情况下,标准委员会无法定义它。另一方面,意图显然是它应该具有熟悉架构的人所期望的行为。当强制转换立即可见时,违反此行为的编译器就是有问题的。 - James Kanzefloat
的内容时,将其视为float
也是实现定义的。未定义的是访问同一内存位置作为两个不兼容类型之一的别名规则(称为char)。char异常允许您通过memcpy
或其他标准库函数来初始化某些不是float的内容,并将其视为float进行访问,仍然是(实现)定义的。 - Jan Hudec查看this以获取有关类型玩弄和严格别名的更多信息。
将类型转换为数组的唯一安全转换是转换为char
数组。如果您想要一个数据地址可以切换到不同的类型,您需要使用一个union
。
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>
中引入了constexpr
std::bit_cast
,对于大小相同的类型功能上等价。然而,如果您想要自己实现此功能(假设不需要constexpr
),或者想要支持具有不同大小的类型,则仍然可以使用上述版本。
constexpr
。我会更新答案。 - plasmacelreinterpret_cast
。(甚至有些编译器会尽力确保它不起作用。)modf
函数)。否则(甚至在序列化中),ldexp
和modf
等函数可能会更好地工作,并且肯定更可读。
union A { float x; int32_t y; }; int32_t value = A{3.14f}.y;
原因是这里从未应用别名规则。然而,此技巧仅适用于C++11及以下版本(我认为C++14将更改规则,以便这变成UB)。 - Johannes Schaub - litbpseudo_cast
也应该使用static_assert
来检查两种类型是否都是可平凡复制的(参见std::is_trivially_copyable
)。 - Ruslan