狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

C#.Net筑基-深入小數內部存儲的秘密

freeflydom
2024年6月26日 11:32 本文熱度 1151

 

為什么0.1 + 0.2 不等于 0.3?為什么16777216f 等于 16777217f?為什么金錢計算都推薦用decimal?本文主要學習了解一下數字背后不為人知的存儲秘密。


01、數值類型

C#中的數字類型主要包含兩類,整數、小數,C#中的小數都為浮點(小)數。


void Main()

{

int a1 = 100;

int a2 = 0x0f; //15

var b2 = 0b11; //3

var x1 = 1;    //整數值默認為int

var y1 = 1.1;  //小數值默認為double

Add(1, 2.3); //3.3

Add(1, 3);   //4

}

private T Add<T>(T x, T y) where T : INumber<T>

{

return x + y * x;

}
  • var類型推斷時,整數值默認為int,小數值默認為double

  • .NET 7 新增的一個專門用來約束數字類型的接口 INumber<T> ,用來約束數字類型非常好用。

數值類型大多提供的成員:

🔸靜態字段說明
MaxValue最大值常量,Console.WriteLine(int.MaxValue); //2147483647
MinValue最小值常量
🔸靜態方法說明
Parse、TryParse轉換為數值類型,是比較常用的類型轉換函數,參數NumberStyles可定義解析的數字格式
Max、Min比較值的大小,返回最大、小的值,int.Max(1,100) //100
Abs計算絕對值
IsInfinity是否有效值,無窮值
IsInteger是否整數
IsNaN是否為NaN
IsPositive是否零或正實數
IsNegative是否表示負實數

數值類型還有很多接口,如加、減、乘、除的操作符接口,作為泛型約束條件使用還是挺不錯的。

🔸操作符接口說明
IAdditionOperators加法
ISubtractionOperators減法
IMultiplyOperators乘法
IDivisionOperators除法

public static T Power<T>(T v1, T v2) where T : INumber<T>,

IMultiplyOperators<T, T, T>, IAdditionOperators<T, T, T>

{

return v1 * v1 + v2 * v2;

}

02、小數、浮點數⁉

C#中的小數類型有float、double、decimal 都是浮點數,浮點 就是“ 浮動小數點位置”,小數位數不固定,小數部分、整數部分是共享數據存儲空間的。相應的,自然也有定點小數,固定小數位數,在很多數據庫中有定點小數,C#中并沒有。

在編碼中我們常用的浮點小數是float、double,經常會遇到精度問題,以及類似下面這些面試題。

  • ❓ 為什么0.1 + 0.2 不等于 0.3

  • ❓ 為什么浮點數無法準確的表示 0.1

  • ❓ 為什么16777216f 等于 16777217f?這里f表示為float

  • ❓ 為什么32float可以最大表示3.402823E3864double可以最大表示1.79*E308,那么點位數根本存不下啊?

  • ❓ 同樣是32位,float的數據范圍遠超int,為什么?


Console.WriteLine(0.1 + 0.2 == 0.3);       //False

Console.WriteLine(16777216f == 16777217f); //True

Console.WriteLine(double.MaxValue); //1.7976931348623157E+308

Console.WriteLine(int.MaxValue);    //2147483647

Console.WriteLine(sizeof(double));  //8 //8字節(64位)

float、double為浮點數,小數位數有限,比較容易損失精度。造成上面這些問題的根本原因是其存儲機制決定的,他們都遵循IEEE754格式規范,幾乎所有編程語言和處理器都支持該規范,因此大多數編程語言都有類似的問題。Decimal 為高精度浮點數,存儲機制與float、double不同,她采用十進制方式表示。

❗ 要搞懂float、double,就不得不了解IEEE754規范!

2.1、IEEE754:float、double存儲原理

IEEE 754 (維基百科)是一個關于浮點數算術的國際標準,它定義了浮點數的表示格式、舍入規則、特殊值、浮點運算等規范。IEEE 754 標準最早發布與1985年,其中包括了四種精度規范,其中最常用的就兩種:單精度(float,4字節32位)和雙精度(double,8字節64位)。大多數編程語言、硬件處理器都支持這兩種浮點數據類型,因此float、double的知識幾乎是所有語言通用的,可以深入了解一下,不虧的!

