如何将整数转换为浮点数并四舍五入为零?

Paw*_*ica 35 c++ floating-point rounding data-conversion

当整数转换为浮点数,并且值不能直接用目标类型表示时,通常选择最接近的值(IEEE-754 要求)。

我想将整数转换为浮点数,并朝零舍入,以防整数值不能直接由浮点类型表示。

例子:

int i = 2147483647;
float nearest = static_cast<float>(i);  // 2147483648 (likely)
float towards_zero = convert(i);        // 2147483520
Run Code Online (Sandbox Code Playgroud)

Eri*_*ers 25

从 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);
}
Run Code Online (Sandbox Code Playgroud)

在 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
Run Code Online (Sandbox Code Playgroud)
  • 省略#pragmag++ 下的似乎没有任何改变。

  • @chux正确地评论说标准没有明确说明fesetround()会影响static_cast<float>(i). 为了保证设置的舍入方向影响转换,使用std::nearbyint及其 -f和 -l变体。另请参阅std::rint及其许多特定于类型的变体。

  • 我可能应该查找格式说明符,以便为正整数和浮点数使用空格,而不是将其填充到前面的字符串常量中。

  • (我尚未测试以下代码段。)您的convert()功能将类似于

    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);
    }
    
    Run Code Online (Sandbox Code Playgroud)

  • 请注意,GCC 实现存在错误,有时可能会忽略舍入模式更改:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=34678 (3认同)
  • 我也同意这是总体[最佳方法](/sf/answers/4423740821/),但即使使用`std::fesetround(FE_TOWARDZERO)`,我也不认为C++是**指定** `static_cast&lt;float&gt;(i)` 将按需要执行,但这样做是完全合理的。 (2认同)
  • 对于 C++11 中添加的功能,最好指定“环境”是指整个进程还是仅指当前线程。切换整个过程的舍入模式是在相邻线程中产生奇怪结果的最佳方法...... (2认同)
  • @马修M。“对于 C++11 中添加的功能,最好指定“环境”是指整个进程还是仅指当前线程。”我没有检查 C++11,但至少检查了当前草案明确指定:“[cfenv.syn]浮点环境具有线程存储持续时间。” (2认同)

jjj*_*jjj 11

您可以使用std::nextafter.

int i = 2147483647;
float nearest = static_cast<float>(i);  // 2147483648 (likely)
float towards_zero = std::nextafter(nearest, 0.f);        // 2147483520
Run Code Online (Sandbox Code Playgroud)

但是你必须检查,如果static_cast<float>(i)是准确的,如果是,nextafter会朝 0 迈出一步,这可能是你不想要的。

您的convert函数可能如下所示:

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);
}
Run Code Online (Sandbox Code Playgroud)

这可能是因为sizeof(int)==sizeof(long),甚至sizeof(int)==sizeof(long long)在这种情况下long(...)可以表现未定义,当static_cast<float>(x)超过所述可能的值。在这种情况下,它可能仍然有效,具体取决于编译器。

  • “长”有什么帮助?`sizeof(int)` 可以等于 `sizeof(long)`。 (7认同)
  • 问题是如何检测何时需要“nextafter”。检查 `int(static_cast&lt;float&gt;(x)) == x` 可能会导致未定义的行为。例如:“2147483647”到“float”是“2147483648.0f”,然后返回“int”是未定义的行为,因为“2147483648”不能用“int”类型表示。请参阅:https://en.cppreference.com/w/cpp/language/implicit_conversion#Floating.E2.80.93integral_conversions。 (5认同)
  • 如果 `int` 是 32 位并且 `static_cast&lt;float&gt;(x)` 生成 2147483648,则未定义 `int(static_cast&lt;float&gt;(x))`。C++ 2018 (draft N4659) 7.10 [conv.fpint] 1 说“如果截断的值无法在目标类型中表示,则行为未定义。” C也有类似的措辞。 (4认同)
  • 如果“sizeof(long) == sizeof(int)”且“x = INT_MIN”,则“std::abs(long(x))”可以具有 UB。 (3认同)
  • 由于“std::abs”,“convert(INT_MIN)”是一个问题 (2认同)

