<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為什么 0.1 + 0.2 = 0.3

          共 2073字,需瀏覽 5分鐘

           ·

          2020-06-25 23:22

          0.1 + 0.2 = 0.3 這個(gè)等式的成立看起來是理所當(dāng)然的,然而前面的文章 為什么 0.1 + 0.2 = 0.300000004 分析了為什么這個(gè)等式在絕大多數(shù)的編程語言中都不成立,標(biāo)準(zhǔn)的浮點(diǎn)數(shù)可以通過 32 位單精度浮點(diǎn)數(shù)或者 64 位的雙精度浮點(diǎn)數(shù)保證有限的精度,所有正確實(shí)現(xiàn)浮點(diǎn)數(shù)的編程語言都會(huì)遇到如下所示的『錯(cuò)誤』:

          > 0.1 + 0.20.30000000000000004

          浮點(diǎn)數(shù)作為編程語言中必不可少的概念,需要在性能和精度方面做出的權(quán)衡,過高的精度需要更多的位數(shù)以及更多的計(jì)算,過低的精度也無法滿足常見的計(jì)算需求,這種重要的決策會(huì)影響上層千千萬萬的應(yīng)用和服務(wù),然而這個(gè)決策需要面對(duì)的問題與軟件工程中需要解決的問題也沒有太多區(qū)別 — 如何盡可能地利用有限地資源實(shí)現(xiàn)特定目的

          b1a93e8ad2a76ed446e9d4b4cf819b3b.webp圖 1 - 性能和精度的權(quán)衡

          雖然浮點(diǎn)數(shù)提供了相對(duì)優(yōu)異的性能,但是在金融系統(tǒng)中使用精度低的浮點(diǎn)數(shù)會(huì)有非常嚴(yán)重的后果。假設(shè)我們?cè)诮灰姿蛘咩y行使用 64 位的雙精度浮點(diǎn)數(shù)存儲(chǔ)賬戶的余額,這時(shí)就存在被用戶攻擊的可能,用戶可以利用雙精度浮點(diǎn)數(shù)的精度限制造出更多余額:

          d03615d08a2d1f81c713a325af72e9f4.webp圖 2 - 金融系統(tǒng)與浮點(diǎn)數(shù)

          當(dāng)用戶分別先向賬戶中充值 0.1 單位和 0.2 單位的資產(chǎn)后,使用雙精度浮點(diǎn)數(shù)在計(jì)算時(shí)會(huì)得到 0.30000000000000004,用戶將這些資產(chǎn)全部提現(xiàn)可以得到 0.00000000000000004 的意外之財(cái)[^1],如果用戶重復(fù)的次數(shù)足夠多,就可以把銀行提破產(chǎn),大家加油,下面是一段使用浮點(diǎn)數(shù)處理充值和提現(xiàn)的代碼:

          var balance float64 = 0
          func main() { deposit(.1) deposit(.2) if balance, ok := withdraw(0.30000000000000004); ok { fmt.Println(balance) }}
          func deposit(v float64) { balance += v}
          func withdraw(v float64) (float64, bool) { if v <= balance { balance -= v return v, true } return 0, false}

          上面的代碼也只是理想的情況,今天的成熟金融系統(tǒng)不可能~~(其實(shí)不一定)~~犯這種低級(jí)的錯(cuò)誤,但是一些新興的交易所中仍然存在這種可能,不過想要真正實(shí)施上述操作還是非常困難。如果我們可以控制的資源是無限的,自然就可以實(shí)現(xiàn)無限精度的小數(shù),然而資源永遠(yuǎn)都是有限的,一些編程語言或者庫會(huì)通過下面的兩種方法提供精度更高的小數(shù)保證 0.1 + 0.2 = 0.3 這個(gè)等式的成立:

          • 使用具有 128 位的高精度定點(diǎn)數(shù)或者無限精度的定點(diǎn)數(shù);
          • 使用有理數(shù)類型和分?jǐn)?shù)系統(tǒng)保證計(jì)算的精度;

          上述這兩種方法都可以實(shí)現(xiàn)精度更高的小數(shù)系統(tǒng),但是兩者的原理卻略有不同,接下來我們將分析它們的設(shè)計(jì)原理。

          十進(jìn)制小數(shù)

          在很多時(shí)候浮點(diǎn)數(shù)的精度損失都是因?yàn)椴煌M(jìn)制的數(shù)據(jù)相關(guān)轉(zhuǎn)換造成的,正如我們?cè)?為什么 0.1 + 0.2 = 0.300000004 一文中提到的,我們無法使用有限的二進(jìn)制位數(shù)準(zhǔn)確地表示十進(jìn)制中的 0.10.2,這就造成了精度的損失,這些精度損失不斷累加在最后就可能累積成較大的錯(cuò)誤:

          a81c987b1ffe1df5d79e1b3e487c009d.webp圖 3 - 二進(jìn)制與十進(jìn)制精度的損失

          如下圖所示,因?yàn)?0.250.5 兩個(gè)十進(jìn)制的小數(shù)都可以用二進(jìn)制的浮點(diǎn)數(shù)準(zhǔn)確表示,所以使用浮點(diǎn)數(shù)計(jì)算 0.25 + 0.5 的結(jié)果也一定是準(zhǔn)確的[^2]:

          d15f3476c1be27fadd4cec824823ff5b.webp圖 4 - 0.25 和 0.5 的浮點(diǎn)數(shù)表示

          為了解決浮點(diǎn)數(shù)的精度問題,一些編程語言引入了十進(jìn)制的小數(shù) Decimal。Decimal 在不同社區(qū)中都十分常見,如果編程語言沒有原生支持 Decimal,我們?cè)陂_源社區(qū)也一定能夠找到使用特定語言實(shí)現(xiàn)的 Decimal 庫。Java 通過 BigDecimal 提供了無限精度的小數(shù),該類中包含三個(gè)關(guān)鍵的成員變量 intVal、scaleprecision[^3]:

          public class BigDecimal extends Number implements Comparable {    private BigInteger intVal;    private int scale;    private int precision = 0;    ...}

          當(dāng)我們使用 BigDecimal 表示 1234.56 時(shí),BigDecimal 中的三個(gè)字段會(huì)分別以下的內(nèi)容:

          • intVal 中存儲(chǔ)的是去掉小數(shù)點(diǎn)后的全部數(shù)字,即 123456
          • scale 中存儲(chǔ)的是小數(shù)的位數(shù),即 2;
          • prevision 中存儲(chǔ)的是全部的有效位數(shù),小數(shù)點(diǎn)前 4 位,小數(shù)點(diǎn)后 2 位,即 6;
          c49202c7223957c8d377f925a2dfdaf9.webp圖 5 - BigDecimal 實(shí)現(xiàn)

          BigDecimal 這種使用多個(gè)整數(shù)的方法避開了二進(jìn)制無法準(zhǔn)確表示部分十進(jìn)制小數(shù)的問題,因?yàn)?BigInteger 可以使用數(shù)組表示任意長(zhǎng)度的整數(shù),所以如果機(jī)器的內(nèi)存資源是無限的,BigDecimal 在理論上也可以表示無限精度的小數(shù)。

          雖然部分編程語言實(shí)現(xiàn)了理論上無限精度的 BigDecimal,但是在實(shí)際應(yīng)用中我們大多不需要無限的精度保證,C# 等編程語言通過 16 字節(jié)的 Decimal 提供的 28 ~ 29 位的精度,而在金融系統(tǒng)中使用 16 字節(jié)的 Decimal 一般就可以保證數(shù)據(jù)計(jì)算的準(zhǔn)確性了[^4]。

          有理數(shù)

          使用 DecimalBigDecimal 雖然可以在很大程度上解決浮點(diǎn)數(shù)的精度問題,但是它們?cè)谟龅綗o限小數(shù)時(shí)仍然無能為力,使用十進(jìn)制的小數(shù)永遠(yuǎn)無法準(zhǔn)確地表示 1/3,無論使用多少位小數(shù)都無法避免精度的損失:

          d1c94ffa413385b97ff3e2d10013a4f6.webp圖 6 - 無限小數(shù)的精度問題

          當(dāng)我們遇到這種情況時(shí),使用有理數(shù)(Rational)是解決類似問題的最好方法,部分編程語言因?yàn)榭茖W(xué)計(jì)算的需求會(huì)將有理數(shù)作為標(biāo)準(zhǔn)庫的一部分,例如:Julia[^5] 和 Haskell[^6]。分?jǐn)?shù)是有理數(shù)的重要組成部分,使用分?jǐn)?shù)可以準(zhǔn)確的表示 1/10、1/51/3,Julia 作為科學(xué)計(jì)算中的常用編程語言,我們可以使用如下所示的方式表示分?jǐn)?shù):

          julia> 1//31//3
          julia> numerator(1//3)1
          julia> denominator(1//3)3

          這種解決精度問題的方法更接近原始的數(shù)學(xué)公式,分?jǐn)?shù)的分子和分母是有理數(shù)結(jié)構(gòu)體中的兩個(gè)變量,多個(gè)分?jǐn)?shù)的加減乘除操作與數(shù)學(xué)中對(duì)分?jǐn)?shù)的計(jì)算沒有任何區(qū)別,自然也就不會(huì)造成精度的損失,我們可以簡(jiǎn)單了解一下 Java 中有理數(shù)的實(shí)現(xiàn)[^7]:

          public class Rational implements Comparable {    private int num;   // the numerator    private int den;   // the denominator    public double toDouble() {        return (double) num / den;    }    ...}

          上述類中的 numden 分別表示分?jǐn)?shù)的分子和分母,它提供的 toDouble 方法可以將當(dāng)前有理數(shù)轉(zhuǎn)換成浮點(diǎn)數(shù),因?yàn)楦↑c(diǎn)數(shù)在軟件工程中雖然更加常用,當(dāng)我們需要嚴(yán)密的科學(xué)計(jì)算時(shí),可以使用有理數(shù)完成絕大多數(shù)的計(jì)算,并在最后轉(zhuǎn)換回浮點(diǎn)數(shù)以減少可能出現(xiàn)的誤差。

          然而需要注意的是,這種使用有理數(shù)計(jì)算的方式不僅在使用上相對(duì)比較麻煩,它在性能上也無法與浮點(diǎn)數(shù)進(jìn)行比較,一次常見的加減法就需要使用幾倍于浮點(diǎn)數(shù)操作的匯編指令,所以在非必要的場(chǎng)景中一定要盡量避免。

          總結(jié)

          想要保證 0.1 + 0.2 = 0.3 這個(gè)公式的成立并不是一件復(fù)雜的事情,作者相信除了文中介紹的這些方案之外,我們還會(huì)有其他的實(shí)現(xiàn)方式,但是文中介紹的方案是最為常見的兩種,我們?cè)賮砘仡櫼幌氯绾问?0.1 + 0.2 = 0.3 這個(gè)公式成立:

          • 使用十進(jìn)制的兩個(gè)整數(shù) — 整數(shù)值和指數(shù)表示有限精度或者無限精度的小數(shù),一些編程語言使用 128 位的 Decimal 表示具有 28 ~ 29 位精度的數(shù)字,而一些編程語言使用 BigDecimal 表示無限精度的數(shù)字;
          • 使用十進(jìn)制的兩個(gè)整數(shù) — 分子和分母表示準(zhǔn)確的分?jǐn)?shù),可以減少浮點(diǎn)數(shù)計(jì)算帶來的精度損失;

          有理數(shù)和小數(shù)是數(shù)學(xué)中的概念,數(shù)學(xué)是一門非常嚴(yán)謹(jǐn)和精確的學(xué)科,通過引入大量的概念和符號(hào),數(shù)學(xué)中的計(jì)算可以實(shí)現(xiàn)絕對(duì)的準(zhǔn)確;但是軟件工程作為一門工程,它需要在復(fù)雜的物理世界,利用有限的資源解決有限的問題,所以我們需要在多個(gè)方案之間做出權(quán)衡和選擇,數(shù)學(xué)中的有理數(shù)和無理數(shù)其實(shí)都可以在軟件中實(shí)現(xiàn),但是在使用時(shí)一定要想清楚 — 為了得到這些我們犧牲了什么?到最后,我們還是來看一些比較開放的相關(guān)問題,有興趣的讀者可以仔細(xì)思考一下下面的問題:

          • 你最常用的編程語言中小數(shù)的結(jié)構(gòu)體是什么樣的,包含了哪些字段?
          • 浮點(diǎn)數(shù)、小數(shù)和有理數(shù)三種不同的策略在加減乘除四則運(yùn)算上的性能如何?

          瀏覽 59
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  后入无码日韩 | 久久精品欧美 | 色吧网色五月 | 国产女人高潮毛片 | 老鸭窝毛片美国黑人毛片 |