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

How to reduce the float rounding error when converting it into fixed-point in C++?

我有一个浮点变量,每一步递增 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;
}

然而,结果是这样的:

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

小数部分有问题。例如 1051.5 应该是 1000001101110000,而不是 1000001101101111(由于 float 变量的性质,小数部分是错误的)。 1052.0 和 1052.5 也有问题。我该如何解决?

How to reduce the float rounding error when converting it into fixed-point in C++?

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

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

迭代i中定点数的期望值为1051 + i/10,转换为定点数用五个小数位编码。它的编码是 (1051 + i/10) • 32 四舍五入到最接近的整数。所以我们要计算的值是 round((1051 + i/10) • 32),其中“round”是所需的舍入到整数函数(例如舍入到-最近的关系到偶数,或舍入到最近的关系到远离)。

我们可以把它写成分数 ((1051•10 + i)•32) / 10。这样做的好处是 (1051•10 + i)•32 是一个整数,可以用整数或浮点运算精确计算,只要它保持在精确运算的范围内。 (对于“单精度”格式,这意味着 (1051•10 + i)•32 ≤ 224,因此 i ≤ 219−10,510 = 513,778.)

那么唯一不需要的舍入是在除法中。该除法紧接在所需的整数舍入之前发生,因此它不会因任何其他操作而加剧。因此我们可以将定点编码计算为 std::round((x*10 + i)*32/10) 并且只关心除以 10 时的舍入误差。 (要使用 std::round,请包括 <cmath>。请注意,std::round 会舍入远离零的中途情况。要使用当前的浮点舍入模式,通常是 round-to-nearest-ties-to - 即使默认情况下,也使用 std::nearbyint.)

除法中的四舍五入只会导致最终结果出现错误,前提是它导致的值为 (x•10 + i)*32/10 其小数部分不正好是 ½ 成为小数正好是 ½ 的值。 (相反,导致小数为 ½ 的值变成其他小数的值不会发生,因为小数为 ½ 的值可以用二进制浮点数精确表示,因此不会发生舍入。一个例外是如果数字太大以至于超出了可以表示任何分数的点。但是,IEEE-754“单精度”格式不会发生这种情况,除非该值也溢出 Q10.5 格式。)

假设使用了最近舍入法,任何计算结果最多与实数算术结果相差 ½ ULP。 (“ULP”代表“最小精度单位”,给定指数缩放后有效位数中最低位的有效位置值。)因此,(x•10 + i)*32/10 只有当它的小数部分最多是该值的 ½ ULP 时,才能舍入到小数 ½ 的值。任何此类商的最接近 ½ 而不是 ½ 的小数部分是 4/10 或 6/10。这些与 ½ 的距离是 1/10。因此,只要 1/10 超过 ½ ULP,std::round((x*10 + i)*32/10) 就会产生所需的结果。

对于[219, 220)中的数字,“单精度”格式的ULP为2−4 = 1/16,小于1/10。因此,只考虑非负数i,只要(x*10 + i)*32/10 < 220,结果就正确。对于 x = 1051,这给了我们 (1051•10 + i)•32/10 < 220i < 317,170.

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

一种解决方案是从十进制浮点数或其他一些数字表示形式(想到有理数)进行转换。十进制浮点数通常内置于 gcc 中,但如果您使用其他编译器,则需要其他解决方案。此外,默认的 gcc 十进制浮点数非常重(如果只使用一次,二进制大小约为 +100k)。它们也相当不准确和缓慢。