chu*_*ica 11

我相信 AC 实现相关的解决方案有一个 C++ 对应物。


暂时更改转换使用的舍入模式,以确定在不精确的情况下采用哪种方式。

通常选择最接近的值(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;
}
Run Code Online (Sandbox Code Playgroud)


nju*_*ffa 11

我理解这个问题仅限于使用 IEEE-754 二进制浮点算法的平台,以及float映射到 IEEE-754 (2008) 的位置binary32。这个答案假设是这种情况。

正如其他答案所指出的那样,如果工具链和平台支持这一点,请使用提供的工具根据fenv.h需要设置转换的舍入模式。

如果这些都没有用,或慢,就不难在模仿截断intfloat转换。基本上,将整数归一化直到设置最高有效位,记录所需的移位计数。现在,将标准化整数移位到位以形成尾数,根据标准化移位计数计算指数,并根据原始整数的符号添加符号位。如果clz(计数前导零)原语可用,则规范化过程可以显着加快,可能作为内在函数。

下面经过详尽测试的代码演示了 32 位整数的这种方法,请参阅函数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;
}
Run Code Online (Sandbox Code Playgroud)

  • `i &gt;&gt; 8`、`i += (127 + 31 - 1 - shift) &lt;&lt; 23` 假定 `float` 特性。并非不合理的假设,但未由 C 指定。 `memcpy (&amp;r, &amp;i, sizeof r);` 还依赖于关于 `float` 大小匹配 `int32_t` 以及常见整数和 FP 字节序的合理但未指定的假设。 (2认同)

chu*_*ica 5

一种指定的方法。


“通常选择最接近的值(IEEE-754 要求)”暗示 OP 期望涉及 IEEE-754。许多 C/C++ 实现确实遵循 IEEE-754 的大部分内容,但并不要求遵守该规范。以下依赖于 C 规范。

整数类型到浮点类型的转换指定如下。注意转换没有指定依赖于舍入模式。

当整数类型的值转换为实浮点类型时,如果被转换的值可以准确地表示为新类型,则该值不变。如果要转换的值在可以表示但不能精确表示的值范围内,则结果是最接近的较高或最接近较低的可表示值,以实现定义的方式选择。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;
}
Run Code Online (Sandbox Code Playgroud)


Pet*_*des 5

更改舍入模式有点昂贵,但我认为一些现代 x86 CPU 确实重命名了 MXCSR,因此它不必耗尽乱序执行后端。

如果您关心性能,将 njuffa 的纯整数版本(使用shift = __builtin_clz(i); i<<=shift;)与舍入模式更改版本进行基准测试是有意义的。(确保在您想要使用它的上下文中进行测试;它是如此之小,以至于它与周围代码重叠的程度很重要。)

AVX-512 可以在每条指令的基础上使用舍入模式覆盖,让您使用自定义舍入模式进行转换,成本与普通 int->float 基本相同。(不幸的是,目前仅在 Intel 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.
}
Run Code Online (Sandbox Code Playgroud)

_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
Run Code Online (Sandbox Code Playgroud)

在循环中,相同的归零寄存器可以作为合并目标重新使用,从而允许将vxorps归零提升到循环之外。

使用_mm_undefined_ps()代替_mm_setzero_ps(),我们可以让 ICC 在转换成 XMM0 之前跳过归零,就像(float)i在这种情况下clang 对 plain 所做的那样。但具有讽刺意味的是,_mm_undefined_ps()在这种情况下,通常对错误依赖关系傲慢和鲁莽的 clang 编译与 setzero 相同。

vcvtsi2ss无论您是否使用舍入模式覆盖(Ice Lake 上的 2 uops,相同的延迟:https : //uops.info/),实践中的性能(标量整数到标量单精度浮点数)都是相同的。AVX-512 EVEX 编码比 AVX1 长 2 个字节。


舍入模式覆盖还会抑制 FP 异常(如“不精确”),因此您无法检查 FP 环境以稍后检测转换是否准确(无舍入)。但在这种情况下,转换回 int 并进行比较就可以了。(由于向 0 舍入,您可以这样做而没有溢出的风险)。