如何提高计算浮点数的精度?

how to improve the precision of computing float numbers?

本文关键字:精度 浮点数 何提高 计算      更新时间:2023-10-16

我在Visual Studio Community 2019 Microsoft C++中编写了一个代码片段,如下所示:

int m = 11;
int p = 3;
float step = 1.0 / (m - 2 * p);

变量步长是 0.200003,0.2 是我想要的。有什么建议可以提高精度吗?

这个问题来自均匀结矢量。结向量是NURBS中的一个概念。你可以认为它只是一个数字数组,如下所示:U[] = {0, 0.2, 0.4, 0.6, 0.8, 1.0};两个相邻数字之间的跨度是一个常量。节点矢量的大小可以根据某些条件更改,但范围在 [0, 1] 中。

整个功能是:


typedef float NURBS_FLOAT;
void CreateKnotVector(int m, int p, bool clamped, NURBS_FLOAT* U)
{
if (clamped)
{
for (int i = 0; i <= p; i++)
{
U[i] = 0;
}
NURBS_FLOAT step = 1.0 / (m - 2 * p);
for (int i = p+1; i < m-p; i++)
{
U[i] = U[i - 1] + step;
}
for (int i = m-p; i <= m; i++)
{
U[i] = 1;
}
}
else
{
U[0] = 0;
NURBS_FLOAT step = 1.0 / m;
for (int i = 1; i <= m; i++)
{
U[i] = U[i - 1] + step;
}
}
}

让我们关注一下代码中发生的事情:

  1. 表达式1.0 / (m - 2 * p)产生 0.2,最接近的可表示double值为 0.2000000000000000011102230246251565404236316680908203125。注意它是多么精确 - 到 16 位有效的十进制数字。这是因为,由于1.0double文字,分母被提升为double,并且整个计算以双精度完成,从而产生double值。

  2. 在上一步中获得的值将写入step,其类型为float。因此,该值必须四舍五入到最接近的可表示值,恰好是 0.20000000298023223876953125。

所以你引用的结果 0.200003 不是你应该得到的。相反,它应该更接近 0.2000000003。

有什么建议可以提高精度吗?

是的。将值存储在更高精度的变量中。例如,使用double step代替float step。在这种情况下,您计算的值不会再次四舍五入,因此精度会更高。

你能得到确切的 0.2 值以在随后的计算中使用它吗?不幸的是,使用二进制浮点运算没有。在二进制中,数字 0.2 是一个周期分数:

0.210= 0.0̅0̅1̅1̅2= 0.0011 0011 0011...阿拉伯数字

有关更多详细信息,请参阅浮点数学是否损坏?问题及其答案。

如果你真的需要十进制计算,你应该使用库解决方案,例如 Boost 的cpp_dec_float。或者,如果您需要任意精度计算,您可以使用例如cpp_bin_float来自同一库。请注意,这两种变体都比使用二进制浮点类型慢C++几个数量级。

在处理浮点数学时,预计会出现一定数量的舍入误差。

对于初学者来说,像0.2这样的值并不完全由float表示,甚至不能用double表示:

std::cout << std::setprecision(60) << 0.2 << 'n';
// ^^^ It outputs something like: 0.200000000000000011102230246251565404236316680908203125

此外,当对不精确的值执行一系列操作时,错误可能会累积。某些操作(如求和和减法(对此类错误比其他操作更敏感,因此最好尽可能避免它们。

这里似乎是这种情况,我们可以将 OP 的函数重写为如下所示的内容

#include <iostream>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <cassert>
#include <type_traits>
template <typename T = double> 
auto make_knots(int m, int p = 0)   // <- Note that I've changed the signature. 
{
static_assert(std::is_floating_point_v<T>);
std::vector<T> knots(m + 1);
int range = m - 2 * p;
assert(range > 0);
for (int i = 1; i < m - p; i++)
{
knots[i + p] = T(i) / range;  // <- Less prone to accumulate rounding errors
}
std::fill(knots.begin() + m - p, knots.end(), 1.0);
return knots;
}
template <typename T>
void verify(std::vector<T> const& v)
{
bool sum_is_one = true;
for (int i = 0, j = v.size() - 1; i <= j; ++i, --j)
{
if (v[i] + v[j] != 1.0)   // <- That's a bold request for a floating point type
{
sum_is_one = false;
break;
}
}
std::cout << (sum_is_one ? "n" : "Rounding errors.n");
}
int main()
{
// For presentation purposes only
std::cout << std::setprecision(60) << 0.2 << 'n';
std::cout << std::setprecision(60) << 0.4 << 'n';
std::cout << std::setprecision(60) << 0.6 << 'n';
std::cout << std::setprecision(60) << 0.8 << "nn";
auto k1 = make_knots(11, 3);
for (auto i : k1)
{
std::cout << std::setprecision(60) << i << 'n';
}
verify(k1);
auto k2 = make_knots<float>(10);
for (auto i : k2)
{
std::cout << std::setprecision(60) << i << 'n';
}
verify(k2);
}

可在此处测试。

避免漂移的一种解决方案(我想这是你担心的?(是手动使用有理数,例如在这种情况下,您可能有:

// your input values for determining step
int m = 11;
int p = 3;
// pre-calculate any intermediate values, which won't have rounding issues
int divider = (m - 2 * p); // could be float or double instead of int
// input
int stepnumber = 1234; // could also be float or double instead of int
// output
float stepped_value = stepnumber * 1.0f / divider;

换句话说,制定你的问题,以便在内部step原始代码始终是 1(或者你可以使用 2 个整数精确表示的任何有理数(,这样就没有舍入问题。如果您需要为用户显示值,那么您可以只显示:1.0 / divider并四舍五入到合适的位数。