iOS浮點數精度問題

前言

浮點數是無法精確表示大部分實數的

單精度浮點數和雙精度浮點數

int type_size_float = sizeof(float) << 3;
printf("float 字節數:%ld 位數%d",sizeof(float),type_size_float);
printf("\n");

int type_size_double = sizeof(double) << 3;
printf("double 字節數:%ld 位數%d",sizeof(double),type_size_double);

輸出值:

float 字節數:4 位數32
double 字節數:8 位數64

單精度(float),一般在計算機中存儲占用4字節,也32位,有效位數為7位。雙精度(double)在計算機中存儲占用8字節,64位,有效位數為16位。

浮點數在計算機上的存儲遵循IEEE規范,使用二進制科學計數法,包含三個部分:符號位,指數位和尾數部分:

(1)符號位(Sign):0代表正數,1代表負數

(2)指數位(Exponent):用于存儲科學計數法中的指數部分,并且采用移位存儲(單精度:127+指數  雙精度:1023+指數)的二進制方式。

(3)尾數位(Mantissa):用于存儲尾數部分(單精度23(bit),雙精度52(bit))

float的符號位,指數位,尾數分別為1, 8, 23。 如圖:

image

double的符號位,指數位,尾數分別為1, 11, 52。如圖:

image

IEEE754標準中,一個規格化浮點數x的真值表示為:

x=(?1)^s(1.M)2^e**
單精度:e=E?127 M=23位數字 雙精度:e=E-1023 M=52位數字

精度主要取決于尾數部分的位數,float為23位,除去全部為0的情況外,最小為2的-23次方,約等于1.19乘以10的-7次方,所以float小數部分只能精確到后面6位,加上小數點前的一位,即有效數字為7位。
類似,double 尾數部分52位,最小為2的-52次方,約為2.22乘以10的-16次方,所以精確到小數點后15位,有效位數為16位。

例子解析

整數部分:除以2,取出余數,商繼續除以2,直到得到0為止,將取出的余數逆序

小數部分:乘以2,然后取出整數部分,將剩下的小數部分繼續乘以2,然后再取整數部分,一直取到小數部分為零為止。如果永遠不為零,則按要求保留足夠位數的小數,最后一位做0舍1入。將取出的整數順序排列

舉例:22.8125 轉二進制的計算過程:

整數部分:除以2,商繼續除以2,得到0為止,將余數逆序排列。
22 / 2     11 余 0
11/2       5  余 1
5  /2      2  余 1
2  /2      1  余 0
1  /2      0  余 1
得到22的二進制是10110


小數部分:乘以2,取整,小數部分繼續乘以2,取整,得到小數部分0為止,將整數順序排列。
0.8125x2=1.625 取整1,小數部分是0.625
0.625x2=1.25   取整1,小數部分是0.25
0.25x2=0.5     取整0,小數部分是0.5
0.5x2=1.0      取整1,小數部分是0,

得到0.8125的二進制是0.1101

結果:十進制22.8125等于二進制10110.1101

以上的數字22.8125在十進制中用科學計數法可用表示未2.28125*10^1表示。而二進制10110.1101也可以用類似的科學計數法表示1.01101101*2^4,同一個浮點數的表示不是唯一的(10.11011012^3101.1011012^2**),所以IEEE754的規范就規定了(?1)^s*(1.M)*2^e來表示一個浮點數。

我們發現浮點數的二進制表示中第一位永遠是1,比如0.28125的二進制表示為0.01001,前面的0都可以舍棄,取第一個1的位置的科學計數法為1.001*2^-2=(1*2^0+0*2^-1+0*2^-2+1*2^-3)*2^-2=0.28125

在內存中存儲的就是:

S(符號位):0
E(指數位):11111010=125
M(尾數位): 00100000000000000000000

所以根據公式x=(?1)^s*(1.M)*2^e計算,e=E-127=125-127=-2:

x=(-1)^0 * (1.00100000000000000000000) * 2^-2=0.28125

由于第一位永遠是1,所以在存儲時實際上并不保存這一位,這使得float的23bit的尾數可以表示24bit的精度,double中52bit的尾數可以表達53bit的精度。

精度缺失問題

float a=0.1;
printf("%.10f\n",a);
float a2=0.2;
printf("%.10f\n",a2);
float a3=0.3;
printf("%.10f\n",a3);
float a4=0.4;
printf("%.10f\n",a4);
float a5=0.5;
printf("%.10f\n",a5);
float a6=0.6;
printf("%.10f\n",a6);
float a7=0.7;
printf("%.10f\n",a7);
float a8=0.8;
printf("%.10f\n",a8);
float a9=0.9;
printf("%.10f\n",a9);

