iOS浮点数精度问题

作者: 齐滇大圣 | 来源:发表于2018-07-15 21:14 被阅读149次

    前言

    浮点数是无法精确表示大部分实数的

    单精度浮点数和双精度浮点数

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

    相关文章

      网友评论

        本文标题:iOS浮点数精度问题

        本文链接:https://www.haomeiwen.com/subject/uncmpftx.html