IEEE 754 浮點數不像十進制字面量值那樣存儲,而是用下面的二進制方式來表示并存儲的,其實就是二進制的科學計數法。其二進制表示包含三個部分:符號位S指數部分(階碼E,2為底的指數)和尾數部分M

  • 🔸符號位(Sign):占用1位,這是浮點數的最高位,用于表示數字的正負。0表示正數,1表示負數。

  • 🔸指數部分(Exponent,階碼):表示為2位底的指數,這里使用了移碼,實際的指數e = E-127,這樣省去了指數的符號位,計算也更方便。

    • float 的指數部分8位,2^8=256 偏移量(移碼)為127,表示十進制范圍為 [-127,128],其數據范圍就為 ±2^128 = ±3.4E38。指數全是1即指數值為255時,表示為無效數字 ±infinity或NaN。

    • double 的指數部分11位,2^11=2048 偏移量(移碼)為1023,十進制值范圍[-1023,1024],因此數據范圍 ±2^1024 = ±1.79E308

  • 🔸尾數部分(Mantissa):這部分表示數字的精確值(有效數字),包括整數和小數部分。尾數長度決定了精度,因為有效數字長度是有限的,因此就必然存在精度丟失的問題。

    • float 的尾數部分23位,十進制 2^23=8388608,最多6~7(不完整的第7位)位有效十進制數字,只有前6位是完整的。

    • double 尾數長度52位,2^52 = 4503599627370496,因此最多有15~16 位有效十進制數字。

IEEE754浮點數都會被轉換為上述二進制形式:**符號*尾數*2^指數**,如 2 = 1.0 * 2^10.5 = 1.0 * 2^-15 = 1.25* 2^2。數據(整數、小數部分)先轉換為二進制形式,然后左移或右移小數點,轉換為1.M形式,始終都是 “1”開頭,因此就只存儲小數部分即可。

🚩浮點數 =  


十進制 2 就表示為 2 = 1.0* 2^1。下圖來自 在線IEEE754轉換器計算:IEEE-754 Floating Point Converter

  • 階碼 E = 127+1 = 128(實際指數e=1) 。

  • 尾數 1.0,實際存儲的尾數就是0

十進制 0.75 表示為0.75 = 1.5* 2^-1,指數為-1,尾數為1.5

  • 階碼 E = 127+ (-1) = 126(實際指數e=-1) 。

  • 尾數 1.5,實際存儲的尾數就是0.5,二進制值為0.1。為什么0.5 的二進制為0.1呢,請看后續章節。

2.2、float、double對比

類型單精度 float雙精度 double
CTS類型System.SingleSystem.Double
長度4字節32位8字節64位
符號位S11
階碼(指數位T)8,[-127,128]11,[-1023,1024]
尾數M2352
階碼偏移量127,e= E -1271023,e= E -1023
精度(10進制)**6~7 **,2^23=838860815~162^52 = 4503599627370496
范圍±3.402823E38 ,2^128=3.4E38±1.79*E308,2^1024=1.79E308
字面量表示(后綴)f/Fd/D

float只能用于 表示6~7個有效數字時,才不會損失精度。


//7位有效數字

Console.WriteLine(4234567f);  //4234567

//第8位就不準確了

Console.WriteLine(42345678f); //42345680

Console.WriteLine(42345671f); //42345670



//7位有效數字

Console.WriteLine(0.2345678f);  //0.2345678

//第8位就不準確了

Console.WriteLine(2.12345678f); //2.1234567

Console.WriteLine(0.212345678f); //0.21234567

2.3、小數是怎么轉換為二進制的?

對于整數轉換小數是非常容易理解的,計算機的二進制是天然支持整數存儲為二進制的。十進制整數轉成二進制通常采用 ”除 2 取余,逆序排列” 即可。


Console.WriteLine($"{1:B4}"); //0001

Console.WriteLine($"{2:B4}"); //0010

Console.WriteLine($"{3:B4}"); //0011

Console.WriteLine($"{4:B4}"); //0100

Console.WriteLine($"{5:B4}"); //0101

Console.WriteLine($"{8:B4}"); //1000

📢“B”格式只支持整數,更多格式化參考《String字符串全面了解>字符串格式化大全

🚩乘2取整法

但小數則不同,采用的是 “乘2取整法”,小數部分循環迭代,直到小數部分=0為止。:如下0.875的十進制浮點數轉換為二進制格式為:0.111

0.111,存儲為IEE754浮點數,轉換為1.M*2^E結構,小數點右移一位,就是1.11*2^-1

  • 指數E = -1 + 127 = 126 ,二進制值為01111110

  • 尾數為 11 后面補0。

十進制小數6.36 轉換為二進制,整數部分+小數部分分別轉換后合體:

🚩無限循環的0.1!

二進制無法準確表示小數0.1,是因為0.1 轉換為二進制后是無限循環的,0.0 0011 0011 0011...,“0011”無限循環。就像十進制小數1/3 = 0.333 一樣。

轉換為1.M*2^E結構,小數點右移4位,尾數就是1.1001 1001,指數 E = -4 +127 = 123

2.4、浮點數的精度是怎么回事?

計算機存儲整數很簡單,每個數字是確定的。但小數則不同,0到1之間的小數都無限種可能,計算機有限的空間無法存儲無限的小數。因此計算機將小數也當成“離散”的值,就像整數那樣,整數之間間隔始終為1。給小數一個間隔刻度,如下圖,用鐘表來舉例,小數刻度(步進)為0.234(十進制)。

這樣做的好處可以兼顧“所有”小數,小數的精度就取決于鐘表的“刻度”,刻度越小,精度越高,當然存儲時所需要的空間也就越大。

因此,這個精度本質上是由表盤間隔刻度(Gap)決定的,即使0.0012的間隔刻度,精度達到了4位十進制數,也只能保障前2~3位小數是可靠的。0.001X、0.002X、0.003X,他始終無法表示0.0013、0.0025。

可通過提高刻度(Gap)來提高精度,但存儲長度是有限的,因此不管是那種浮點數都是有精度限制的。精度越高的數據類型,也需要更多的長度來存儲數據。

32位float 用了23位來存儲有效數字,十進制也就6~7位(2^23=8388608)。在IEEE754規范中,小數的“刻度”并不是均勻分布的,而是越來越大,數值越大則精度越低。如下面的表盤和刻度尺的示意圖,其精度(Gap)的分布是不均勻的,0附近數字的精度最高,然后精度就越來越低了,低到超過1。

看看 float 的間隔刻度(Gap)如下圖,來自官方IEEE_754文檔

  • 當數值大于8388608時,刻度(Gap)為1,就不能包含小數了。

  • 當數字大于16777216(1600+萬)時間隔刻度為2,連整數精度都不能保證了😂。


//float大于8388608后的間隔為1

Console.WriteLine(8388608.1f == 8388608.4f); //True

//大于16777216后的間隔為2

Console.WriteLine(16777216f == 16777217f); //True

Console.WriteLine(16777218f == 16777219f); //False

Console.WriteLine(16777219f == 16777220f); //True

下圖是double的刻度表:小于8的數字都能有16位精度。

😂 怎么感覺float很雞肋呢?限制太多了!所以編程中浮點數多大都用的 double 居多,float比較少。


03、更精確的 Decimal

System.Decimal 是16字節(128位)的高精度十進制浮點數,不同于float、double 的二進制存儲機制,Decimal 采用10進制存儲,表示-7.9E28 到 +7.9E28之間的十進制數。Decimal 最大限度地減少了因舍入而導致的錯誤,比較適用于對精度要求高場景,如財務計算。

📢 Decimal并不屬于IEEE754規范,也不是處理器支持的類型,計算性能要差一點點(約 double 的 10%)。


Console.WriteLine(1f / 3f * 3f); //1

Console.WriteLine(0.1 + 0.2 == 0.3); //False

//decimal更高精度

Console.WriteLine(1m / 3m * 3m); //0.9999999999999999999999999999

Console.WriteLine(0.1m + 0.2m == 0.3m); //True

Decimal可以準確的表示0.1,Decimal 128位的存儲結構如下圖(圖來源):

  • 96位存儲一個大整數,就是有效數字,Math.Pow(2,96) = 7.9E28,最多28位有效數字,因此小數最多也就是28位(全是小數時)。

  • 剩下的32位中,有一個符號位,0 表示正數,1 表示負數。其中有5位(下圖中的第111位)表示10的指數部分(0到28的整數),可以理解為小數點的位置,其他位數沒有使用默認為0(有點浪費呢?)。

Decimal 表示小數其實是“障眼法”,內部有三個int (High、Mid、Low)來表示96位有效數字,還有一個int表示指數。可以通過 decimal.GetBits()方法獲取他們的值。下圖來自 Decimal 源碼 Decimal.cs

3.1、為什么Decimal沒有0.1問題?

在Decimal中就沒有 0.1+0.2 不等于0.3 的問題,因為她能準確表示0.1

其根本原因就是 Decimal 不會把小數轉換為二進制,而是就用十進制。把小數都轉為整數存儲,如 0.1在Decimal 中會被表示為 1* 10^-1,尾數為1,指數為-1指數就是小數點位置

