详谈浮点精度(float、double)运算不精确的原因

目录
  • 为什么浮点精度运算会有问题
  • 精度运算丢失的解决办法
  • 拓展:详解浮点型

为什么浮点精度运算会有问题

我们平常使用的编程语言大多都有一个问题——浮点型精度运算会不准确。比如

double num = 0.1 + 0.1 + 0.1;
// 输出结果为 0.30000000000000004
double num2 = 0.65 - 0.6;
// 输出结果为 0.05000000000000004

笔者在测试的时候发现 C/C++ 竟然不会出现这种问题,我最初以为是编译器优化,把这个问题解决了。但是 C/C++ 如果能解决其他语言为什么不跟进?根据这个问题的产生原因来看,编译器优化解决这个问题逻辑不通。后来发现是打印的方法有问题,打印输出方法会四舍五入。使用 printf("%0.17f\n", num); 以及 cout << setprecision(17) << num2 << endl; 多打印几位小数即可看到精度运算不准确的问题。

那么精度运算不准确这是为什么呢?

我们接下来就需要从计算机所有数据的表现形式二进制说起了。如果大家很了解二进制与十进制的相互转换,那么就能轻易的知道精度运算不准确的问题原因是什么了。如果不知道就让我们一起回顾一下十进制与二进制的相互转换流程。

一般情况下二进制转为十进制我们所使用的是按权相加法。十进制转二进制是除2取余,逆序排列法。很熟的同学可以略过。

// 二进制到十进制
10010 = 0 * 2^0 + 1 * 2^1 + 0 * 2^2 + 0 * 2^3 + 1 * 2^4 = 18  

// 十进制到二进制
18 / 2 = 9 .... 0
9 / 2 = 4 .... 1
4 / 2 = 2 .... 0
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1

10010

那么,问题来了十进制小数和二进制小数是如何相互转换的呢?

十进制小数到二进制小数一般是整数部分除 2 取余,逆序排列,小数部分使用乘 2 取整数位,顺序排列。二进制小数到十进制小数还是使用按权相加法。

// 二进制到十进制
10.01 = 1 * 2^-2 + 0 * 2^-1 + 0 * 2^0 + 1 * 2^1 = 2.25

// 十进制到二进制
// 整数部分
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1
// 小数部分
0.25 * 2 = 0.5 .... 0
0.5 * 2 = 1 .... 1 

// 结果 10.01

转小数我们也了解了,接下来我们回归正题,为什么浮点运算会有精度不准确的问题。接下来我们看一个简单的例子 2.1 这个十进制数转成二进制是什么样子的。

2.1 分成两部分
// 整数部分
2 / 2 = 1 .... 0
1 / 2 = 0 .... 1

// 小数部分
0.1 * 2 = 0.2 .... 0
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
0.2 * 2 = 0.4 .... 0
0.4 * 2 = 0.8 .... 0
0.8 * 2 = 1.6 .... 1
0.6 * 2 = 1.2 .... 1
............

落入无限循环结果为 10.0001100110011........ , 我们的计算机在存储小数时肯定是有长度限制的,所以会进行截取部分小数进行存储,从而导致计算机存储的数值只能是个大概的值,而不是精确的值。

从这里看出来我们的计算机根本就无法使用二进制来精确的表示 2.1 这个十进制数字的值,连表示都无法精确表示出来,计算肯定是会出现问题的。

精度运算丢失的解决办法

现有有三种办法

  • 如果业务不是必须非常精确的要求可以采取四舍五入的方法来忽略这个问题。
  • 转成整型再进行计算。
  • 使用 BCD 码存储和运算二进制小数(感兴趣的同学可自行搜索学习)。

一般每种语言都用高精度运算的解决方法(比一般运算耗费性能),比如 Python 的 decimal 模块,Java 的 BigDecimal,但是一定要把小数转成字符串传入构造,不然还是有坑,其他语言大家可以自行寻找一下。

# Python 示例
from decimal import Decimal

num = Decimal('0.1') + Decimal('0.1') + Decimal('0.1')
print(num)
// Java 示例
import java.math.BigDecimal;

