繁体   English   中英

如何将 integer 转换为浮点数并舍入为零?

[英]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及其fl变体。 另请参阅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提供的工具根据需要设置转换的舍入模式。

在那些不可用或速度缓慢的情况下,在intfloat转换期间模拟截断并不困难。 基本上,标准化 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范围之外。

与其依赖longlong 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 为什么英特尔同时定义了isi名称,或者某些编译器可能只有一个。

这可以使用 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;
}
  1. 将 integer 右移一个算术移位,直到位数与浮点运算的精度一致。 计算班次。
  2. 将 integer 转换为浮点数。 结果现在是精确的。
  3. 将得到的浮点数乘以对应于班次数的 2 的幂。

对于非负值,这可以通过取 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM