浮点数不是完美精确的表示,尤其是在涉及到舍入误差和不同数值规模时。
一般实现:
fabs(a - b) < epsilon
这种通过比较差的绝对值来判断浮点数是否相等的方法,存在一些潜在的问题:
- 误差累积:在连续的浮点数运算中,误差可能会逐步累积。这会导致最后的结果偏离真实值,这样用差值来比较就可能得到错误的结果。
- 选择合适的epsilon不好确定:epsilon 的选择可能依赖于具体的应用和上下文,并不能确定一个通用的值。太大或者太小的 epsilon 都可能造成不准确的判断。例如,太小的值可能会使接近但并非相等的数被误判为相等;而太大的数则可能让实际相等的数被误判为不等。
- 容易受到溢出和欠流的影响:如果两个浮点数的差值太大或者太小,可能会导致溢出或者欠流,从而无法准确的表示出实际的差值。
- 在一些需要极高精度的场合这种方式可能无法满足需求。
以下是一些改进浮点数比较策略的方法:
相对误差比较
使用相对误差而非绝对误差来比较浮点数。这涉及到计算两个数的差相对于数值大小的比例,以确定它们是否足够接近。
bool areAlmostEqual(double a, double b, double epsilon) {
// 在非常小的值附近使用绝对误差比较
if (fabs(a - b) < epsilon) {
return true;
}
// 使用相对误差比较
return fabs(a - b) < epsilon * fmax(fabs(a), fabs(b));
}
使用 ULPs 比较(单位最后位置)
ULPs 比较是一种浮点数比较策略,它通过比较两个浮点数之间的表示差异来确定它们是否足够接近。这种方法可以处理不同数值规模的准确性问题,但实现起来较为复杂。
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <limits>
#include <type_traits>
template <class T>
std::enable_if_t<not std::numeric_limits<T>::is_integer, bool>
equal_within_ulps(T x, T y, std::size_t n)
{
// Since `epsilon()` is the gap size (ULP, unit in the last place)
// of floating-point numbers in interval [1, 2), we can scale it to
// the gap size in interval [2^e, 2^{e+1}), where `e` is the exponent
// of `x` and `y`.
// If `x` and `y` have different gap sizes (which means they have
// different exponents), we take the smaller one. Taking the bigger
// one is also reasonable, I guess.
const T m = std::min(std::fabs(x), std::fabs(y));
// Subnormal numbers have fixed exponent, which is `min_exponent - 1`.
const int exp = m < std::numeric_limits<T>::min()
? std::numeric_limits<T>::min_exponent - 1
: std::ilogb(m);
// We consider `x` and `y` equal if the difference between them is
// within `n` ULPs.
return std::fabs(x - y) <= n * std::ldexp(std::numeric_limits<T>::epsilon(), exp);
}
int main()
{
double x = 0.3;
double y = 0.1 + 0.2;
std::cout << std::hexfloat;
std::cout << "x = " << x << '\n';
std::cout << "y = " << y << '\n';
std::cout << (x == y ? "x == y" : "x != y") << '\n';
for (std::size_t n = 0; n <= 10; ++n)
if (equal_within_ulps(x, y, n))
{
std::cout << "x equals y within " << n << " ulps" << '\n';
break;
}
}
建议:
- 优先使用数学库函数:一些数学库提供了特制的函数用于浮点数的比较,例如 C++ 的
std::numeric_limits<double>::epsilon()
可以提供double
类型可接受的最小差值。如果可行,优先使用这些函数,因为它们考虑了浮点数的底层细节。 - 避免差值比较:在设计算法和编写代码时,尽量避免将两个浮点数相减然后比较差值。这种情况下,尽可能设计算法以避免这种直接比较。
- 自定义比较运算符:在面向对象语言中(如 C++),你可以为使用到的浮点数类型定义自己的比较运算符,这些运算符内部使用更稳健的浮点数比较策略。
- 减少浮点数的使用:如转为int。