📢 Decimal值 = 

 

var arr = decimal.GetBits(0.1M);

Console.WriteLine($"尾數:{arr[2]}{arr[1]}{arr[0]}");

Console.WriteLine($"指數:"+$"{arr[3]:B32}".Substring(0,16));

//尾數:001

//指數:0000000000000001

100.1024 存儲為1001024* 10^-4

  • 尾數為1001024,全都轉換為整數了。不用擔心超出整數int范圍,96位有三個整數并行存儲呢!

  • 指數為4,小數點位置在第四格。


var arr = decimal.GetBits(100.1024M);

Console.WriteLine($"尾數:{arr[2]}{arr[1]}{arr[0]}");

Console.WriteLine($"指數:"+$"{arr[3]:B32}".Substring(0,16));

//尾數:001001024

//指數:0000000000000100

如果是負數-100.1024,則只有符號位為1,其他一樣


var arr = decimal.GetBits(-100.1024M);

Console.WriteLine($"尾數:{arr[2]}{arr[1]}{arr[0]}");

Console.WriteLine($"指數:"+$"{arr[3]:B32}".Substring(0,16));

//尾數:001001024

//指數:1000000000000100

📢 所以 Decimal 值只要沒有超過28~29位有效數字,就沒有精度損失!是不是Very Nice!flaot、double 損失精度的根本原因是其存儲機制,必須把小數轉換為二進制值,再加上有限的精度位數。

3.2、Decimal、Double、Float對比

類型單精度 float雙精度 doubleDecimal 高精度浮點數
類型System.SingleSystem.DoubleSystem.Decimal
規范IEEE754IEEE754無,.Net自定義類型
是否基元類型
長度32位(4字節)64位(8字節)128位(16字節)
內部表示二進制,基數為2二進制,基數為2十進制,基數為10
字面量(后綴)f/F后綴d/D后綴m/M
最大精度6~715~1628~29位
范圍±3.4E38 ,2^23=3.4E38范圍很大,±1.7*E308-2^(96) 到 2^(96),±7.9E28
特殊值+0、-0、+∞、-∞、NaN+0、-0、+∞、-∞、NaN
速度處理器原生支持,速度很快處理器原生支持,速度很快非原生支持,約double10%

Decimal 雖然精度高,但長度也大,計算速度較慢,所以還是根據實際場景選擇。財務計算一般都用 Decimal 是因為他對精度要求較高,錢不能算錯,傳說算錯了要從程序員工資里扣😂😂。


04、一些編程實踐

  • 對于精度要求高的場景不適合用浮點數(double、float),推薦decimal,特別是價格、財務計算。

  • 浮點數不適合直接相等比較,直接相等大多會出Bug。

  • 在存儲比較大的數字時,需注意float、double 對于整數也有精度問題。

4.1、浮點數的相等比較

  • 使用相同的精度進行比較,Math.Round()獲取相同的精度值。

  • 比較相似性,根據實際場景設定一個誤差值,如1e-8,只要差值在這個誤差范圍內,都認為相等。


var f1 = 0.1 + 0.2;

var f2 = 0.3;



Console.WriteLine(f1 == f2); //False

//相同精度

Console.WriteLine(Math.Round(f1,6) == Math.Round(f2,6)); //True

//誤差范圍

Console.WriteLine(Math.Abs(f1-f2)<1e-8); //True

4.2、取整與四舍五入

取整方式說明/示例
整數相除 10/4=2拋棄余數,只留整數部分
強制轉換(int)2.9=2直接截斷,只留整數部分,需要注意‼️
Convert轉換,四舍五入取整Convert.ToInt32(2.7) = 3; Convert.ToInt32(2.2) = 2;
格式化截斷,四射五入字符串格式化時的截斷,都是四舍五入, $"{2.7:F0}" = "3"
Math.Ceiling(),向上取整Math.Ceiling(2.3) = 3,⁉️注意負數Math.Ceiling(-2.3) = -2
Math.Floor(),向下取整Math.Floor(2.3) = 2,⁉️注意負數Math.Floor(-2.3) = -3
Math.Truncate(),截斷取整Math.Truncate(2.7) = 2,只保留整數部分,同強制轉換
Math.Round(),四舍五入可指定四舍五入精度,Math.Round(2.77,1) = 2.8


轉自https://www.cnblogs.com/anding/p/18221160 作者安木夕


該文章在 2024/6/26 11:37:48 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved