C++ 将浮点除法分解为整数部分和小数部分

C++ 将浮点除法分解为整数部分和小数部分,c++,division,modulo,floating-accuracy,C++,Division,Modulo,Floating Accuracy,我尝试使用双精度进行整数除法+模运算(用于基于样条曲线的插值),但在使用std::floor和std::fmod时遇到了一些与浮点精度相关的问题 我一直在使用下面的div1的等价物,但在50时它会产生错误的结果(即整数部分是3,而模部分是除数减去ε)div2可以工作,但相当复杂div3至少是一致的,但没有返回我想要的结果类型(剩余部分可能是负数,因此需要进一步操作才能使用它) 我是做错了什么,还是std::floor(num/denom)+std::fmod(num,denom)不可靠?如果是,

我尝试使用双精度进行整数除法+模运算(用于基于样条曲线的插值),但在使用
std::floor
std::fmod
时遇到了一些与浮点精度相关的问题

我一直在使用下面的div1的等价物,但在50时它会产生错误的结果(即整数部分是3,而模部分是除数减去ε)
div2
可以工作,但相当复杂
div3至少是一致的,但没有返回我想要的结果类型(剩余部分可能是负数,因此需要进一步操作才能使用它)

我是做错了什么,还是std::floor(num/denom)
+
std::fmod(num,denom)
不可靠?如果是,什么是好的替代品?
div2
是最佳选择吗

包含大多数答案的代码示例版本:
问题不在于您的
fmod
,而在于您对
楼层的输入。由于浮点精度的模糊性,
fmod
可以返回接近分母的值。问题是,您必须小心使用与余数相同的规则处理商,以便得出结果(使用模糊等式):

x/y==(quot,rem)==quot*y+rem

为了举例说明,我添加了
div4
div5

std::pairdiv4(int-num,double-denom){
现状;
自动rem=std::remquo(num,denom,&quo);
返回{quo,rem};
}
std::pairdiv5(int-num,double-denom){
自动整体=标准::地板(数量/静态_-cast(denom));
自动保持=std::fmod(num,denom);
返回{全部,保留};
}
下面是一个关于失败案例的例子。输出为:

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
,它用于同时计算余数和商,您将得到类似的对
(2,~除数)
,然后所有的对都正确地乘以50。(请注意,商以整数形式返回,而不是该标准函数中的浮点数。)[更新:如注释中所述,这仅对商中的3位精度有效,也就是说,它对需要检测四分之一或八分之一的周期函数有用,但对一般商无效。]

或者,如果您使用我的
div4
,我使用了您的
div1
逻辑,但在除法操作之前,将输入升级到
floor
long double
精度,这使它有足够的数字来正确计算floor。结果是
(3,~0)
,它显示余数而不是商的模糊性

long double
方法最终只是以更高的精度解决了同样的问题。对于周期函数的有限情况,使用
std::remquo
在数值上更可靠。你选择哪个版本取决于你更关心的是什么:数值计算还是漂亮的显示

更新:您还可以尝试使用FP异常来检测何时出现问题:

void printError()
{

如果(std::fetestexcept(FE_DIVBYZERO))std::cout你的核心问题是
denom=100.0/6
与数学上精确的值
denomMath=100/6=50/3
,因为它不能表示为二的幂和。我们可以编写
denom=denomMath+eps
(有一个小的正或负epsilon)。赋值后,
denom
与最近的浮点数无法区分!如果您现在尝试将某个值
denomMath*k=denom*k+eps*k
除以
denom
,对于足够大的
k
,您将在数学上得到错误的结果(即在精确的算术中)已经-在这种情况下,您没有希望。这种情况多久发生取决于所涉及的大小(如果值<1,则所有
div
将产生零的整部分,并且是精确的,而对于大于
2^54
的值,您甚至不能表示奇数)

但即使在此之前,也不能保证将
denomMath
的(数学)倍数除以
denom
就可以得到
floor
ed或
fmod
ed到正确的整数。四舍五入可能会让你安全一段时间,但如上所示,只要误差不太大

因此:

  • div1
    遇到此处描述的问题:

    当初始化
    trunc
    参数的
    x/y
    四舍五入失去太多精度时,表达式
    x-trunc(x/y)*y
    可能不等于
    fmod(x,y)
    ,例如:
    x=30.508474576271183309
    y=6.1016949152542370172

    在您的例子中,
    50/denom
    产生的数字与精确结果相比稍大(
    3
    ),因为
    denom
    稍大于
    denomMath

    您不能依靠
    std::floor(num/denom)+std::fmod(num,denom)
    来等于
    num

  • div2
    有上面描述的问题:在您的情况下它可以工作,但是如果您尝试更多的情况,您会发现
    num/denom
    稍微太小,而不是太大,它也会失败

  • div3
    具有上述承诺。它实际上为您提供了您所希望的最精确的结果

这确实让人觉得它可能是t中的一个bug
 - div1: 3, 16.6666... 
 - div2: 3, 0
 - div3: 3, -3e-15
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
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};
}
== 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
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};
}
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);
}