将浮点数准确舍入到最接近的 X 倍数的通用函数

Generic function to accurately round floating-point to the nearest multiple of X

我正在尝试编写一个通用函数,它将 double 输入值舍入到最接近 X 的倍数。

由于浮点精度的原因,简单的缩放和舍入方法可能会失败:

double round(double in, double multiple)
{
    return std::round(in / multiple) * multiple;
}

例如,由于 0.15 实际上存储为 ~0.14999999999999999445,因此调用上述函数舍入为 0.1 的倍数时将 return 0.1而不是0.2,如下面的godbolt.

所以也许我应该添加一个 epsilon 值,但是什么 epsilon 才是通用且足够的?

我看到有一个 std::numeric_limits<double>::epsilon(), and I see from 所以答案,我需要将值缩放到我正在使用的数字的大小。

那么我应该使用 multiple 输入参数作为我的比例因子吗?

如果是这样,我的 round 函数将变为:

double round(double in, double multiple)
{
    const double epsilon = std::numeric_limits<double>::epsilon() * multiple;
    return std::round((in + epsilon) / multiple) * multiple;
}

最后,为了使我的函数适用于负数,我使用 std::signbit 计算是否应该减去 epsilon 而不是加:

double round(double in, double multiple)
{
    const double epsilon = std::numeric_limits<double>::epsilon() * multiple;
    const double incr = std::signbit(in) ? -epsilon : epsilon;
    return std::round((in + incr) / multiple) * multiple;
}

我已经使用一系列输入和倍数对此进行了测试,对于我尝试过的输入,它似乎有效...但我不确定。

我的最终版本 round 准确吗?

如果不是,在什么情况下/对于什么输入它会无法产生我想要的结果,是否有可能产生准确和通用的函数?

您的舍入函数运行正常。 0.14999999999999999445 的值向下舍入,正如您所期望的那样。您面临的问题是 double 由于精度有限,值不能表示任意值。现在考虑使用 round() 函数的以下程序:

double x = 0.15;
double y = 0.14999999999999999445;

std::cout << round(x, 0.1) << " ";
          << round(y, 0.1) << std::endl;

它输出类似 0.1 0.1 的内容。因为 xy 在内部具有完全相同的值,所以您永远不会得到不同的结果。现在,如果您在四舍五入之前添加一个小值(您提到的 epsilon),您将得到 0.2 0.2。这对于 0.15 值可能是正确的,但对于另一个值则不再正确。

我想在这里向您展示的是,无论您如何仔细设计 round() 函数,由于精度限制,总会有不一致之处。 舍入截止值 将始终波动。有时它略高于 real 截止值,有时它可能略低于该截止值。你对此无能为力。

所以恐怕您的问题没有令人满意的解决方案。如果您想使用二进制浮点数,您必须接受一点模糊。

加法
当您询问第二个实现(带有 epsilon 的那个)失败的示例时,请查看 here。对于 0.35,舍入结果向 0.3.

舍入

除了

  • OP 预计 0.15,四舍五入到最接近的 0.1,四舍五入到 0.20 而不是 0.10。但是 none 的 0.15、0.1、0.20 和 0.10 完全 可编码为 binary64。相反,附近的值**(第一舍入)被使用并且打印的“0.10”是给定这些编码数字的最佳结果。

  • OP 的问题通过使用接近 half-way 点 0.10 和 0.20 的值突出显示,而不是将输出打印到 17 位以上的有效小数位以查看问题。

  • OP 的 round(in / multiple) * multiple 使问题更加复杂,因为除法和乘法都会产生舍入(第 2 和第 4 - 第 3 是 round())。

  • 显示的输出是第 5 次舍入。

要正确地四舍五入到最接近的倍数要么需要扩展的精度数学(如果它更宽,可能long double)或者简单地容忍这些接近half-way 例。

我建议不要添加 epsilon。任何 non-crafted 代码肯定会产生更多的角落错误。舍入到倍数的初始步骤需要 exact 数学。随意使用浮点数 */+- 不提供

而是考虑一个更窄的问题:double round_decimal(double x, int pow10)round_decimal(0.15, -1) 仍然会四舍五入到 0.1000000000000000055...,因为调用确实是 round_decimal(0.1499999999999999944..., -1),但至少可以编写合理的代码来提供最佳答案。 “将 floating-point 精确舍入到 X 的最接近倍数的通用函数” 更具挑战性


**

0.100000000000000005551115123125782702118158340454101562500000
0.149999999999999994448884876874217297881841659545898437500000
0.200000000000000011102230246251565404236316680908203125000000