前言
在iOS开发中,和价格计算相关的,需要注意计算精度的问题,使用float、double来计算价格数值会出现精度损失,使用官方的NSDecimalNumber是更好的选择。
本篇文章探讨下浮点数的精度损失,以及在iOS中高精度计算的处理
1.浮点数的表示
现在所有通用计算机都采用IEEE 754来表示浮点数, IEEE二进制浮点数算术标准是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。
IEEE 754规定,一个浮点数可以表示成如下形式**32位的单精度浮点数,最高1位是符号位s,接着的8位是指数E,剩下的23位是有效数字M
64位的双精度浮点数,最高1位是符号位s,接着的11位是指数E,剩下的52位为有效数字M
以float举例
float的存储正是将四字节的32位划分成了三部分,分别是:
Sign(1位):用来表示浮点数是正数还是负数,0表示正数,1表示负数。
Exponent(8位):阶码或者称为指数,用移码表示(将每一个数值加上一个偏置常数 bias,当编码位数为n时,IEEE 754标准的bias取 (2^n-1)-1,单精度为为127, 双精度为1023,把阶码的值调整到一个无符号数的范围内以便进行比较) ,全0或全1用来表示特殊值。其真实值,单精度要减去127,双精度减去1023
Mantissa(23位):尾数部分,1≤M<2,即M可以写成1.xxxxx的形式,其中xxxxx表示小数部分。IEEE 754规定小数点前总为1,所以在内部保存时隐含表示,省一位。
十进制向二进制的转换
- 十进制整数转换为二进制整数
十进制整数转换为二进制整数采用"除2取余,逆序排列"法。具体做法是:用2去除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为零时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。
把 173 转换为二进制数。
2.十进制小数转换为二进制小数
十进制小数转换成二进制小数采用"乘2取整,顺序排列"法。具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数 部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。
然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位。
把 0.8125 转换为二进制小数。
浮点数和真值转换的两个例子
一、已知float型变量x的机器数为BEE00000H,求x的值是多少?
1 01111101 11000000000000000000000 (末尾补零)
按照(-1)^s x (mantissa) x 2^(Exponent-127)
数符:1 负数
阶码:二进制的01111101转换到十进制为125 ,125-127=-2
尾数部分的值:1+2^(-1) +2^(-2) = 1+0.5 +0.25 = 1.75
所以真值为-1.75 * 2^(-2) = - 0.4375
二、已知float型变量x的值为-12.75,求x的机器数是多少?
-12.75 = -1100.11B
= -1.10011 * 2^3
所以符号位s为1,阶码E = 127+3 = 128+2 = 1000 0010 显式显示的部分尾数:100 1100 0000 0000 0000 0000
x的机器数表示:1 10000010 100 1100 0000 0000 0000 0000
舍入
因为表示方法限制了浮点数的精度和范围,所以浮点数只能近似的表示实数运算。对于值x,我们一般想用一种系统的方法找到“最接近的”匹配值,它可以用期望的浮点形式表示出来。这就是舍入运算的任务。IEEE浮点格式定义了四种不同的舍入方式,默认的方法是找到最接近的匹配,而其他三种可用于计算上界和下界。
四种舍入方式 :
向偶数舍入(默认)
向零舍入
向下舍入
向上舍入
浮点数的精度误差的原因
了解了浮点数的结构,总结一下精度误差原因
- float类型占32位,一共有2^32种组合, double类型占64位,一共有2^64种组合, 而实数是无穷的,当输入的数据是不可表示数时,会将其转化为最邻近的可表示数
2.有些十进制数,无法用有限的二进制浮点数表示。 比如0.1,转换为二进制 0.0 0011 0011 0011...无限循环,当你声明float m = 0.1时,其存储的数值精度已经发生了变化
2.iOS中高精度计算的处理
很多语言中都有相应的高精度计算方式,如Java中的BigDecimal类,C#中的decimal类型都是用来解决高精度计算问题的。Objective-C中提供的有一个NSDecimalNumber类,用来处理精确计算。
NSDecimalNumber是NSNumber的一个不可变子类,创建之后不能改变它们的值。 它提供了一个面向对象的包装器来做base-10算术。实例可以表示任何可以用mantissa x 10^exponent表示的数字,其中mantissa是一个长度不超过38位的十进制整数,exponent是一个从-128到127的整数。
其表示的值 value = sign mantissa*10^exponent
sign:符号位,定义了它是正数还是负数,
mantissa:尾数,unsigned long long类型
exponent:指数,决定了小数点在尾数中的位置
比如 15.99,
NSDecimalNumber price = [NSDecimalNumber decimalNumberWithMantissa:1599 exponent:-2 isNegative:NO];
NSDecimalNumberprice = [NSDecimalNumber decimalNumberWithString:@"15.99"];
可以使用字符串或者手动装配mantissa、exponent、sign的构造方法来生成一个NSDecimalNumber实例
基本的算术方法
加法
- (NSDecimalNumber *)decimalNumberByAdding:(NSDecimalNumber *)decimalNumber;
减法
- (NSDecimalNumber *)decimalNumberBySubtracting:(NSDecimalNumber *)decimalNumber;
乘法
- (NSDecimalNumber *)decimalNumberByMultiplyingBy:(NSDecimalNumber *)decimalNumber;
除法
- (NSDecimalNumber *)decimalNumberByDividingBy:(NSDecimalNumber *)decimalNumber;
幂次方
-(NSDecimalNumber*)decimalNumberByRaisingToPower:(NSUInteger)power;
指数
-(NSDecimalNumber*)decimalNumberByMultiplyingByPowerOf10:(short)power;
自定义处理行为
- (instancetype)initWithRoundingMode:(NSRoundingMode)roundingMode scale:(short)scale raiseOnExactness:(BOOL)exact raiseOnOverflow:(BOOL)overflow raiseOnUnderflow:(BOOL)underflow raiseOnDivideByZero:(BOOL)divideByZero NS_DESIGNATED_INITIALIZER;
+ (instancetype)decimalNumberHandlerWithRoundingMode:(NSRoundingMode)roundingMode scale:(short)scale raiseOnExactness:(BOOL)exact raiseOnOverflow:(BOOL)overflow raiseOnUnderflow:(BOOL)underflow raiseOnDivideByZero:(BOOL)divideByZero;
// Rounding policies :
// Original
// value 1.2 1.21 1.25 1.35 1.27
// Plain 1.2 1.2 1.3 1.4 1.3
// Down 1.2 1.2 1.2 1.3 1.
// Up 1.2 1.3 1.3 1.4 1.3
// Bankers 1.2 1.2 1.2 1.4 1.3
roundingMode 要使用的舍入模式,有四种值:
NSRoundUp, 向上舍入
NSRoundDown, 向下舍入
NSRoundPlain 四舍五入;当被夹在两个正数中间时,取整;当被夹在两个负数之间时,四舍五入。
NSRoundBankers 四舍五入;当为中间值时,将数值向上或向下舍入,使得结果的最低有效数字是偶数。
NSRoundBankers比较特殊,保留位数后一位的数字为5时,根据前一位的奇偶性决定。为偶时向下舍入,为奇数时向上舍入。如:1.25保留一位小数。5之前是2偶数向下取整1.2;1.35保留一位小数时。5之前为3奇数,向上取整1.4。
关于向偶数舍入,摘自csapp第二章 IEEE浮点部分:有什么理由偏向取偶数呢?为什么不始终把位于两个可表示的中间的值都向上舍入?假想一种场景,这组方法舍入一组数值,会在计算这些值的平均数中引入统计偏差。向上舍入得到的一组数的平均值比这些数平均值偏高,向下舍入的话,得到的平均值比这些数的平均值略低一些。 向偶数舍入在大多数现实情况避免了这种统计偏差。在50%的时间里向上舍入,50%的时间里向下舍入。
scale 结果保留几位小数
raiseOnExactness
如果为YES,在发生精确错误的情况下,处理程序将引发异常,否则它将忽略该错误并将控制权返回给调用方法。
raiseOnOverflow
如果为YES,在发生溢出错误时,处理程序将引发异常,否则它将忽略该错误并将控制权返回给调用方法
raiseOnUnderflow
如果为YES,则在发生下溢错误时,处理程序将引发异常,否则它将忽略该错误并将控制权返回给调用方法
raiseOnDivideByZero
如果为YES,则在出现除零错误时,处理程序将引发异常,否则它将忽略该错误并将控制权返回给调用方法
decimalNumber的比较
- (NSComparisonResult)compare:(NSNumber *)decimalNumber;
参考链接
计算机系统基础(一):程序的表示、转换与链接
计算机组成原理(唐朔飞)
书籍《深入理解计算机系统》
关于OC中的小数精确计算
十进制小数转换为二进制
浮点数之迷