C++中如何减少浮点数转换为定点数时的舍入误差?

efe*_*373 2 c++ ieee-754 bitset

我有一个浮点变量,每一步递增 0.1。我想将其转换为 16 位固定值,其中有 5 位小数部分。为了做到这一点,我有下面的代码片段:

#include <iostream>
#include <bitset>
#include <string>

using namespace std;

int main() {
    bitset<16> mybits;
    string mystring;
    float x = 1051.0;
    for (int i = 0; i < 20; i++)
    {
        mybits = bitset<16>(x*32);
        mystring = mybits.to_string<char, string::traits_type, string::allocator_type>();
        cout << x << "\t" << "mystring: " << mystring << '\n';
        x += 0.1;
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

然而,结果是这样的:

1051    mystring: 1000001101100000
1051.1  mystring: 1000001101100011
1051.2  mystring: 1000001101100110
1051.3  mystring: 1000001101101001
1051.4  mystring: 1000001101101100
1051.5  mystring: 1000001101101111
1051.6  mystring: 1000001101110011
1051.7  mystring: 1000001101110110
1051.8  mystring: 1000001101111001
1051.9  mystring: 1000001101111100
1052    mystring: 1000001101111111
1052.1  mystring: 1000001110000011
1052.2  mystring: 1000001110000110
1052.3  mystring: 1000001110001001
1052.4  mystring: 1000001110001100
1052.5  mystring: 1000001110001111
1052.6  mystring: 1000001110010011
1052.7  mystring: 1000001110010110
1052.8  mystring: 1000001110011001
1052.9  mystring: 1000001110011100
Run Code Online (Sandbox Code Playgroud)

小数部分有问题。例如 1051.5 应该是1000001101110000,而不是1000001101101111(由于浮点变量的性质,小数部分是错误的)。1052.0和1052.5也存在问题。我该如何修复它?

Eri*_*hil 5

\n

C++中如何减少浮点数转换为定点数时的舍入误差?

\n
\n

重新安排定点编码的计算以将结果舍入为整数,以便精确执行其中的所有算术,直到舍入之前的单个除法,如mybits = bitset<16>(std::round((x*10 + i)*32/10));。这将产生正确的结果,直到超过i= 317,169。(x += 0.1;从循环中删除;x在这个新公式中用作不变值。)

\n

该问题源于以下事实:.1 无法以基于二进制的浮点格式表示,因此源文本0.1转换为 0.1000000000000000055511151231257827021181583404541015625 (当 IEEE-754 \xe2\x80\x9c 双精度\xe2\x80\x9d被使用对于double),每次将其添加到x(in x += 0.1;) 都会执行一个运算,将理想实数算术和四舍五入到可以在 中表示的最接近的值double,并且,由于xfloat,再次将其四舍五入到可以在float(通常是 IEEE-754 \xe2\x80\x9c 单精度\xe2\x80\x9d 格式)。

\n

迭代i中定点数的期望值为 1051 + i /10,转换为具有 5 个小数位的定点编码。其编码为 (1051 + i /10) \xe2\x80\xa2 32 四舍五入到最接近的整数。因此我们要计算的值是 round((1051 + i /10) \xe2\x80\xa2 32),其中 \xe2\x80\x9cround\xe2\x80\x9d 是所需的舍入到整数函数(例如如舍入到最近的关系到偶数,或舍入到最近的关系到远离)。

\n

我们可以将其写为分数 ((1051\xe2\x80\xa210 + i )\xe2\x80\xa232) / 10。这样做的优点是 (1051\xe2\x80\xa210 + i )\xe2\ x80\xa232 是一个整数,可以使用整数或浮点算术进行精确计算,只要它保持在精确算术的范围内即可。(对于 \xe2\x80\x9c 单精度\xe2\x80\x9d 格式,这意味着 (1051\xe2\x80\xa210 + i )\xe2\x80\xa232 \xe2\x89\xa4 2 24,所以i \xe2 \x89\xa4 2 19 \xe2\x88\x9210,510 = \xc2\xa0513,778。)

\n

那么唯一不需要的舍入是在除法中。该除法发生在所需舍入为整数之前,因此任何其他操作都不会加剧这种情况。因此,我们可以将定点编码计算为std::round((x*10 + i)*32/10)并且只关心除以十时的舍入误差。(要使用std::round,请包括<cmath>。请注意,std::round将中间情况舍入到远离零的位置。要使用当前的浮点舍入模式,通常默认情况下舍入到最近的联系到偶数,请使用std::nearbyint。)

\n

仅当除法中的舍入导致 ( x \xe2\x80\xa210 + i )*32/10 的值(其小数部分不完全是 \xc2\xbd )变为以下值时,才会导致最终结果出现错误\xc2\xbd 的一小部分。(相反,不会发生导致 \xc2\xbd 分数的值变成具有其他分数的值,因为具有 \xc2\xbd 分数的值可以完全用二进制浮点数表示,因此不会发生舍入。一个例外是,如果数字太大,它将超出任何分数可表示的点。但是,对于 IEEE-754 \xe2\x80\x9csingle precision\xe2\x80\x9d 格式,不会发生这种情况,除非值也溢出了 Q10.5 格式。)

\n

假设使用舍入到最近值,则任何计算结果最多与实际算术结果相差 \xc2\xbd ULP。(\xe2\x80\x9cULP\x​​e2\x80\x9d 代表 \xe2\x80\x9c 最小精度单位,\xe2\x80\x9d 表示给定指数缩放后有效数中最低位的有效位置值。)因此, ( x \xe2\x80\xa210 + i )*32/10 仅当其分数部分至多为该值的 \xc2\xbd ULP 时,才可以舍入为带有分数 \xc2\xbd 的值。任何此类商的最接近的分数部分可以是 \xc2\xbd 而不是 \xc2\xbd 是 4/10 或 6/10。它们与 \xc2\xbd 的距离是 1/10。因此只要 1/10 超过 \xc2\xbd ULP,std::round((x*10 + i)*32/10)就会产生所需的结果。

\n

对于 [2 19 , 2 20 ) 中的数字, \xe2\x80\x9c 单精度\xe2\x80\x9d 格式的 ULP 为 2 \xe2\x88\x924 = 1/16,小于 1/10。因此,仅考虑非负i,只要(x*10 + i)*32/10< 2 20,结果就是正确的。对于x= 1051,这给我们 (1051\xe2\x80\xa210 + i)\xe2\x80\xa232/10 < 2 20 \xe2\x87\x92 i< 317,170。

\n

因此我们至少可以使用mybits = bitset<16>(std::round((x*10 + i)*32/10));到= 317,169。i

\n