将浮点数准确舍入到最接近的 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
的内容。因为 x
和 y
在内部具有完全相同的值,所以您永远不会得到不同的结果。现在,如果您在四舍五入之前添加一个小值(您提到的 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
我正在尝试编写一个通用函数,它将 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
的内容。因为 x
和 y
在内部具有完全相同的值,所以您永远不会得到不同的结果。现在,如果您在四舍五入之前添加一个小值(您提到的 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