BigDecimal add = new BigDecimal("0.1").add(new BigDecimal("0.1")).add(new BigDecimal("0.1"));
System.out.println(add);

拓展:详解浮点型

上面既然提到了浮点型的存储是有限制,那么我们看一下我们的计算机是如何存储浮点型的,是不是真的正如我们上面提到的有小数长度的限制。

那我们就以 Float 的数据存储结构来说,根据 IEEE 标准浮点型分为符号位,指数位和尾数位三部分(各部分大小详情见下图)。

IEEE 754 标准

一般情况下我们表示一个很大或很小的数通常使用科学记数法,例如:1000.00001 我们一般表示为 1.00000001 * 10^3,或者 0.0001001 一般表示为 1.001 * 10^-4。

符号位

0 是正数,1 是负数

指数位

指数很有意思因为它需要表示正负,所以人们创造了一个叫 EXCESS 的系统。这个系统是什么意思呢?它规定 最大值 / 2 - 1 表示指数为 0。我们使用单精度浮点型举个例子,单精度浮点型指数位一共有八位,表示的十进制数最大就是 255。那么 255 / 2 - 1 = 127,127 就代表指数为 0。如果指数位存储的十进制数据为 128 那么指数就是 128 - 127 = 1,如果存储的为 126,那么指数就是 126 - 127 = -1。

尾数位

比如上述例子中 1.00000001 以及 1.001 就属于尾数,但是为什么叫尾数呢?因为在二进制中比如 1.xx 这个小数,小数点前面的 1 是永远存在的,存了也是浪费空间不如多存一位小数,所以尾数位只会存储小数部分。也就是上述例子中的 00000001 以及 001 存储这样的数据。

IEEE 754 标准

