深入理解浮点数
在实际项目中我们常用浮点数进行运算和比较,往往运算结果和自己心算的结果不一致,但相差很小,或者本应该相等的浮点数返回比较结果总是false.因此我们有必要深入了解下浮点数。
1.十进制和二进制互相转换
1.1 十进制转二进制
整数部分整除,小数部分短乘
举个栗子:
1.2 二进制转十进制
按照相应的指数求和
举个栗子:
2. 浮点数的表示
任意一个浮点数都有n 多种表达方式,比如 33.1415,可以表示为33.1415、3.31415 * 10 、0.331415 * 10^2 、331.1415 * 10 ^ -1 ….
为了方便统一表示,IEEE 754标准规定:
float单精度浮点数为32位:符号位 1bit、指数8bits、尾数23bits。
double双精度浮点数为64位:符号位 1bit、指数11bits、尾数52bits
3. 浮点数的陷阱
为了方便说明浮点数的陷阱,将IEEE 754标准 简化 浮点型 为5 位,4 位尾数,1位指数,进位规则为奇进偶舍(最后一位如果是奇数,则进位,偶数则舍弃)
3.1 0.2 * 3 != 0.6
0.2 的二进制表示为0.0011 0011 0011 …… 由于简化模型尾数为4位,并且第四位是奇数,所以 0.2 在简化模型中的表达是 0.010,乘以 3 后 表达为0.11 ,十进制值为0.75
0.6 的二进制表示为0.1001 1001 1001 …… 由于简化模型尾数为4位,并且第四位是偶数,所以0.6 在简化模型中的表达是 0.100,十进制值为0.5
3.2 不满足交换律 (a + b)+ c != a + ( b + c )
例如 (3 + 0.125) + 0.125 != 3 + (0.125 + 0.125)
3.3 不满足结合律 (a + b)*c != a*c + b*c
例如 (0.125 + 0.125)* 0.5 != 0.125* 0.5 + 0.125*0.5
4. java语言BigDecimal与数据库decimal是否能解决精度精确计算问题?
理论上能,坑很多,非常不建议以这种方式解决精度问题。即使java ok了,oc和js怎么办? 强烈建议取最小单位规格化成整数存储和运算,例如金额用分,土地面积用厘做单位。
4.1 BigDecimal的存储格式
实际是定点存储,即完全作为整数存,再附加一个小数点位置对应的负指数,因此才不丢精度。但是要非常注意,必须使用String初始化BigDecimal才可能不丢精度,所有算数运算也要通过BigDecimal操作才行,只要有中间值涉及到浮点数就会丢精度。
Java(JDK8)的 java.math.BigDecimal
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
public BigDecimal(
char[] in,
int offset,
int len, MathContext mc) {
......
char coeff[] =
new char[len];
for (; len >
0; offset++, len--) {
c = in[offset];
......
if (dot)
++scl;
......
}
......
rb =
new BigInteger(coeff, isneg ? -
1 :
1, prec);
......
this.scale = scl;
this.intVal = rb;
}
}
public class BigInteger extends Number implements Comparable<BigInteger> {
final int[] mag;
BigInteger(
char[] val,
int sign,
int len) {
int numWords;
if (len <
10) {
numWords =
1;
}
else {
long numBits = ((numDigits * bitsPerDigit[
10]) >>>
10) +
1;
if (numBits +
31 >= (
1L <<
32)) {
reportOverflow();
}
numWords = (
int) (numBits +
31) >>>
5;
}
int[] magnitude =
new int[numWords];
......
while (cursor < len) {
group = val.substring(cursor, cursor += digitsPerInt[radix]);
groupVal = Integer.parseInt(group, radix);
destructiveMulAdd(magnitude, superRadix, groupVal);
}
......
mag = trustedStripLeadingZeroInts(magnitude);
}
}
4.2 mysql decimal的存储格式
数据库decimal实际也是定点数存储,与BigDecimal的存储方式都是一样的。但是将浮点数存入decimal,或者将decimal读取成浮点数,都会损失精度,与用float/double无异。使用必须小心翼翼,在程序层面用BigDecimal,存储时将BigDecimal转String再cast成db decimal,从数据库读取时也是以String读出db decimal,再以String初始化BigDecimal。
mariadb-server-
10.3\include\decimal.h
typedef struct st_decimal_t {
int intg, frac, len;
my_bool sign;
decimal_digit_t *buf;
} decimal_t;
mariadb-server-
10.3\strings\decimal.c
int internal_str2dec(
const char *from, decimal_t *to,
char **end, my_bool fixed) {
......
buf=to->buf+intg1;
for (x=
0, i=
0; intg; intg--)
{
......
if (unlikely(++i == DIG_PER_DEC1))
{
*--buf=x;
x=
0;
i=
0;
}
}
......
buf=to->buf+intg1;
for (x=
0, i=
0; frac; frac--)
{
......
if (unlikely(++i == DIG_PER_DEC1))
{
*buf++=x;
x=
0;
i=
0;
}
}
......
}
5. 基础是否重要?
每个人都要独立面对问题,所以要有解决问题的能力,而非背诵答案的能力。知道浮点数不可精确比较,比不知道好,但是不知道原理,就难以规避浮点数的其他陷阱。