iOS浮點數精度問題

齊滇大聖發表於2018-07-13

前言

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

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

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。 如圖:

iOS浮點數精度問題

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

iOS浮點數精度問題

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.1101101*2^3101.101101*2^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的使用,貨幣計算/精確數值計算/保留位數等

相關文章