通过上述程序我们得到的存储 1.25 的 float 二进制结构的具体值为 00111111101000000000000000000000 ,我们拆分一下 0 为符号位他是个正值。01111111 为指数位,01000000000000000000000 是尾数。接下来我们验证一下 01111111 转为十进制是 127,那么经过计算指数为 0。尾数是 01000000000000000000000 加上默认省略的 1 为 1.01(省略后面多余的 0),转换为十进制小数就是 1.25。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 基于C++浮点数(float、double)类型数据比较与转换的详解

    浮点数在内存中的存储机制和整型数不同,其有舍入误差,在计算机中用近似表示任意某个实数.具体的说,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学记数法.所以浮点数在运算过程中通常伴随着因为无法精确表示而进行的近似或舍入.但是这种设计的好处是可以在固定的长度上存储更大范围的数.1.将字符串转换为float.double过程存在精度损失,只是float.double各自损失的精度不相同而已std::string str="8.2&

  • C语言实现将double/float 转为字符串(带自定义精度)

    目录 将double/float转为字符串(带自定义精度) 需用到头文件有 简单用法 float和double精度问题 double和float的区别 float和double的精度 将double/float转为字符串(带自定义精度) char *double_to_string(double d, int decimal) { decimal = decimal < 0 ? 0 : decimal; char *p; char dd[20]; switch (decimal) { case

  • C语言中魔性的float浮点数精度问题

    从一个问题引入 如果你以前接触过C语言,那么对下面的这段代码一定很熟悉: #include <stdio.h> int main(void) { float f_num1 = 21.75; float f_num2 = 13.45; printf("f_num1 = %f\n", f_num1); printf("f_num2 = %f\n", f_num2); printf("f_num1 + f_num2 = %f\n", f_n

  • 详谈浮点精度(float、double)运算不精确的原因

    目录 为什么浮点精度运算会有问题 精度运算丢失的解决办法 拓展:详解浮点型 为什么浮点精度运算会有问题 我们平常使用的编程语言大多都有一个问题--浮点型精度运算会不准确.比如 double num = 0.1 + 0.1 + 0.1; // 输出结果为 0.30000000000000004 double num2 = 0.65 - 0.6; // 输出结果为 0.05000000000000004 笔者在测试的时候发现 C/C++ 竟然不会出现这种问题,我最初以为是编译器优化,把这个问题解决了

  • 详谈javascript精度问题与调整

    一个经典的问题: 0.1+0.2==0.3 答案是:false 因为:0.1+0.2=0.30000000000000004 第一次看到这个结果就是无比惊讶,下巴碰到地上,得深入了解下问题出在哪里,该怎么去调整. 产生问题的原因 在JS中数值类型就只有number类型,没有int,float,double之分,number类型实际上存储的就是IEEE754标准的浮点数,计算规则也是. 在表达式计算前,先要按照标准将两个数转成浮点数. IEEE 754规定: 1.32位的浮点数(单精度),最高的1

  • Python浮点型(float)运算结果不正确的解决方案

    一.问题说明 以前对浮点数运行没有没有太在意.昨天同事要求把百分比结果保存到文件上,然后就以保存1位小数的形式给他保存了. 但是今天同事运行时问能不能统一以一位小数的形式保存,当时觉得很奇怪昨天就是以一位小数形式存的怎么还会提这种要求呢. 其给回的截图确实是部分是一位小数的,但一部分是很长的.查看代码都统一如下格式: # 使用round保留三位小数,然后乘以100,最后格式化为带百分号的字符串 rate=f"{round(x/y,3) * 100}%" 代码上没看出什么问题,直接运行确

  • MySQL中Decimal类型和Float Double的区别(详解)

    MySQL中存在float,double等非标准数据类型,也有decimal这种标准数据类型. 其区别在于,float,double等非标准类型,在DB中保存的是近似值,而Decimal则以字符串的形式保存数值. float,double类型是可以存浮点数(即小数类型),但是float有个坏处,当你给定的数据是整数的时候,那么它就以整数给你处理.这样我们在存取货币值的时候自然遇到问题,我的default值为:0.00而实际存储是0,同样我存取货币为12.00,实际存储是12. 幸好mysql提供

  • 详谈Linux开发中常见段错误问题的原因及分析

    1    使用非法的内存地址(指针),包括使用未经初始化及已经释放的指针.不存在的地址.受系统保护的地址,只读的地址等,这一类也是最常见和最好解决的段错误问题,使用GDB print一下即可知道原因. 2    内存读/写越界.包括数组访问越界,或在使用一些写内存的函数时,长度指定不正确或者这些函数本身不能指定长度,典型的函数有strcpy(strncpy),sprintf(snprint)等等. 3    对于C++对象,应该通过相应类的接口来去内存进行操作,禁止通过其返回的指针对内存进行写操

  • java精度计算代码 java指定精确小数位

    本文实例为大家分享了java指定精确小数位的具体代码,供大家参考,具体内容如下 java代码: public class App2 { public static void main(String[] args) { String val = checkNumber("10.1234155", 2, 6).toString(); System.out.println(val); } public static BigDecimal checkNumber(String number,

  • jquery精度计算代码 jquery指定精确小数位

    本文实例为大家分享了jquery指定精确小数位的具体代码,供大家参考,具体内容如下 /** * 将标签的值格式化 * id 标签id * min 最小值 * max 最大值 */ function toFloat(id,min,max){ var htmlVal = $("#"+id).html(); var index = htmlVal.indexOf("."); var result = ""; if(index > 0){ html

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

    问题: 对两个double类型的值进行运算,有时会出现结果值异常的问题.比如: System.out.println(19.99+20); System.out.println(1.0-0.66); System.out.println(0.033*100); System.out.println(12.3/100); 输出: 39.989999999999995 0.33999999999999997 3.3000000000000003 0.12300000000000001 Java中的简

  • 深入解析Java中的数据类型与变量

    Java数据类型转换(自动转换和强制转换) 数据类型的转换,分为自动转换和强制转换.自动转换是程序在执行过程中"悄然"进行的转换,不需要用户提前声明,一般是从位数低的类型向位数高的类型转换:强制类型转换则必须在代码中声明,转换顺序不受限制. 自动数据类型转换 自动转换按从低到高的顺序转换.不同类型数据间的优先关系如下:     低--------------------------------------------->高     byte,short,char-> int

  • Mysql高性能优化技能总结

    数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符 临时库表必须以tmp_为前缀并以日期为后缀,备份表必须以bak_为前缀并以日期(时间戳)为后缀 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低) 数据库基本设计规范 1

随机推荐