自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

為什么 0.1 + 0.2 = 0.30000000000000004?

開發(fā)
我有點(diǎn)懷疑是否有人能耐心完成以上所有些算術(shù),但它寫出來對我很有幫助,所以我還是發(fā)表了這篇文章,希望它能有所幫助。

嗨!昨天我試著寫點(diǎn)關(guān)于浮點(diǎn)數(shù)的東西,我發(fā)現(xiàn)自己對這個(gè) 64 位浮點(diǎn)數(shù)的計(jì)算方法很好奇:

>>> 0.1 + 0.2
0.30000000000000004

我意識(shí)到我并沒有完全理解它是如何計(jì)算的。我的意思是,我知道浮點(diǎn)計(jì)算是不精確的,你不能精確地用二進(jìn)制表示 0.1,但是:肯定有一個(gè)浮點(diǎn)數(shù)比 0.30000000000000004 更接近 0.3!那為什么答案是 0.30000000000000004 呢?

如果你不想閱讀一大堆計(jì)算過程,那么簡短的答案是: 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 正好位于兩個(gè)浮點(diǎn)數(shù)之間,即 0.299999999999999988897769753748434595763683319091796875 (通常打印為 0.3) 和 0.3000000000000000444089209850062616169452667236328125(通常打印為 0.30000000000000004)。答案是 0.30000000000000004,因?yàn)樗奈矓?shù)是偶數(shù)。

浮點(diǎn)加法是如何計(jì)算的

以下是浮點(diǎn)加法的簡要計(jì)算原理:

  • 把它們精確的數(shù)字加在一起
  • 將結(jié)果四舍五入到最接近的浮點(diǎn)數(shù)

讓我們用這些規(guī)則來計(jì)算 0.1 + 0.2。我昨天才剛了解浮點(diǎn)加法的計(jì)算原理,所以在這篇文章中我可能犯了一些錯(cuò)誤,但最終我得到了期望的答案。

第一步:0.1 和 0.2 到底是多少

首先,讓我們用 Python 計(jì)算 0.1 和 0.2 的 64 位浮點(diǎn)值。

>>> f"{0.1:.80f}"
'0.10000000000000000555111512312578270211815834045410156250000000000000000000000000'
>>> f"{0.2:.80f}"
'0.20000000000000001110223024625156540423631668090820312500000000000000000000000000'

這確實(shí)很精確:因?yàn)楦↑c(diǎn)數(shù)是二進(jìn)制的,你也可以使用十進(jìn)制來精確的表示。但有時(shí)你只是需要一大堆數(shù)字:)

第二步:相加

接下來,把它們加起來。我們可以將小數(shù)部分作為整數(shù)加起來得到確切的答案:

>>> 1000000000000000055511151231257827021181583404541015625 + 2000000000000000111022302462515654042363166809082031250
3000000000000000166533453693773481063544750213623046875

所以這兩個(gè)浮點(diǎn)數(shù)的和是 0.3000000000000000166533453693773481063544750213623046875

但這并不是最終答案,因?yàn)樗皇且粋€(gè) 64 位浮點(diǎn)數(shù)。

第三步:查找最接近的浮點(diǎn)數(shù)

現(xiàn)在,讓我們看看接近 0.3 的浮點(diǎn)數(shù)。下面是最接近 0.3 的浮點(diǎn)數(shù)(它通常寫為 0.3,盡管它不是確切值):

>>> f"{0.3:.80f}"
'0.29999999999999998889776975374843459576368331909179687500000000000000000000000000'

我們可以通過 struct.pack 將 0.3 序列化為 8 字節(jié)來計(jì)算出它之后的下一個(gè)浮點(diǎn)數(shù),加上 1,然后使用 struct.unpack

>>> struct.pack("!d", 0.3)
b'?\xd3333333'
# 手動(dòng)加 1
>>> next_float = struct.unpack("!d", b'?\xd3333334')[0]
>>> next_float
0.30000000000000004
>>> f"{next_float:.80f}"
'0.30000000000000004440892098500626161694526672363281250000000000000000000000000000'

當(dāng)然,你也可以用 math.nextafter

>>> math.nextafter(0.3, math.inf)
0.30000000000000004

所以 0.3 附近的兩個(gè) 64 位浮點(diǎn)數(shù)是 0.299999999999999988897769753748434595763683319091796875 和 0.3000000000000000444089209850062616169452667236328125。

第四步:找出哪一個(gè)最接近

結(jié)果證明 0.3000000000000000166533453693773481063544750213623046875 正好在 0.299999999999999988897769753748434595763683319091796875 和 0.3000000000000000444089209850062616169452667236328125 的中間。

你可以通過以下計(jì)算看到:

>>> (3000000000000000444089209850062616169452667236328125000 + 2999999999999999888977697537484345957636833190917968750) // 2 == 3000000000000000166533453693773481063544750213623046875
True

所以它們都不是最接近的。

如何知道四舍五入到哪一個(gè)?

在浮點(diǎn)數(shù)的二進(jìn)制表示中,有一個(gè)數(shù)字稱為“尾數(shù)”。這種情況下(結(jié)果正好在兩個(gè)連續(xù)的浮點(diǎn)數(shù)之間),它將四舍五入到偶數(shù)尾數(shù)的那個(gè)。

