您好,登錄后才能下訂單哦!
本文將為大家詳細介紹Go語言浮點數的存儲方式,內容詳細步驟清晰,細節處理妥當,希望大家通過這篇文章有所收獲,我們先來看看浮點數如何在程序中被使用的:
下面的一段簡單程序 0.3 + 0.6 結果是什么?
1 var f1 float64 = 0.3
2 var f2 float64 = 0.6
3 fmt.Println(f1 + f2)
有人會天真的認為是0.9,但實際輸出卻是0.8999999999999999(go 1.13.5)
問題在于大多數小數表示成二進制之后是近似且無限的。
以0.1為例。它可能是你能想到的最簡單的十進制之一,但是二進制看起來卻非常復雜:0.0001100110011001100…
其是一串連續循環無限的數字(涉及到10進制轉換為2進制,暫不介紹)。
結果的荒誕性告訴我們,必須深入理解浮點數在計算機中的存儲方式及其性質,才能正確處理數字的計算。
golang 與其他很多語言(C、C++、Python…)一樣,使用了IEEE-754標準存儲浮點數。
IEEE-754規范使用特殊的以2為基數的科學表示法表示浮點數。
32位的單精度浮點數 與 64位的雙精度浮點數的差異
符號位:1 為 負數, 0 為正數。
指數位:存儲 指數加上偏移量,偏移量是為了表達負數而設計的。
小數位:存儲系數的小數位的準確或者最接近的值。
以 數字 0.085 為例。
以0.36 為例:
010 1110 0001 0100 0111 1011 = 0.36 (第一位數字代表1/2,第二位數字是1/4…,0.36 是所有位相加)
分解后的計算步驟為:
接下來用一個案例有助于我們理解并驗證IEEE-754 浮點數的表示方式。
math.Float32bits 可以為我們打印出32位數據的二進制表示。(注:math.Float64bits可以打印64位數據的二進制)
下面的go代碼將輸出0.085的浮點數二進制表達,并且為了驗證之前理論的正確性,根據二進制表示反向推導出其所表示的原始十進制0.085
輸出:表明我們對于浮點數的理解正確。
1 Starting Number: 0.085000
2 Bit Pattern: 0 | 0111 1011 | 010 1110 0001 0100 0111 1011
3 Sign: 0 Exponent: 123 (-4) Mantissa: 0.360000 Value: 0.085000
下面是一個有趣的問題,如何判斷一個浮點數其實存儲的是整數?
思考10秒鐘…
下面是一段判斷浮點數是否為整數的go代碼實現,我們接下來逐行分析函數。
它可以加深對于浮點數的理解
1、要保證是整數,一個重要的條件是必須要指數位大于127,如果指數位為127,代表指數為0. 指數位大于127,代表指數大于0, 反之小于0.
下面我們以數字234523為例子:
第一步,計算指數。由于 多減去了23,所以在第一個判斷中 判斷條件為 exponent < -23
exponent := int(bits >> 23) - bias - 23
第二步,
(bits & ((1 << 23) - 1)) 計算小數位。
| (1 << 23) 代表 將1加在前方。
1 + 小數 = 系數。
如下,指數是17位,其不能夠彌補最后6位的小數。即不能彌補1/2^18 的小數。
由于2^18位之后為0.所以是整數。
要理解decimal包,首先需要知道兩個重要的概念,Normal number、denormal (or subnormal) number 以及精度。
wiki的解釋是:
什么意思呢?在IEEE-754中指數位有一個偏移量,偏移量是為了表達負數而設計的。比如單精度中的0.085,實際的指數是 -3, 存儲到指數位是123。
所以表達的負數就是有上限的。這個上限就是2^-126。如果比這個負數還要小,例如2^-127,這個時候應該表達為0.1 * 2 ^ -126. 這時系數變為了不是1為前導的數,這個數就叫做denormal (or subnormal) number。
正常的系數是以1為前導的數就叫做Normal number。
精度是一個非常復雜的概念,在這里筆者討論的是2進制浮點數的10進制精度。
精度為d表示的是在一個范圍內,如果我們將d位10進制(按照科學計數法表達)轉換為二進制。再將二進制轉換為d位10進制。數據不損失意味著在此范圍內是有d精度的。
精度的原因在于,數據在進制之間相互轉換時,是不能夠精準匹配的,而是匹配到一個最近的數。如圖所示:
精度轉換
在這里暫時不深入探討,而是給出結論:(注:精度是動態變化的,不同的范圍可能有不同的精度。這是由于 2的冪 與 10的冪之間的交錯是不同的。)
float32的精度為6-8位,
float64的精度為15-17位
目前使用比較多的精準操作浮點數的decimal包是shopspring/decimal。鏈接:https://github.com/shopspring/decimal
decimal包使用math/big包存儲大整數并進行大整數的計算。
比如對于字符串 “123.45” 我們可以將其轉換為12345這個大整數,以及-2代表指數。參考decimal結構體:
在本文不會探討math/big是如何進行大整數運算的,而是探討decimal包一個非常重要的函數:
NewFromFloat(value float64) Decimal
其主要調用了下面的函數:
此函數會將浮點數轉換為Decimal結構。
讀者想象一下這個問題:如果存儲到浮點數中的值(例如0.1)本身就是一個近似值,為什么decimal包能夠解決計算的準確性?
原因在于,deciimal包可以精準的將一個浮點數轉換為10進制。這就是NewFromFloat為我們做的事情。
下面我將對此函數做逐行分析。
第5行:剝離出IEEE浮點數的指數位
exp := int(bits>>flt.mantbits) & (1<<flt.expbits - 1)
第6行:剝離出浮點數的系數的小數位
mant := bits & (uint64(1)<<flt.mantbits - 1)
第7行:如果是指數位為0,代表浮點數是denormal (or subnormal) number;
默認情況下會在mant之前加上1,因為mant只是系數的小數,在前面加上1后,代表真正的小數位。
現在 mant = IEEE浮點數系數 * 2^53
第13行:加上偏移量,exp現在代表真正的指數。
第14行:引入了一個中間結構decimal
第15行:調用d.Assign(mant) , 將mant作為10進制數,存起來。
10進制數的每一位都作為一個字符存儲到 decimal的byte數組中
第16行:調用shift函數,這個函數非常難理解。
此函數的功能是為了獲取此浮點數代表的10進制數據的整數位個數以及小數位個數,此函數的完整證明附后。(注1)
exp是真實的指數,其也是能夠覆蓋小數部分2進制位的個數。(參考前面如何判斷浮點數是整數)
exp - int(flt.mantbits)代表不能被exp覆蓋的2進制位的個數
如果exp - int(flt.mantbits) > 0 代表exp能夠完全覆蓋小數位 因此 浮點數是一個非常大的整數,這時會調用leftShift(a, uint(k))。否則將調用rightShift(a, uint(-k)), 常規rightShift會調用得更多。因此我們來看看rightShift函數的實現。
第5行:此for循環將計算浮點數10進制表示的小數部分的有效位為 r-1 。
n >> k 是一個重要的衡量指標,代表了小數部分與整數部分的分割。此函數的完整證明附后。(注1)
第21行:此時整數部分所占的有效位數為a.dp -=(r-1)
第24行:這兩個循環做了2件事情:
1、計算10進制表示的有效位數
2、將10進制表示存入bytes數組中。例如對于浮點數64.125,現在byte數組存儲的前5位就是64125
繼續回到newFromFloat函數,第18行,調用了roundShortest函數,
此函數非常關鍵。其會將浮點數轉換為離其最近的十進制數。
這是為什么decimal.NewFromFloat(0.1)能夠精準表達0.1的原因。
參考上面的精度,此函數主要考察了2的冪與10的冪之間的交錯關系。四舍五入到最接近的10進制值。
此函數實質實現的是Grisu3 算法,有想深入了解的可以去看看論文。筆者在這里提示幾點:
1、2^exp <= d < 10^dp。
2、10進制數之間至少相聚10^(dp-nd)
3、2的冪之間的最小間距至少為2^(exp-mantbits)
4、什么時候d就是最接近2進制的10進制數?
如果10^(dp-nd) > 2^(exp-mantbits),表明 當十進制下降一個最小位數時,匹配到的是更小的數字value - 2^(exp-mantbits),所以d就是最接近浮點數的10進制數。
繼續回到newFromFloat函數,第19行 如果精度小于19,是位于int64范圍內的,可以使用快速路徑,否則使用math/big包進行賦值操作,效率稍微要慢一些。
第36行,正常情況幾乎不會發生。如果setstring在異常的情況下會調用NewFromFloatWithExponent 指定精度進行四舍五入截斷。
以典型的數字64.125 為例 , 它可以被浮點數二進制精準表達為:
Bit Patterns: 0 | 10000000101 | 0000000010000000000000000000000000000000000000000000
Sign: 0 | Exponent: 1029 (6) | Mantissa: 0.001953
即 64.125 = 1.001953125 * 2^6
注意觀察浮點數的小數位在第九位有1, 代表2^-9 即 0.001953125.
我們在浮點數的小數位前 附上數字1,10000000010000000000000000000000000000000000000000000 代表其為1 / 2^0 .
此時我們可以認為這個數代表的是1.001953125. 那么這樣長的二進制數變為10進制又是多少呢:4512395720392704。
即 1.001953125 = 4512395720392704 * 2^(-52)
所以64.125 = 4512395720392704 * 2^(-52) * 2^6 = 4512395720392704 * 2^(-46)
在這里,有一種重要的等式。即 (2 ^ -46) 等價于向左移動了46位。并且移動后剩下的部分即為64,而舍棄的部分其實是小數部分0.125。
這個等式看似復雜其實很好證明,即第46位其實代表的是2^45。其除以2^46后是一個小數。依次類推…
因此對于數字 4512395720392704 , 我們可以用4,45,451,4512 … 依次除以 2 ^ 46. 一直到找到數451239572039270 其除以2^46不為0。這個不為0的數一定為6。
接著我們保留后46位,其實是保留了小數位。
假設 4512395720392704 / 2^46 = (6 + num)
64.125 =(6 + num) * 10 + C = 60 + 10* num + C
當我們將通過位運算保留后46位,設為A, 則 A / 2^46 = num
所以 (A * 10 + C) / 2 ^46 =(num * 10 +C) = 4.125
此我們又可以把4提取出來。
關于Go語言浮點數的存儲方式就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。