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的使用,货币计算/精确数值计算/保留位数等

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,273评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,905评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,281评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,817评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,690评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,491评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,886评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,513评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,810评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,839评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,642评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,455评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,901评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,091评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,381评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,895评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,104评论 2 338

推荐阅读更多精彩内容