一、抛砖引玉

一个简单的示例:

double a = 0.0;
IntStream.range(0,3).foreach(i->a+=0.1);
System.out.println(a); // 0.30000000000000004
System.out.println(a == 0.3); //false

可以看到计算机因二进制&浮点数造成的问题离我们并不遥远,一个double经过简单的相加,便出现了影响正常性的结果。

我们可以通过 BigDecimal 来更详细展示:

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
System.out.println( x + ", as double "+x.doubleValue());
x = x.add(_0_1);
}

输出:

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

二、不精确的原因

常听说double&float不精确,ieee754标准什么的,难道是标准导致的问题吗?

原因:问题是多综合因素导致的,而当下 iEEE754 标准则是各方面权衡下的尽可能逼近正确结果的一种方案

1. 二进制的必然局限

正如10进制下 1/3 = 0.333…无法精确表示,在二进制中若想表示1/10,则也是无限循环小数

具体的 \(0.1_{(10)}=0.0010011001100110011..._{(2)}\)

这就本质上造成了若不以分数表示,一些其他进制中的精确数值在二进制中无法以有限位精确表示

2. 计算机中数值存储方案

计算机中CPU对数值的存储&运算没有分数表示,而是以有有限位bit进行。(当然,可能会疑问为什么不以一定规则用分数精确存储,并附上相应的一套运算规则?可参考这个讨论

因此对于无限小数,存储位数一定的情况下必然会造成数值丢失。

如:\(0.1_{(10)}*3\) 在二进制 8bit 规则(若是单纯截断,没有舍入)下,结果为 \(0.00011001_{(2)}* 3=0.01001011_{(2)}=0.29296875_{(10)}\) 而不会是 0.3

这就如 \(0.1_{(3)}*3\) 在十进制计算机中(若是单纯截断)结果是 0.99999999 而不会是 1

3. 计算机数值表示规范 IEEE-754

根据上述讨论,便能认知到对于数值的存储和计算规则是可以千变万化的。

因此 IEEE 协会为了规范统一(方便CPU指令制造,各平台兼容等等)出台了 IEEE Standard for Floating-Point Arithmetic(IEEE-754)二进制浮点数算数标准,选用了浮点数作为储存和算数标准。

该标准描述了包括"浮点数的格式"、"一些特殊数值"、"浮点数的运算"、"舍入规则与例外情况" 等等内容

三、IEEE-754 标准"部分"概述

1. 它定义了5种基本格式:

binary32、binary64、binary128、decimal64、decimal128

其中 binary32、binary64 便是常说的 float、double

2. float、double解析:

以 binary64(double)为例:

它具有以下格式:

  • sign:符号位,0为正,1为负
  • exponent:无符号整数,此处范围为[0,2047]。实际应用时会加上一固定的偏移量,该偏移量根据exponent长度有所不同,而此处double 为 -1023,因此实际应用范围为[-1022,1023](缺少-1023和+1024是因为全0全1为特殊保留字)
  • precision:精度值,存储有效数字(隐式的整数位1并不包含其中)

其最终值结果表达式为: \((-1)^{sign}*1.fraction_{(2)}*2^{e-1023}\)

基于这种格式,这也是为什么数越大精度越低,越小精度越高。因为越大则fraction中整数占位越多,而小数占位则越少。(下图可见,小数部分已全部舍去,整数部分都开始舍入)

binary 32(float)同理:偏移量为 -127

3. 舍入规则:

IEEE-754 仅提供了一些舍入规则,但没有强制说选用某种规则,具体规则的选用由具体实现决定。

以下是一些规则:

  • Roundings to nearest 就近舍入

    • Round to nearest, ties to even:就近舍入。若数字位于中间,则偏向舍入到偶数最低有效位
    • Round to nearest, ties away from zero:就近舍入。偏向远离0,即四舍五入。
  • Directed roundings 定向舍入
    • Round toward 0:朝向0舍入
    • Round toward +∞:朝向+∞舍入
    • Round toward −∞:朝向-∞舍入

而在 Java 中,默认舍入模式为 RoundingMode.HALF_EVEN,即 "Round to nearest, ties to even"

该舍入模式也被称为 "Banker's rounding",在统计学上这种模式可以使累计的误差最小

4.手动计算IEEE754值示例

以常见的 0.1 和 float 为例:

\(0.1_{(10)}=0.0001100110011..._{(2)}=(-1)^0*1.100110011...01_{(2)}*2^{(123-127)}\)

因此 IEEE-754,存储的实际值为 0.10000000149011611938

可见,有效数字其实已经尽最大可能的去保留精度,无奈位数有限,并在最后做了舍入。

5.其他解决方案探讨

IEEE-754 浮点数不过是一种标准,它是性能&存储空间&表示范围&精度各方面权衡下的一个结果。正如上述和stackexchange所讨论的,若对精度或其他方面有着更高的需求,则可以另一套规则定义数值的存储和计算。

Decimal 便是其中的一种。摘一段网上的介绍

Decimal types work much like floats or fixed-point numbers, but they assume a decimal system, that is, their exponent (implicit or explicit) encodes power-of-10, not power-of-2. A decimal number could, for example, encode a mantissa of 23456 and an exponent of -2, and this would expand to 234.56. Decimals, because the arithmetic isn't hard-wired into the CPU, are slower than floats, but they are ideal for anything that involves decimal numbers and needs those numbers to be exact, with rounding occurring in well-defined spots - financial calculations, scoreboards, etc. Some programming languages have decimal types built into them (e.g. C#), others require libraries to implement them. Note that while decimals can accurately represent non-repeating decimal fractions, their precision isn't any better than that of floating-point numbers; choosing decimals merely means you get exact representations of numbers that can be represented exactly in a decimal system (just like floats can exactly represent binary fractions).

Decimal(十进制)的工作方式与 fixed-point(定点数)非常相似,只是以十进制为基础(指乘数为10的幂,而非2的幂),例如 234.56=23456*10^(−2) 可以扩展为 23456 与 -2,因为都是整数所以精确存储。

但 Decimal 并不会就比浮点数精确度高,正如其名十进制,它仅可以精确表示能在十进制中精确表示的数。而十进制中本身就无法精确表示的数,如 \(0.1_{(3)}\),其依然无法精确保存。

四、Java 中 BigDecimal 实现概述

不可变的,任意精度的有符号十进制数。

因十进制小数对二进制的转化是不精确的,因此它将 \(原值*10^{(scale)}\) 扩展为整数后,后通过 long intCompat 来存储扩展后部分。

并在需要真实值时,再计算还原 \(intCompact * 10^{(-scale)}\)

BigDecimal 常见API&情形:

  1. setScale(int newScale, RoundingMode roundingMode)

    设置该BigDecimal的小数点后精度位数,若涉及到数值舍入,必须指定舍入规则,否则报错。

    如:保留2位小数,截断式:.setScale(2, RoundingMode.DOWN)

五、延申

1. 定点数(fixed-point)解决方案

定点数在实现上并不是字面意思固定某位为小数点分别存整数和小数

同Decimal实现一样,先将原值扩展到到足够大的整数,并存下scale,以后续还

2. 各语言情况及解决概览

https://0.30000000000000004.com

3. 为什么数据库MYSQL SELECT (0.2+0.1)=0.3 返回 true?

参考:https://stackoverflow.com/a/55309851/9908241

答:在显式精确数值计算时,Mysql 可能会使用 Precision Math 计算( https://dev.mysql.com/doc/refman/8.0/en/precision-math-examples.html

SELECT (0.1+0.2) = 0.3 或多或少可能以如下方式执行实际查询:SELECT CAST((0.1 + 0.2) AS DECIMAL(1, 1)) = CAST((0.3) AS DECIMAL(1, 1));

IEEE 754 标准浮点数的精度问题是仍然存在的,以下通过显式声明浮点类型可复现:

create table test (f float);
insert into test values (0.1), (0.2);
select sum(f) from test; // 输出经典 0.30000000447034836

4. 浮点数为什么会这样设计,为什么exponent需要偏移量

可参考:IEEE 754格式是什么? - wuxinliulei的回答 - 知乎

撰文参考:

- 0.1d相加多次异常展示: https://stackoverflow.com/questions/26120311/why-does-adding-0-1-multiple-times-remain-lossless

- 数值存储&计算多种解决方案讨论: https://softwareengineering.stackexchange.com/questions/167147/why-dont-computers-store-decimal-numbers-as-a-second-whole-number/167151#167151

- 十转二进制计算教学 How to Convert a Number from Decimal to IEEE 754 Floating Point: https://www.wikihow.com/Convert-a-Number-from-Decimal-to-IEEE-754-Floating-Point-Representation

- 计算IEEE-754全步骤(可自定数字) https://binary-system.base-conversion.ro/convert-real-numbers-from-decimal-system-to-32bit-single-precision-IEEE754-binary-floating-point.php

- CSDN https://blog.csdn.net/weixin_44588495/article/details/97615664

- https://en.wikipedia.org/wiki/IEEE_754

- https://en.wikipedia.org/wiki/Double-precision_floating-point_format

- https://en.wikipedia.org/wiki/Single-precision_floating-point_format

- http://cr.openjdk.java.net/~darcy/Ieee754TerminologyUpdate/2020-04-21/specs/float-terminology-jls.html

- IEEE754 在线转换网站: https://www.binaryconvert.com/result_float.html

- 十进制-二进制(可小数)在线转换: https://www.mathsisfun.com/binary-decimal-hexadecimal-converter.html

- https://0.30000000000000004.com

Java 浮点数精确性探讨(IEEE754 / double / float)与 BigDecimal 解决方案的更多相关文章

  1. java中浮点数的比较(double, float)(转)

    问题的提出:如果我们编译运行下面这个程序会看到什么? public static void main(String args[]){ System.out.println(0.05+0.01); Sy ...

  2. Java浮点数float,bigdecimal和double精确计算的精度误差问题总结

    (转)Java浮点数float,bigdecimal和double精确计算的精度误差问题总结 1.float整数计算误差 案例:会员积分字段采用float类型,导致计算会员积分时,7位整数的数据计算结 ...

  3. Java中的浮点型(Double&Float)计算问题

    在刚刚做完的一个项目中,遇到了double型计算不精确的问题.到网上查找后,问题得到解决.经验共享,在这里总结一下. Java中的浮点数类型float和double不能够进行精确运算.这个问题有时候非 ...

  4. Java中浮点类型的精度问题 double float

    要说清楚Java浮点数的取值范围与其精度,必须先了解浮点数的表示方法与浮点数的结构组成.因为机器只认识01,你想表示小数,你要机器认识小数点这个东西,必须采用某种方法.比如,简单点的,float四个字 ...

  5. java浮点数剖析

    定点数表达法的缺点在于其形式过于僵硬,固定的小数点位置决定了固定位数的整数部分和小数部分,不利于同时表达特别大的数或者特别小的数.计算机系统采纳了所谓的浮点数表达方式.这种表达方式利用科学计数法来表达 ...

  6. JAVA浮点数计算精度损失底层原理与解决方案

    浮点数会有精度损失这个在上大学的时候就已经被告知,但是至今完全没有想明白其中的原由,老师讲的时候也是一笔带过的,自己也没有好好琢磨.终于在工作的时候碰到了,于是google了一番. 问题: 对两个do ...

  7. MySQL中Decimal类型和Float Double的区别 & BigDecimal与Double使用场景

    MySQL中存在float,double等非标准数据类型,也有decimal这种标准数据类型. 其区别在于,float,double等非标准类型,在DB中保存的是近似值,而Decimal则以字符串的形 ...

  8. Java 浮点数精度丢失

    Java 浮点数精度丢失 问题引入 昨天帮室友写一个模拟发红包抢红包的程序时,对金额统一使用的 double 来建模,结果发现在实际运行时程序的结果在数值上总是有细微的误差,程序运行的截图: 输入依次 ...

  9. 第二章 Java浮点数精确计算

    1.实际意义 在实际开发中,如果需要进行float或double的精确计算(尤其是财务计算),直接使用float或double是不行的(具体的例子看下边的代码的main方法的测试结果),需要使用Big ...

随机推荐

  1. 处理python中的信号

    什么是信号 信号(signal)-- 进程间通讯的一种方式,也可作为一种软件中断的方法.一个进程一旦接收到信号就会打断原来的程序执行来按照信号进行处理. 简化术语,信号是一个事件,用于中断运行功能的执 ...

  2. JPA事务中的异常最后不也抛出了,为什么没被catch到而导致回滚?

    上周,我们通过这篇文章<为什么catch了异常,但事务还是回滚了?>来解释了,之前test4为什么会回滚的原因. 但还是收到了很多没有理解的反馈,主要是根据前文给出的线索去跟踪,是获得到了 ...

  3. kubernates 1.20.6安装

    kubernates 安装 1. 前置要求 硬件条件 三台主机 1主2从 硬件配置 master 2核4G slave 2核2G 2. 安装 访问GitHub 仓库 https://github.co ...

  4. 【Azure 应用服务】Azure Function App 执行PowerShell指令[Get-Azsubscription -TenantId $tenantID -DefaultProfile $cxt]错误

    问题描述 使用PowerShell脚本执行获取Azure订阅列表的指令(Get-Azsubscription -TenantId $tenantID -DefaultProfile $cxt).在本地 ...

  5. CentOS-Docker搭建Nextcloud

    下载镜像 $ docker pull nextcloud 运行镜像 $ docker run -d --restart=unless-stopped --name nextcloud -v /home ...

  6. CentOS-Docker搭建VeryNginx

    下载镜像 $ docker pull camil/verynginx $ cd /home GIT克隆(yum install git -y) $ git clone https://github.c ...

  7. 基于Vue/React项目的移动端适配方案

    本文的目标是通过下文介绍的适配方案,使用vue或react开发移动端及H5的时候,不需要再关心移动设备的大小,只需要按照固定设计稿的px值布局,提升开发效率. 下文给出了本人分别使用create-re ...

  8. lua环境搭建

    前言: Linux & Mac上安装 Lua 安装非常简单,只需要下载源码包并在终端解压编译即可,本文介绍Linux 系统上,lua5.3.0版本安装步骤: ↓ 1. Linux 系统上安装 ...

  9. git rebase 和 git merger

    & git merge 在上图中,每一个绿框均代表一个commit.除了c1,每一个commit都有一条有向边指向它在当前branch当中的上一个commit. 图中的项目,在c2之后就开了另 ...

  10. Java程序设计(2021春)——第三章类的重用笔记与思考

    Java程序设计(2021春)--第三章类的重用笔记与思考 本章概览: 3.1 类的继承(概念与语法) 3.2 Object类(在Java继承最顶层的类) 3.3 终结类和终结方法(只能拿来用,不可以 ...