輸出:
0.1000000015
0.2000000030
0.3000000119
0.4000000060
0.5000000000
0.6000000238
0.6999999881
0.8000000119
0.8999999762
float a=0.1;
printf("%.7f\n",a);
float a2=0.2;
printf("%.7f\n",a2);
float a3=0.3;
printf("%.7f\n",a3);
float a4=0.4;
printf("%.7f\n",a4);
float a5=0.5;
printf("%.7f\n",a5);
float a6=0.6;
printf("%.7f\n",a6);
float a7=0.7;
printf("%.7f\n",a7);
float a8=0.8;
printf("%.7f\n",a8);
float a9=0.9;
printf("%.7f\n",a9);

輸出:
0.1000000
0.2000000
0.3000000
0.4000000
0.5000000
0.6000000
0.7000000
0.8000000
0.9000000

理解了IEEE規范的浮點數存儲之后,我們就能基本了解為什么單精度和雙精度浮點數所能表示的有效位數。

我們用上面的代碼來輸出0.1~0.9這多個數,輸出的時候精確到小數點后10位。我輸入的時候為小數點后一位,按道理說你存儲的時候應該沒問題吧。最多輸出的時候后面的位數都為0來表示。

但是我們發現以上數字只有0.5是能精確表示的,其他都無法精確表示,其實我們手動去轉一下其他數字為二進制,其實都是無法精確用二進制來表示的,最后都會變成00110011這樣不停的循環。所以其實在存儲大部分浮點數的時候本來就是無法精確存儲的,不管后面的位數是多少位。

這也就能理解為什么我們把上面的輸出位數printf("%.10f\n",a);改為printf("%.7f\n",a);時表示的都是準確的了,只是系統在打印的時候把后面的位數舍入掉了。如0.1000000015舍掉0150.6999999881入位881變成了0.7000000

結論就是再復述一遍前言中所說:浮點數是無法精確表示大部分實數的。所以根本就不是精度缺失,是根本沒辦法保存精度。

問題

double轉NSNumber時精度缺失

使用一下方法輸出NSNumberNSDecimalNumber的值和對應的stringValue的值,發現NSNumber會有很多值都是會損失精度的,NSDecimalNumber會好一點,但是也有一些,比如0.07 0.56 0.57。只有[decNumber stringValue]是準確的。

for (double i = 0.01; i<100; i+=0.01) {
        NSNumber *number = [NSNumber numberWithDouble:i];
        if ([[number stringValue] length]>5) {
            NSLog(@"NSNumber: %@",number);
            NSLog(@"NSNumber string:%@",[number stringValue]);
            
            //0.07  0.56  0.57
            NSString *doubleString = [NSString stringWithFormat:@"%lf", i];
            NSDecimalNumber *decNumber = [NSDecimalNumber decimalNumberWithString:doubleString];
            NSLog(@"NSDecimalNumber: %@",decNumber);
            NSLog(@"NSDecimalNumber string:%@",[decNumber stringValue]);
            NSLog(@"\n");
        }
    }
2018-07-07 16:10:55.727827+0800 NumberTest[38798:4238675] NSNumber: 0.07000000000000001
2018-07-07 16:10:55.728016+0800 NumberTest[38798:4238675] NSNumber string:0.07000000000000001
2018-07-07 16:10:55.728268+0800 NumberTest[38798:4238675] NSDecimalNumber: 0.06999999999999999
2018-07-07 16:10:55.728425+0800 NumberTest[38798:4238675] NSDecimalNumber string:0.07

思考

double amount1 = 4551.44;
double amount2 = 44551.44;
printf("%.2f\n%.2f\n",amount1,amount2);
printf("%.20f\n%.20f\n",amount1,amount2);
NSLog(@"\n%@\n%@",@(amount1),@(amount2));
NSLog(@"\n%@\n%@",@(amount1*100),@(amount2*100));
NSString *string = [NSString stringWithFormat:@"%.lf",amount1*100];
NSString *string2 = [NSString stringWithFormat:@"%.lf",amount2*100];
NSLog(@"\n%@\n%@",string,string2);

輸出:
#printf("%.2f\n%.2f\n",amount1,amount2);
4551.44
44551.44

#printf("%.20f\n%.20f\n",amount1,amount2);
4551.43999999999959982233
44551.44000000000232830644

#NSLog(@"\n%@\n%@",@(amount1),@(amount2));
4551.44
44551.44

#NSLog(@"\n%@\n%@",@(amount1*100),@(amount2*100));
455143.9999999999
4455144

#NSLog(@"\n%@\n%@",string,string2);
455144
4455144

根據之前的學習我們知道以上的兩個浮點數都沒辦法精確存儲,所以在輸出的時候會舍入,但是我這里為什么只有在NSLog(@"\n%@\n%@",@(amount1*100),@(amount2*100));這種情況下@(amount1*100)的值并沒有處理舍入呢?

參考

深入淺出iOS浮點數精度問題 (上)
iOS開發之NSDecimalNumber的使用,貨幣計算/精確數值計算/保留位數等

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容