我正在尝试使用双精度进行整数除法+模运算(用于基于样条的插值),但是在使用std::floor
和std::fmod
时遇到了一些与浮点精度有关的问题。
我一直在使用下面的div1
等效项,但是在50时会产生错误的结果(即整数部分为3,而模部分为除数减去epsilon)。 div2
有效,但令人费解。 div3
至少是一致的,但是不会返回我想要的结果类型(余数可能是负数,因此在我可以使用它之前需要进一步的操作)。
#include <iostream>
#include <cmath>
std::pair<double, double> div1(int num, double denom){
double whole = std::floor(num / denom);
double remain = std::fmod(num, denom);
return {whole, remain};
}
std::pair<double, double> div2(int num, double denom){
double floatdiv = num / denom;
double whole;
double remain = std::modf(floatdiv, &whole);
return {whole, remain * denom};
}
std::pair<double, double> div3(int num, double denom){
double whole = std::round(num / denom);
double remain = std::remainder(num, denom);
return {whole, remain};
}
int main() {
double denom = 100.0 / 6;
int divtype = 0;
for(auto div: {div1, div2, div3}){
std::cerr << "== Using div" << ++divtype << " for this run ==\n";
for(int i = 40; i <= 60; ++i){
auto res = div(i, denom);
std::cerr << i << ": " << res.first << ", " << res.second << " = " << res.first * denom + res.second << "\n";
}
auto oldprec = std::cerr.precision(64);
auto res = div(50, denom);
std::cerr << 50 << ": " << res.first << ", " << res.second << " = " << res.first << " * " << denom << " + " << res.second << " = " << std::floor(res.first) * denom + res.second << "\n";
std::cerr.precision(oldprec);
std::cerr << "\n";
}
return 0;
}
对于50,产生以下结果:
- div1: 3, 16.6666...
- div2: 3, 0
- div3: 3, -3e-15
我做错什么了吗?还是std::floor(num / denom)
+ std::fmod(num, denom)
不可靠?如果是这样,什么是好的替代品? div2
是最好的选择吗?
包含大多数答案的代码示例版本: https://ideone.com/l2wGRj
答案 0 :(得分:2)
问题不在于您的fmod
,而是您对floor
的输入。由于浮点精度的模糊性,fmod
可以返回接近分母的值。问题是,您必须小心使用与余数相同的规则来处理商,以便得出结果(使用模糊等式):
x/y == (quot, rem) == quot * y + rem
为了说明,我添加了div4
和div5
:
std::pair<double, double> div4( int num, double denom){
int quo;
auto rem = std::remquo(num, denom, &quo );
return {quo, rem};
}
std::pair<double, double> div5( int num, double denom){
auto whole = std::floor(num / static_cast<long double>( denom ) );
auto remain = std::fmod(num, denom);
return {whole, remain};
}
这里是一个slimmed-down version of your code,仅关注失败案例。输出为:
div1: 50 / 16.6666666666666678509 = (whole, remain) = (3, 16.6666666666666642982) = 66.6666666666666571928
...
div4: 50 / 16.6666666666666678509 = (whole, remain) = (3, -3.55271367880050092936e-15) = 50
div5: 50 / 16.6666666666666678509 = (whole, remain) = (2, 16.6666666666666642982) = 50
对于div1
,您得到了3的整数,其余(几乎)是一个除数。错误是,由于浮点模糊性,发送到floor
的值正好在行上,因此被提高到3,该值实际上应该是2。
如果使用我的div5
(同时使用std::remquo
来计算余数和商),您将得到相似的对(2, ~divisor)
,然后将它们全部正确地相乘返回50.(请注意,商以整数形式返回,而不是该标准函数的浮点数。)[更新:如注释中所述,这仅对3位精度有效商,这对需要检测象限或八分位的周期性函数很有用,而不是一般商。]
或者,如果您使用我的div4
,则我使用了div1
逻辑,但在除法运算之前将输入的精度升级为floor
至long double
,这给了它足够的位数正确评估地板。结果为(3, ~0)
,它显示了余数而不是商的模糊性。
long double
方法最终只是以更高的精确度将罐子踢向了相同问题的道路。对于周期函数的有限情况,使用std::remquo
在数值上更可靠。您选择哪个版本将取决于您更关心什么:数值计算或漂亮的显示。
更新:您还可以尝试使用FP异常来检测何时出现问题:
void printError()
{
if( std::fetestexcept(FE_DIVBYZERO) ) std::cout << "pole error occurred in an earlier floating-point operation\n";
if( std::fetestexcept(FE_INEXACT) ) std::cout << "inexact result: rounding was necessary to store the result of an earlier floating-point operation\n";
if( std::fetestexcept(FE_INVALID) ) std::cout << "domain error occurred in an earlier floating-point operation\n";
if( std::fetestexcept(FE_OVERFLOW) ) std::cout << "the result of the earlier floating-point operation was too large to be representable\n";
if( std::fetestexcept(FE_UNDERFLOW) ) std::cout << "the result of the earlier floating-point operation was subnormal with a loss of precision\n";
}
// ...
// Calling code
std::feclearexcept(FE_ALL_EXCEPT);
const auto res = div(i, denom);
printError();
// ...
此报告inexact result: rounding was necessary to store the result of an earlier floating-point operation
的功能1、2、3和5。请参见 Coliru 。
答案 1 :(得分:2)
您的核心问题是denom = 100.0/6
与数学上的精确值denomMath = 100/6 = 50/3
不同,因为它不能表示为2的幂的和。我们可以写denom = denomMath + eps
(带有小的正或负ε)。分配后,denom
与最接近的浮点数没有区别!如果您现在尝试将某个值denomMath * k = denom * k + eps * k
除以denom
,对于足够大的k
,您将数学上(即精确算术)得到错误的结果-在这种情况下,您没有希望。这种情况发生的时间取决于所涉及的幅度(如果值<1,那么您所有的div
都将产生零的整数部分并且是精确的,而对于大于2^54
的值,您甚至不能表示奇数)。
但即使在此之前,也无法保证将denomMath
的(数学)倍数除以denom
会产生可以floor
或fmod
化为正确的整数。四舍五入可以使您安全一会儿,但如上所述,只要错误不会变得太大,就可以了。
所以:
div1
遇到此处描述的问题:https://en.cppreference.com/w/cpp/numeric/math/fmod
当
x - trunc(x/y)*y
的四舍五入以初始化fmod(x,y)
的参数时,表达式x/y
可能不等于trunc
(例如:x = 30.508474576271183309
,y = 6.1016949152542370172
)
在您的情况下,50 / denom
产生的数字与精确结果(3
相比稍大(3 - some epsilon
),因为denom
略大于{{ 1}})
您不能依靠denomMath
等于std::floor(num / denom) + std::fmod(num, denom)
。
num
遇到上述问题:就您而言,它可以工作,但如果尝试更多情况,您会发现div2
有点小而不是太大而已也会失败。
num / denom
具有上述承诺。它实际上为您提供了您希望的最精确的结果。
答案 2 :(得分:1)
对于正分子和分母,可以使用以下代码来计算数学上精确的商和余数,只要商不超过浮点格式的有效位数(对于2为 53 典型的double
):
std::pair<double, double> divPerfect(int num, double denom)
{
double whole = std::floor(num / denom);
double remain = std::fma(whole, -denom, num);
if (remain < 0)
{
--whole;
remain = std::fma(whole, -denom, num);
}
return {whole, remain};
}
理由:
num
/ denom
是整数,则可以表示,并且num / denom
必须产生它,并且std::floor(num / denom)
的值相同。否则,num / denom
可能会略微向上或向下舍入,这无法减少std::floor(num / denom)
,但可能会将其增加一。因此,double whole = std::floor(num / denom)
为我们提供了适当的商或更多。whole
是正确的,则std::fma(whole, -denom, num)
是精确的,因为精确的数学答案的大小小于denom
或等于whole
(如果whole
<denom
),并且其最低有效位至少分别与denom
或whole
中的可用最低有效位位置一样大,因此其所有位都适合于浮动-点格式,因此它是可表示的,因此必须作为结果生成。此外,该余数为非负数。whole
太高,则std::fma(whole, -denom, num)
是负数(但如上所述,仍然精确)。然后,我们更正whole
并重复std::fma
以得到准确的结果。 我希望可以避免第二个std::fma
:
std::pair<double, double> divPerfect(int num, double denom)
{
double whole = std::floor(num / denom);
double remain = std::fma(whole, -denom, num);
return 0 <= remain ? std::pair(whole, remain) : std::pair(whole - 1, remain + denom);
}
但是,我想再想一想。
答案 3 :(得分:0)
确实 认为这可能是fmod()
实现中的错误。根据cppreference.com上std::fmod的定义:
此函数计算的除法运算x / y的浮点余数正好是值x-n * y,其中n是x / y,其小数部分被截断了
因此我添加了:
std::pair<double, double> div4(int num, double denom){
double whole = std::floor(num / denom);
int n = trunc(num / denom) ;
double remain = num - n * denom ;
return {whole, remain};
}
并查看从div1
到div4
的{{1}}和49
的输出,我得到 1 :
51
确实能达到预期的效果。
1 因为这是我要立即处理的全部,所以上面的输出是通过通过Emscripten运行原始代码并使用clang转换代码添加到JavaScript中,然后再与node.js运行。因为这会与原始代码产生相同的“问题”,所以我希望/希望我的== Using div1 for this run ==
49: 2, 15.6667 = 49
50: 3, 16.6667 = 66.6667
51: 3, 1 = 51
50: 3, 16.66666666666666429819088079966604709625244140625 = 3 * 16.666666666666667850904559600166976451873779296875 + 16.66666666666666429819088079966604709625244140625 = 66.666666666666657192763523198664188385009765625
== Using div4 for this run ==
49: 2, 15.6667 = 49
50: 3, 0 = 50
51: 3, 1 = 51
50: 3, 0 = 3 * 16.666666666666667850904559600166976451873779296875 + 0 = 50
如果编译为本机代码也能做到相同。