在本例中為 0.300000000000000044408920985006261616945266723632812500。

我們之前就見到了這個(gè)數(shù)字的尾數(shù):

  • 0.30000000000000004 是 struct.unpack('!d', b'?\xd3333334') 的結(jié)果
  • 0.3 是 struct.unpack('!d', b'?\xd3333333') 的結(jié)果

0.30000000000000004 的大端十六進(jìn)制表示的最后一位數(shù)字是 4,它的尾數(shù)是偶數(shù)(因?yàn)槲矓?shù)在末尾)。

我們用二進(jìn)制來算一下

之前我們都是使用十進(jìn)制來計(jì)算的,這樣讀起來更直觀。但是計(jì)算機(jī)并不會(huì)使用十進(jìn)制,而是用 2 進(jìn)制,所以我想知道它是如何計(jì)算的。

我不認(rèn)為本文的二進(jìn)制計(jì)算部分特別清晰,但它寫出來對我很有幫助。有很多數(shù)字,讀起來可能很糟糕。

64 位浮點(diǎn)數(shù)如何計(jì)算:指數(shù)和尾數(shù)

64 位浮點(diǎn)數(shù)由 2 部分整數(shù)構(gòu)成:指數(shù)尾數(shù),還有 1 比特 符號(hào)位.

以下是指數(shù)和尾數(shù)對應(yīng)于實(shí)際數(shù)字的方程:

例如,如果指數(shù)是 1,尾數(shù)是 2**51,符號(hào)位是正的,那么就可以得到:

它等于 2 * (1 + 0.5),即 3。

步驟 1:獲取 0.1 和 0.2 的指數(shù)和尾數(shù)

我用 Python 編寫了一些(to 校正:這里原文加了一個(gè) inefficient 形容詞,不知道如何翻譯)函數(shù)來獲取正浮點(diǎn)數(shù)的指數(shù)和尾數(shù):

def get_exponent(f):
    # 獲取前 52 個(gè)字節(jié)
    bytestring = struct.pack('!d', f)
    return int.from_bytes(bytestring, byteorder='big') >> 52
def get_significand(f):
    # 獲取后 52 個(gè)字節(jié)
    bytestring = struct.pack('!d', f)
    x = int.from_bytes(bytestring, byteorder='big')
    exponent = get_exponent(f)
    return x ^ (exponent << 52)

我忽略了符號(hào)位(第一位),因?yàn)槲覀冎恍枰幚?0.1 和 0.2,它們都是正數(shù)。

首先,讓我們獲取 0.1 的指數(shù)和尾數(shù)。我們需要減去 1023 來得到實(shí)際的指數(shù),因?yàn)楦↑c(diǎn)運(yùn)算就是這么計(jì)算的。

>>> get_exponent(0.1) - 1023
-4
>>> get_significand(0.1)
2702159776422298

它們根據(jù) 2**指數(shù) + 尾數(shù) / 2**(52 - 指數(shù)) 這個(gè)公式得到 0.1。

下面是 Python 中的計(jì)算:

>>> 2**-4 + 2702159776422298 / 2**(52 + 4)
0.1

(你可能會(huì)擔(dān)心這種計(jì)算的浮點(diǎn)精度問題,但在本例中,我很確定它沒問題。因?yàn)楦鶕?jù)定義,這些數(shù)字沒有精度問題 -- 從 2**-4 開始的浮點(diǎn)數(shù)以 1/2**(52 + 4) 步長遞增。)

0.2 也一樣:

>>> get_exponent(0.2) - 1023
-3
>>> get_significand(0.2)
2702159776422298

它們共同工作得到 0.2:

>>> 2**-3 + 2702159776422298 / 2**(52 + 3)
0.2

(順便說一下,0.1 和 0.2 具有相同的尾數(shù)并不是巧合 —— 因?yàn)?nbsp;x 和 2*x 總是有相同的尾數(shù)。)

步驟 2:重新計(jì)算 0.1 以獲得更大的指數(shù)

0.2 的指數(shù)比 0.1 大 -- -3 大于 -4。

所以我們需要重新計(jì)算:

2**-4 + 2702159776422298 / 2**(52 + 4)

等于 X / (2**52 + 3)

如果我們解出 2**-4 + 2702159776422298 / 2**(52 + 4) = X / (2**52 + 3),我們能得到:

X = 2**51 + 2702159776422298 /2

在 Python 中,我們很容易得到:

>>> 2**51 + 2702159776422298 //2
3602879701896397

步驟 3:添加符號(hào)位

現(xiàn)在我們試著做加法:

2**-3 + 2702159776422298 / 2**(52 + 3) + 3602879701896397 / 2**(52 + 3)

我們需要將 2702159776422298 和 3602879701896397 相加:

>>> 2702159776422298  + 3602879701896397
6305039478318695

棒。但是 6305039478318695 比 2**52-1(尾數(shù)的最大值)大,問題來了:

>>> 6305039478318695 > 2**52
True

步驟 4:增加指數(shù)

目前結(jié)果是:

2**-3 + 6305039478318695 / 2**(52 + 3)

首先,它減去 2**52:

2**-2 + 1801439850948199 / 2**(52 + 3)

完美,但最后的 2**(52 + 3) 需要改為 2**(52 + 2)。

我們需要將 1801439850948199 除以 2。這就是難題的地方 -- 1801439850948199 是一個(gè)奇數(shù)!

>>> 1801439850948199  / 2
900719925474099.5

它正好在兩個(gè)整數(shù)之間,所以我們四舍五入到最接近它的偶數(shù)(這是浮點(diǎn)運(yùn)算規(guī)范要求的),所以最終的浮點(diǎn)結(jié)果是:

>>> 2**-2 + 900719925474100 / 2**(52 + 2)
0.30000000000000004

它就是我們預(yù)期的結(jié)果:

>>> 0.1 + 0.2
0.30000000000000004

在硬件中它可能并不是這樣工作的

在硬件中做浮點(diǎn)數(shù)加法,以上操作方式可能并不完全一模一樣(例如,它并不是求解 “X”),我相信有很多有效的技巧,但我認(rèn)為思想是類似的。

打印浮點(diǎn)數(shù)是非常奇怪的

我們之前說過,浮點(diǎn)數(shù) 0.3 不等于 0.3。它實(shí)際上是:

>>> f"{0.3:.80f}"
'0.29999999999999998889776975374843459576368331909179687500000000000000000000000000'

但是當(dāng)你打印它時(shí),為什么會(huì)顯示 0.3?

計(jì)算機(jī)實(shí)際上并沒有打印出數(shù)字的精確值,而是打印出了最短的十進(jìn)制數(shù) d,其中 f 是最接近 d 的浮點(diǎn)數(shù)。

事實(shí)證明,有效做到這一點(diǎn)很不簡單,有很多關(guān)于它的學(xué)術(shù)論文,比如 快速且準(zhǔn)確地打印浮點(diǎn)數(shù)如何準(zhǔn)確打印浮點(diǎn)數(shù) 等。

如果計(jì)算機(jī)打印出浮點(diǎn)數(shù)的精確值,會(huì)不會(huì)更直觀一些?

四舍五入到一個(gè)干凈的十進(jìn)制值很好,但在某種程度上,我覺得如果計(jì)算機(jī)只打印一個(gè)浮點(diǎn)數(shù)的精確值可能會(huì)更直觀 -- 當(dāng)你得到一個(gè)奇怪的結(jié)果時(shí),它可能會(huì)讓你看起來不那么驚訝。

對我來說,0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125 比 0.1 + 0.2 = 0.30000000000000000004 驚訝少一點(diǎn)。

這也許是一個(gè)壞主意,因?yàn)樗隙〞?huì)占用大量的屏幕空間。

PHP 快速說明

有人在評論中指出在 PHP 中 <?php echo (0.1 + 0.2 );?> 會(huì)輸出 0.3,這是否說明在 PHP 中浮點(diǎn)運(yùn)算不一樣?

非也 —— 我在 這里 運(yùn)行:

<?php echo (0.1 + 0.2 )- 0.3);?>,得到了與 Python 完全相同的答案:5.5511151231258E-17。因此,浮點(diǎn)運(yùn)算的基本原理是一樣的。

我認(rèn)為在 PHP 中 0.1 + 0.2 輸出 0.3 的原因是 PHP 顯示浮點(diǎn)數(shù)的算法沒有 Python 精確 —— 即使這個(gè)數(shù)字不是最接近 0.3 的浮點(diǎn)數(shù),它也會(huì)顯示 0.3。

總結(jié)

我有點(diǎn)懷疑是否有人能耐心完成以上所有些算術(shù),但它寫出來對我很有幫助,所以我還是發(fā)表了這篇文章,希望它能有所幫助。

責(zé)任編輯:龐桂玉 來源: Linux中國
相關(guān)推薦

2020-10-12 13:27:21

計(jì)算機(jī)瀏覽器電腦

2019-10-21 11:20:12

編程小程序開發(fā)

2023-11-08 13:32:00

JavaScript浮點(diǎn)數(shù)計(jì)算

2024-08-23 08:43:08

2021-10-29 22:49:57

JavaScript開發(fā)精度

2022-07-14 14:27:34

Javascript數(shù)字精度二進(jìn)制

2024-04-03 09:23:31

ES索引分析器

2017-02-16 07:37:19

前端程序軟件

2020-04-07 13:40:13

GraphQLAPI編程語言

2021-01-20 08:36:15

工具AtomicRefer JDK

2020-05-29 09:49:43

騰訊

2011-02-16 09:42:04

DevOps

2012-09-26 10:02:44

框架開發(fā)項(xiàng)目

2011-11-28 10:21:52

Nginx特性

2019-05-24 13:47:45

出海以色列

2020-09-25 08:10:55

Rust系統(tǒng)編程

2018-04-10 13:40:14

Kubernetes容器服務(wù)器

2017-09-20 17:16:32

方太

2023-03-21 08:02:36

Redis6.0IO多線程

2023-06-06 09:03:06

InnodbMySQL
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)