簡體   English   中英

std::fmod 糟糕的雙精度

[英]std::fmod abysmal double precision

fmod(1001.0, 0.0001)給出0.00009999999995 ,考慮到預期的結果0 ,這似乎是一個非常低的精度 (10 -5 )。

根據cppreferencefmod()可以使用remainder() ,但remainder(1001.0, 0.0001)給出-4.796965775988316e-14 (仍然遠非double精度,但比 10 -5好得多)。

為什么fmod精度如此依賴輸入參數? 正常嗎?

MCVE:

#include <cmath>
#include <iomanip>
#include <iostream>
using namespace std;

int main() {
    double a = 1001.0, b = 0.0001;
    cout << setprecision(16);
    cout << "fmod:      " << fmod(a, b) << endl;
    cout << "remainder: " << remainder(a, b) << endl;
    cout << "actual:    " << a-floor(a/b)*b << endl;
    cout << "a/b:       " << a / b << endl;
}

輸出:

fmod:      9.999999995203035e-05
remainder: -4.796965775988316e-14
actual:    0
a/b:       10010000

(與 GCC、Clang、MSVC 的結果相同,有和沒有優化)

現場演示

如果我們將您的程序修改為:

#include <cmath>
#include <iomanip>
#include <iostream>

int main() {
    double a = 1001.0, b = 0.0001;
    std::cout << std::setprecision(32) << std::left;
    std::cout << std::setw(16) << "a:" << a << "\n"; 
    std::cout << std::setw(16) << "b:" << b << "\n"; 
    std::cout << std::setw(16) << "fmod:" << fmod(a, b) << "\n";
    std::cout << std::setw(16) << "remainder:" << remainder(a, b) << "\n";
    std::cout << std::setw(16) << "floor a/b:" << floor(a/b) << "\n";
    std::cout << std::setw(16) << "actual:" << a-floor(a/b)*b << "\n";
    std::cout << std::setw(16) << "a/b:" << a / b << "\n";
    std::cout << std::setw(16) << "floor 10009999:" << floor(10009999.99999999952) << "\n";
}

它輸出:

a:              1001
b:              0.00010000000000000000479217360238593
fmod:           9.9999999952030347032290447106817e-05
remainder:      -4.796965775988315527911254321225e-14
floor a/b:      10010000
actual:         0
a/b:            10010000
floor 10009999: 10010000

我們可以看到0.0001不能表示為double所以b實際上設置為0.00010000000000000000479217360238593

這導致a/b10009999.9999999995203034224因此其意味着fmod應返回1001 - 10009999*0.000100000000000000004792173602385939.99999999520303470323e-5

(在 speedcrunch 中計算的數字可能與 IEEE 雙精度值不完全匹配)

您的“實際”值不同的原因是floor(a/b)返回10010000不是fmod使用的確切值10009999 ,這本身是由於10009999.99999999952不能表示為雙10009999.99999999952值,因此在被四舍五入為10010000之前傳遞到地板。

fmod產生准確的結果,沒有錯誤。

鑒於C ++源代碼fmod(1001.0, 0.0001)使用IEEE-754 binary64(對於最常用的格式在一個實施double ),源文本0.0001被轉換為double值0.000100000000000000004792173602385929598312941379845142364501953125。

然后1001 = 10009999•0.000100000000000000004792173602385929598312941379845142364501953125 + 0.000099999999952030347032290447106817055100691504776477813720703125,所以fmod(1001, 0.0001)是完全0.000099999999952030347032290447106817055100691504776477813720703125。

唯一的錯誤是將源文本中的十進制數字轉換為基於二進制的double格式。 fmod操作沒有錯誤。

這里的基本問題( 0.0001的 IEEE-754 表示)已經很好地建立起來,但只是為了踢球,我使用std::remainderhttps://en.cppreference.com/w/cpp復制了fmod的實現/numeric/math/fmod並將其與std::fmod

#include <iostream>
#include <iomanip>
#include <cmath>

// Possible implementation of std::fmod according to cppreference.com
double fmod2(double x, double y)
{
#pragma STDC FENV_ACCESS ON
    double result = std::remainder(std::fabs(x), (y = std::fabs(y)));
    if (std::signbit(result)) result += y;
    return std::copysign(result, x);
}

int main() {
    // your code goes here
    double b = 0.0001;
    std::cout << std::setprecision(25);
    std::cout << "              b:" << std::setw(35) << b << "\n"; 
    
    double m = 10010000.0;
    double c = m * b;
    double d = 1001.0 - m * b;
    std::cout << std::setprecision(32);
    std::cout << "     10010000*b:" << std::setw(6) << c << "\n"; 
    std::cout << std::setprecision(25);
    std::cout << "1001-10010000*b:" << std::setw(6) << d << "\n";
    
    long double m2 = 10010000.0;
    long double c2 = m2 * b;
    long double d2 = 1001.0 - m2 * b;
    std::cout << std::setprecision(32);
    std::cout << "     10010000*b:" << std::setw(35) << c2 << "\n"; 
    std::cout << std::setprecision(25);
    std::cout << "1001-10010000*b:" << std::setw(35) << d2 << "\n";
    
    std::cout << "      remainder:" << std::setw(35) << std::remainder(1001.0, b) << "\n"; 
    std::cout << "           fmod:" << std::setw(35) << std::fmod(1001.0, b) << "\n"; 
    std::cout << "          fmod2:" << std::setw(35) << fmod2(1001.0, b) << "\n"; 
    std::cout << " fmod-remainder:" << std::setw(35) <<
                 std::fmod(1001.0, b) - std::remainder(1001.0, b) << "\n"; 
    return 0;
}

結果是:

              b:     0.0001000000000000000047921736
     10010000*b:  1001
1001-10010000*b:     0
     10010000*b:  1001.0000000000000479616346638068
1001-10010000*b:    -4.796163466380676254630089e-14
      remainder:    -4.796965775988315527911254e-14
           fmod:     9.999999995203034703229045e-05
          fmod2:     9.999999995203034703229045e-05
 fmod-remainder:     0.0001000000000000000047921736

如最后兩行輸出所示,實際的std::fmod (至少在這個實現中)與 cppreference 頁面上建議的實現相匹配,至少在這個例子中是這樣。

我們還看到 IEEE-754 的 64 位精度不足以表明10010000 * 0.0001不同於整數。 但是如果我們轉到 128 位,小數部分就清楚地表示出來了,當我們從1001.0減去它時,我們發現余數與std::remainder的返回值大致相同。 (差異大概是由於std::remainder是用少於 128 位計算的;它可能使用 80 位算術。)

最后,請注意std::fmod(1001.0, b) - std::remainder(1001.0, b)結果等於 64 位 IEEE-754 值0.0001 也就是說,兩個函數都返回與模0.0001000000000000000047921736相同的值一致的結果,但std::fmod選擇最小的正值,而std::remainder選擇最接近零的值。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM