[英]How can I convert an integer to float with rounding towards zero?
当一个 integer 被转换为浮点数,并且该值不能直接由目标类型表示时,通常选择最接近的值(IEEE-754 要求)。
如果 integer 值不能直接用浮点类型表示,我想将 integer 转换为浮点数并舍入为零。
例子:
int i = 2147483647;
float nearest = static_cast<float>(i); // 2147483648 (likely)
float towards_zero = convert(i); // 2147483520
由于 C++11,可以使用浮点环境舍入方向管理器fesetround()
。 有四个标准舍入方向,并且允许添加额外的舍入方向。
#include <cfenv> // for fesetround() and FE_* macros
#include <iostream> // for cout and endl
#include <iomanip> // for setprecision()
#pragma STDC FENV_ACCESS ON
int main(){
int i = 2147483647;
std::cout << std::setprecision(10);
std::fesetround(FE_DOWNWARD);
std::cout << "round down " << i << " : " << static_cast<float>(i) << std::endl;
std::cout << "round down " << -i << " : " << static_cast<float>(-i) << std::endl;
std::fesetround(FE_TONEAREST);
std::cout << "round to nearest " << i << " : " << static_cast<float>(i) << std::endl;
std::cout << "round to nearest " << -i << " : " << static_cast<float>(-i) << std::endl;
std::fesetround(FE_TOWARDZERO);
std::cout << "round toward zero " << i << " : " << static_cast<float>(i) << std::endl;
std::cout << "round toward zero " << -i << " : " << static_cast<float>(-i) << std::endl;
std::fesetround(FE_UPWARD);
std::cout << "round up " << i << " : " << static_cast<float>(i) << std::endl;
std::cout << "round up " << -i << " : " << static_cast<float>(-i) << std::endl;
return(0);
}
在 g++ 7.5.0 下编译,生成的可执行文件输出
round down 2147483647 : 2147483520
round down -2147483647 : -2147483648
round to nearest 2147483647 : 2147483648
round to nearest -2147483647 : -2147483648
round toward zero 2147483647 : 2147483520
round toward zero -2147483647 : -2147483520
round up 2147483647 : 2147483648
round up -2147483647 : -2147483520
在 g++ 下省略#pragma
似乎没有任何改变。
@chux正确评论该标准没有明确 state fesetround()
影响static_cast<float>(i)
中的舍入。 为了保证设置的舍入方向影响转换,请使用std::nearbyint
及其f
和l
变体。 另请参阅std::rint
及其许多特定于类型的变体。
我可能应该查找格式说明符,以便为正整数和浮点数使用空格,而不是将其填充到前面的字符串常量中。
(我还没有测试过下面的代码片段。)你的convert()
function 会是这样的
float convert(int i, int direction = FE_TOWARDZERO){ float retVal = 0.; int prevdirection = std::fegetround(); std::fesetround(direction); retVal = static_cast<float>(i); std::fesetround(prevdirection); return(retVal); }
您可以使用std::nextafter
。
int i = 2147483647;
float nearest = static_cast<float>(i); // 2147483648 (likely)
float towards_zero = std::nextafter(nearest, 0.f); // 2147483520
但是你必须检查,如果static_cast<float>(i)
是准确的,如果是这样, nextafter
将 go 一步向 0,你可能不想要。
您的convert
function 可能如下所示:
float convert(int x){
if(std::abs(long(static_cast<float>(x))) <= std::abs(long(x)))
return static_cast<float>(x);
return std::nextafter(static_cast<float>(x), 0.f);
}
当sizeof(int)==sizeof(long)
<float> sizeof(int)==sizeof(long long)
static_cast<float>(x)
long(...)
可能的值。 根据编译器的不同,在这种情况下它可能仍然有效。
我相信 C 实现相关的解决方案有 C++ 对应解决方案。
在不精确的情况下临时更改转换使用的舍入模式来确定 go 的方式。
通常选择最接近的值(IEEE-754 要求)。
并不完全准确。 不精确的情况取决于舍入模式。
C 未指定此行为。 C允许这种行为,因为它是实现定义的。
如果要转换的值在可以表示但不能准确表示的值范围内,则结果是最接近的较高或最近的较低可表示值,以实现定义的方式选择。
#include <fenv.h>
float convert(int i) {
#pragma STDC FENV_ACCESS ON
int save_round = fegetround();
fesetround(FE_TOWARDZERO);
float f = (float) i;
fesetround(save_round);
return f;
}
我理解这个问题仅限于使用 IEEE-754 二进制浮点算术的平台,并且float
映射到 IEEE-754 (2008) binary32
。 这个答案假设是这种情况。
正如其他答案所指出的,如果工具链和平台支持这一点,请使用fenv.h
提供的工具根据需要设置转换的舍入模式。
在那些不可用或速度缓慢的情况下,在int
到float
转换期间模拟截断并不困难。 基本上,标准化 integer 直到设置最高有效位,记录所需的移位计数。 现在,将归一化的 integer 移动到位以形成尾数,根据归一化移位计数计算指数,并根据原始 integer 的符号添加符号位。 如果clz
(计数前导零)原语可用,则可以显着加快规范化过程,这可能是内在的。
下面经过详尽测试的代码演示了这种用于 32 位整数的方法,请参阅 function int32_to_float_rz
。 我使用英特尔编译器版本 13 成功地将其构建为 C 和 C++ 代码。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fenv.h>
float int32_to_float_rz (int32_t a)
{
uint32_t i = (uint32_t)a;
int shift = 0;
float r;
// take absolute value of integer
if (a < 0) i = 0 - i;
// normalize integer so MSB is set
if (!(i > 0x0000ffffU)) { i <<= 16; shift += 16; }
if (!(i > 0x00ffffffU)) { i <<= 8; shift += 8; }
if (!(i > 0x0fffffffU)) { i <<= 4; shift += 4; }
if (!(i > 0x3fffffffU)) { i <<= 2; shift += 2; }
if (!(i > 0x7fffffffU)) { i <<= 1; shift += 1; }
// form mantissa with explicit integer bit
i = i >> 8;
// add in exponent, taking into account integer bit of mantissa
if (a != 0) i += (127 + 31 - 1 - shift) << 23;
// add in sign bit
if (a < 0) i |= 0x80000000;
// reinterpret bit pattern as 'float'
memcpy (&r, &i, sizeof r);
return r;
}
#pragma STDC FENV_ACCESS ON
float int32_to_float_rz_ref (int32_t a)
{
float r;
int orig_mode = fegetround ();
fesetround (FE_TOWARDZERO);
r = (float)a;
fesetround (orig_mode);
return r;
}
int main (void)
{
int32_t arg;
float res, ref;
arg = 0;
do {
res = int32_to_float_rz (arg);
ref = int32_to_float_rz_ref (arg);
if (res != ref) {
printf ("error @ %08x: res=% 14.6a ref=% 14.6a\n", arg, res, ref);
return EXIT_FAILURE;
}
arg++;
} while (arg);
return EXIT_SUCCESS;
}
指定的方法。
“通常选择最接近的值(IEEE-754 要求)”意味着 OP 预计涉及 IEEE-754。 许多 C/C++ 实现确实遵循 IEEE-754 的大部分内容,但不需要遵守该规范。 以下依赖于 C 规格。
integer 类型到浮点类型的转换指定如下。 注意转换未指定为取决于舍入模式。
当 integer 类型的值转换为真正的浮点类型时,如果被转换的值可以在新类型中精确表示,则它是不变的。 如果要转换的值在可以表示但不能精确表示的值范围内,则结果是最接近的较高或最近的较低可表示值,以实现定义的方式选择。 C17dr § 6.3.1.4 2
当结果不准确时,转换后的值是最近的高点还是最近的低点?
往返int
--> float
--> int
是有保证的。
往返需要注意convert(near_INT_MAX)
转换到int
范围之外。
与其依赖long
或long long
具有比int
更宽的范围(C 未指定此属性),不如让代码在负侧进行比较,因为INT_MIN
(带有 2 的补码)可以预期精确转换为float
。
float convert(int i) {
int n = (i < 0) ? i : -i; // n <= 0
float f = (float) n;
int rt_n = (int) f; // Overflow not expected on the negative side
// If f rounded away from 0.0 ...
if (rt_n < n) {
f = nextafterf(f, 0.0); // Move toward 0.0
}
return (i < 0) f : -f;
}
更改舍入模式有点昂贵,尽管我认为一些现代 x86 CPU 确实重命名了 MXCSR,因此它不必耗尽无序执行后端。
如果您关心性能,将 njuffa 的纯 integer 版本(使用shift = __builtin_clz(i); i<<=shift;
)与舍入模式更改版本进行基准测试是有意义的。 (确保在您想要使用它的上下文中进行测试;它是如此之小,以至于它与周围代码的重叠程度很重要。)
AVX-512 可以在每条指令的基础上使用舍入模式覆盖,让您使用自定义舍入模式进行转换,其成本与普通 int->float 基本相同。 (不幸的是,目前仅在英特尔 Skylake 服务器和 Ice Lake CPU 上可用。)
#include <immintrin.h>
float int_to_float_trunc_avx512f(int a) {
const __m128 zero = _mm_setzero_ps(); // SSE scalar int->float are badly designed to merge into another vector, instead of zero-extend. Short-sighted Pentium-3 decision never changed for AVX or AVX512
__m128 v = _mm_cvt_roundsi32_ss (zero, a, _MM_FROUND_TO_ZERO |_MM_FROUND_NO_EXC);
return _mm_cvtss_f32(v); // the low element of a vector already is a scalar float so this is free.
}
_mm_cvt_roundi32_ss
是同义词,IDK 为什么英特尔同时定义了i
和si
名称,或者某些编译器可能只有一个。
这可以使用 Godbolt 编译器资源管理器上的所有 4 个主流 x86 编译器 (GCC/clang/MSVC/ICC) 高效编译。
# gcc10.2 -O3 -march=skylake-avx512
int_to_float_trunc_avx512f:
vxorps xmm0, xmm0, xmm0
vcvtsi2ss xmm0, xmm0, {rz-sae}, edi
ret
int_to_float_plain:
vxorps xmm0, xmm0, xmm0 # GCC is always cautious about false dependencies, spending an extra instruction to break it, like we did with setzero()
vcvtsi2ss xmm0, xmm0, edi
ret
在循环中,可以将相同的归零寄存器重新用作合并目标,从而允许将vxorps
归零提升到循环之外。
使用_mm_undefined_ps()
而不是_mm_setzero_ps()
,我们可以让 ICC 在转换为 XMM0 之前跳过归零,就像 clang 在这种情况下对普通(float)i
所做的那样。 但具有讽刺意味的是,clang 在这种情况下,通常对错误的依赖项不屑一顾,编译_mm_undefined_ps()
与 setzero 相同。
无论您是否使用舍入模式覆盖, vcvtsi2ss
(标量 integer 到标量单精度浮点数)在实践中的性能都是相同的(Ice Lake 上的 2 uops,相同的延迟: https://uops.info/ )。 AVX-512 EVEX 编码比 AVX1 长 2 个字节。
舍入模式覆盖还抑制 FP 异常(如“不精确”),因此您无法检查 FP 环境以稍后检测转换是否准确(无舍入)。 但在这种情况下,转换回 int 并进行比较就可以了。 (您可以这样做而没有溢出的风险,因为四舍五入为 0)。
一个简单的解决方案是使用更高精度的浮点数进行比较。 只要高精度浮点能准确表示所有整数,我们就可以准确比较float
结果是否更大。
对于 32 位整数, double
应该足够了,而long double
在大多数系统上对于 64 位就足够了,但最好是验证它。
float convert(int x) {
static_assert(std::numeric_limits<double>::digits
>= sizeof(int) * CHAR_BIT);
float f = x;
double d = x;
return std::abs(f) > std::abs(d)
? std::nextafter(f, 0.f)
: f;
}
对于非负值,这可以通过取 integer 值并右移直到最高设置位从右起小于 24 位(即 IEEE 单精度),然后移回来完成。
对于负值,您将右移,直到设置 24 及以上的所有位,然后移回。 对于向后移位,您首先需要将值转换为unsigned
以避免左移负值的未定义行为,然后将结果转换回int
之前转换为float
。
另请注意,从无符号到有符号的转换是实现定义的,但是我们已经在处理 ID,因为我们假设float
是 IEEE754,而int
是二进制补码。
float rount_to_zero(int x)
{
int cnt = 0;
if (x >= 0) {
while (x != (x & 0xffffff)) {
x >>= 1;
cnt++;
}
return x << cnt;
} else {
while (~0xffffff != (x & ~0xffffff)) {
x >>= 1;
cnt++;
}
return (int)((unsigned)x << cnt);
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.