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

时间:2019-06-13 12:22:50

标签: c++ division modulo floating-accuracy

我正在尝试使用双精度进行整数除法+模运算(用于基于样条的插值),但是在使用std::floorstd::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;
}

https://ideone.com/9UbHcE

对于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

4 个答案:

答案 0 :(得分:2)

问题不在于您的fmod,而是您对floor的输入。由于浮点精度的模糊性,fmod可以返回接近分母的值。问题是,您必须小心使用与余数相同的规则来处理商,以便得出结果(使用模糊等式):

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

为了说明,我添加了div4div5

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逻辑,但在除法运算之前将输入的精度升级为floorlong 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会产生可以floorfmod化为正确的整数。四舍五入可以使您安全一会儿,但如上所述,只要错误不会变得太大,就可以了。

所以:

  • div1遇到此处描述的问题:https://en.cppreference.com/w/cpp/numeric/math/fmod

      

    x - trunc(x/y)*y的四舍五入以初始化fmod(x,y)的参数时,表达式x/y可能不等于trunc(例如:x = 30.508474576271183309y = 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),并且其最低有效位至少分别与denomwhole中的可用最低有效位位置一样大,因此其所有位都适合于浮动-点格式,因此它是可表示的,因此必须作为结果生成。此外,该余数为非负数。
  • 如果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};
}

并查看从div1div4的{​​{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 如果编译为本机代码也能做到相同。