资讯详情

IEEE 754浮点数精度揭秘:0.1+0.2为何不等于0.3

📅 2026/10/20 4:33:41 | 华诺云谱 👁 阅读
IEEE 754浮点数精度揭秘:0.1+0.2为何不等于0.3
1. 从一个让无数人怀疑人生的等式说起0.1 0.2 0.3这行代码如果你在浏览器控制台或者 Python 交互环境里敲下去返回的结果大概率是一个干脆利落的false。第一次撞见这个现象的人反应基本分三档第一档觉得电脑坏了第二档觉得语言有 bug第三档开始怀疑自己当年数学是不是白学了。我当年属于第三档盯着屏幕愣了好一会儿还特意拿计算器按了一遍确认 0.1 加 0.2 确实等于 0.3。这个现象不是某个语言的锅也不是浏览器抽风它是浮点数在计算机里的二进制表示方式决定的必然结果。JavaScript、Python、Java、C、Go只要用的是 IEEE 754 标准的双精度浮点数都会得到同样的答案。换句话说这不是 bug是 feature是计算机在“用有限位表示无限小数”这件事上做出的妥协。这篇内容我想干的事情很明确把“为什么 0.1 0.2 不等于 0.3”这件事从根上讲透。不是那种“因为浮点数精度问题所以不要用 比较”的一句话敷衍而是真正把十进制小数怎么变成二进制、二进制怎么存进 64 位、误差从哪一步开始产生、为什么有些小数能精确表示有些不能这一整条链路拆开给你看。看完之后你不仅能给别人讲明白还能在实际写代码时知道哪些坑要绕、哪些场景必须换方案。适合谁看只要写过代码、调过金额计算、做过数据比对的人都值得花时间把这套逻辑理顺。前端同学处理价格显示、后端同学做订单金额、数据同学做浮点聚合全都绕不开这个话题。哪怕你只是好奇为什么计算器算得对电脑算不对这篇也能给你一个交代。2. 十进制小数转二进制误差的源头在这里2.1 整数和小数转换规则完全不是一回事要理解误差从哪来得先搞清楚计算机怎么把十进制小数变成二进制。很多人对二进制转换的印象停留在整数那一套除 2 取余倒序排列。比如 13 转二进制13 除以 2 得 6 余 16 除以 2 得 3 余 03 除以 2 得 1 余 11 除以 2 得 0 余 1倒着读就是1101。这套方法干净利落任何整数都能在有限步内转完。但小数部分完全是另一套逻辑乘 2 取整顺序排列。拿 0.625 举例0.625 乘 2 得 1.25取整数部分 10.25 乘 2 得 0.5取整数部分 00.5 乘 2 得 1.0取整数部分 1小数部分变成 0停止。顺序读下来就是0.101验证一下1/2 0/4 1/8 0.5 0.125 0.625对上了。问题就出在这个“停止”上。整数转换一定会停因为整数除以 2 迟早会到 0。但小数乘 2 不一定停它可能陷入无限循环。0.625 运气好三步就结束了。换成 0.1 呢我们来实际走一遍。2.2 0.1 的二进制展开一个无限循环小数0.1 乘 2 得 0.2取整 00.2 乘 2 得 0.4取整 00.4 乘 2 得 0.8取整 00.8 乘 2 得 1.6取整 10.6 乘 2 得 1.2取整 10.2 乘 2 得 0.4取整 0……到这里你应该发现了0.2 又回来了接下来就是 0.4、0.8、1.6、1.2、0.4 无限循环。所以 0.1 的二进制是0.000110011001100110011...其中0011这四位无限重复。用数学语言说0.1 在二进制下是一个无限循环小数。这跟十进制里 1/3 0.3333... 是同一类东西只不过十进制里循环的是 3二进制里循环的是 0011。0.2 呢0.2 乘 2 得 0.4 取 00.4 乘 2 得 0.8 取 00.8 乘 2 得 1.6 取 10.6 乘 2 得 1.2 取 1然后 0.2 又回来了。所以 0.2 的二进制是0.001100110011...循环节同样是 0011只是起始位置比 0.1 晚一位。这里有个关键认知十进制里看起来人畜无害的 0.1 和 0.2在二进制里都是无限循环小数。而计算机的存储空间是有限的64 位双精度浮点数能用来存小数部分的位数就那么多不可能存下无限循环的东西。存不下怎么办只能截断只能近似。误差从这一刻就埋下了。2.3 哪些小数能精确表示哪些不能判断一个十进制小数能不能被二进制精确表示有个简单的规律看它能不能写成若干个 2 的负整数次幂之和。2 的负整数次幂是 1/2、1/4、1/8、1/16、1/32……也就是 0.5、0.25、0.125、0.0625、0.03125 这些。0.5 能就是 1/2。0.25 能就是 1/4。0.625 能是 1/2 1/8。0.75 能是 1/2 1/4。但 0.1 不能因为 0.1 等于 1/10分母 10 含有质因数 5而 2 的幂次分母只含质因数 2。一个最简分数的分母如果含有 2 以外的质因数它在二进制下就是无限循环小数。这个规律可以推广在 b 进制下能精确表示的最简分数其分母的质因数必须全部是 b 的质因数。十进制下分母只含 2 和 5 的分数能精确表示所以 1/2、1/4、1/5、1/8、1/10 都能写成有限小数。二进制下分母只能含 2所以 1/10 就不行了。十进制小数二进制表示能否精确表示0.50.1能0.250.01能0.6250.101能0.10.000110011...不能无限循环0.20.001100110...不能无限循环0.30.010011001...不能无限循环看到这张表你应该明白了0.1、0.2、0.3 三个数在二进制里全是无限循环小数计算机存的都是近似值。三个近似值做运算结果自然也是近似值两个近似值恰好相等的概率极低。这就是0.1 0.2 ! 0.3的根本原因。3. IEEE 754 双精度浮点数64 位到底怎么分配3.1 符号位、指数位、尾数位的分工知道了小数转二进制会出问题接下来得看计算机具体怎么存。目前主流语言用的都是 IEEE 754 标准的双精度浮点数总共 64 位分成三段符号位sign1 位0 表示正数1 表示负数。指数位exponent11 位用来表示数量级也就是小数点该往哪挪。尾数位fraction/mantissa52 位用来存有效数字。这里有个容易搞混的点浮点数存的是科学计数法的二进制形式也就是1.xxxxx × 2^e这种结构。尾数位存的是1.xxxxx里小数点后面的xxxxx部分那个整数位的 1 是隐含的不占存储空间。这叫隐含前导 1规则是 IEEE 754 的一个巧妙设计相当于白赚一位精度。指数位不是直接存指数本身而是存偏移后的值。双精度的偏移量是 1023也就是说实际指数 e 存进去的时候要加 1023。比如 e -4存的就是 1019。这样做的目的是让指数位能表示负数同时方便硬件比较大小。3.2 0.1 在 64 位里长什么样我们来实际算一下 0.1 的存储。0.1 的二进制是0.000110011001100110011...把它规格化成科学计数法小数点要右移 4 位变成1.100110011001100110011... × 2^-4。指数是 -4加上偏移 1023 得 1019转成 11 位二进制是01111111011。尾数部分取1001100110011001100110011001100110011001100110011001注意这里只取 52 位后面无限循环的部分被截断了。截断的时候会做舍入IEEE 754 默认用的是“向偶数舍入”也就是四舍五入的变种遇到正好在中间的情况往偶数那边靠。最终 0.1 在内存里的 64 位是0 01111111011 1001100110011001100110011001100110011001100110011001符号位 0指数位 01111111011尾数位 52 位。这个值跟真正的 0.1 之间有一个极小的误差大约是 5.55e-18 量级。0.2 同理指数是 -3尾数也是 0011 循环同样被截断。3.3 加法运算时误差是怎么累积的0.1 和 0.2 各自带着误差存进去做加法的时候两个数要先对齐指数。0.1 是 1.xxx × 2^-40.2 是 1.xxx × 2^-3要把 0.1 的尾数右移一位或者把 0.2 的尾数左移一位让指数一致才能相加。右移的过程中最低位会被挤出去又产生一次精度损失。加完之后的结果再规格化、再舍入存回 64 位。这一连串操作下来最终得到的“0.1 0.2”的结果跟真正的 0.3 之间存在一个微小的偏差。在双精度下这个偏差大约是 5.55e-17 量级。而 0.3 单独存储的时候也有自己的舍入误差。两个误差来源不同、大小不同所以0.1 0.2的结果和0.3的存储值不相等。如果你在控制台打印0.1 0.2会看到0.30000000000000004那个尾巴上的 4 就是误差的可视化体现。注意这个 4 不是随便出现的它是二进制舍入误差在十进制下的投影。不同语言、不同平台打印出来的位数可能略有差异但底层 64 位表示是一致的。4. 实际开发中怎么处理浮点数比较4.1 绝对不能用 直接比较知道了原理第一条铁律就出来了永远不要用或直接比较两个浮点数。不管是 JavaScript、Python 还是 Java这条规则都适用。0.1 0.2 0.3返回 false 只是最经典的例子实际业务里还有更多隐蔽的坑。比如判断两个金额是否相等、判断循环是否该结束、判断坐标是否重合只要涉及浮点数直接比较都可能出问题。我见过一个真实案例某系统用浮点数累加计算总价累加 100 次 0.01结果跟预期的 1.0 差了 1e-15 量级导致对账时判定为不一致排查了大半天才定位到浮点误差。4.2 用 epsilon 做容差比较正确的做法是引入一个容差epsilon判断两个数的差的绝对值是否小于这个容差function nearlyEqual(a, b, epsilon 1e-9) { return Math.abs(a - b) epsilon; } nearlyEqual(0.1 0.2, 0.3); // trueepsilon 取多大要看业务场景。科学计算可能取 1e-12普通业务取 1e-9 或 1e-6 都行。原则是epsilon 要大于你业务中可接受的最大误差同时小于你业务中需要区分的最小差值。Python 里可以用math.isclose()它内部就是做容差比较还支持相对容差import math math.isclose(0.1 0.2, 0.3) # TrueJava 里可以用Double.compare()配合自定义容差或者用BigDecimal做精确计算。4.3 金额计算必须换方案涉及金额的场景浮点数绝对不能碰。金额有明确的精度要求通常到分而且对账必须分毫不差。这时候要用定点数或者十进制浮点数。最稳妥的方案是用整数存最小单位。比如金额以分为单位存成整数1.23 元存成 123 分所有运算都用整数做最后显示的时候再除以 100。这样完全避开了浮点误差。如果语言支持也可以用专门的十进制类型。Java 的BigDecimal、Python 的decimal.Decimal、C# 的decimal都是为精确十进制运算设计的。它们内部用十进制存储能精确表示 0.1、0.2 这些数代价是运算速度比原生浮点慢不少。BigDecimal a new BigDecimal(0.1); BigDecimal b new BigDecimal(0.2); BigDecimal c a.add(b); // 0.3精确 System.out.println(c.equals(new BigDecimal(0.3))); // true提示用BigDecimal的时候一定要用字符串构造用new BigDecimal(0.1)会把浮点误差带进去等于白用。方案精度性能适用场景原生浮点数有误差最快科学计算、图形、非精确场景epsilon 容差比较可控误差快一般数值比较整数存最小单位精确快金额、计数BigDecimal/Decimal精确较慢金融、财务、高精度要求5. 那些年我踩过的浮点数坑5.1 循环累加导致的偏差早年做数据统计有个需求是把一批小数累加求和。我图省事直接用浮点数循环加结果加了几万条之后总和跟用数据库SUM出来的结果对不上。排查半天才发现数据库内部用的是更高精度的累加而我这边每加一次都产生一次舍入误差误差累积起来就明显了。后来改成先排序再累加把绝对值小的数先加能减少大数吃小数的现象。更好的做法是用 Kahan 求和算法专门补偿浮点累加的误差。再后来涉及金额的场景直接全换成整数运算一劳永逸。5.2 前端显示金额的坑前端展示金额时toFixed(2)是常用方法但它也有坑。(1.005).toFixed(2)返回的是1.00而不是1.01因为 1.005 在二进制里存的是 1.00499999...四舍五入自然往下走。这个坑在价格计算里特别致命用户看到的价格可能少一分钱。解决办法是后端算好金额再传给前端前端只负责显示不做任何金额运算。如果非要在前端算用整数分做运算显示时再格式化。5.3 数据库字段类型选错数据库里存金额FLOAT和DOUBLE都是雷。MySQL 的FLOAT精度只有 7 位左右存大额金额直接丢精度。正确做法是用DECIMAL(m, d)比如DECIMAL(10, 2)表示总共 10 位、小数 2 位能精确存储金额。我见过一个项目订单表金额字段用了DOUBLE上线半年后对账发现有几笔订单差了一分钱查了半天才定位到字段类型。改字段类型涉及数据迁移成本不小所以这个坑最好在设计阶段就避开。5.4 常见问题速查表现象原因解决方案0.1 0.2 ! 0.3二进制无法精确表示 0.1 和 0.2用 epsilon 比较或换十进制类型toFixed 结果不符合预期底层存储值有偏差后端算好再传或整数运算累加结果偏差大误差累积Kahan 求和或整数运算数据库金额对不上字段用了 FLOAT/DOUBLE改用 DECIMAL大数吃小数指数对齐时小指数被截断排序后累加或提高精度注意浮点数的坑不止这些但核心逻辑就一条——只要涉及精确数值就别用原生浮点。把这条刻在脑子里能避开八成以上的相关问题。6. 浮点数还能怎么用理解边界才能用好它浮点数不是设计缺陷它是工程上的权衡。用 64 位表示一个实数范围从 1e-308 到 1e308 的数还要兼顾精度和性能能做到现在这样已经很了不起了。科学计算、图形渲染、机器学习这些领域大量依赖浮点数它们对绝对精度不敏感对范围和速度敏感浮点数正好合适。真正的问题在于用错场景。把浮点数用在金额、计数、需要精确比较的地方就是拿螺丝刀当锤子使。理解它的边界知道什么时候该用、什么时候该换才是正确的态度。如果你想把这块知识再往深挖可以看看 IEEE 754 标准的完整文档里面还有单精度、半精度、特殊值NaN、Infinity的处理规则。也可以自己写个小工具把任意十进制小数转成 64 位二进制表示亲手验证一遍 0.1 的存储结构比看十篇文章都管用。我个人在实际项目里的体会是浮点数相关的 bug 往往不是技术难题而是认知盲区。一旦你把二进制表示这条链路理顺了再遇到类似问题第一反应就会是“这里是不是精度问题”排查效率能提升一大截。最后分享一个小技巧调试浮点数问题时别只看十进制打印结果用Number.prototype.toString(2)或者 Python 的float.hex()看看底层二进制很多疑惑当场就解开了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