一塌糊涂·重生 BBS
bbs.ytht.io :: 纯文字论坛 / 修真 MUD
MOTD: 以文入道
0.1+0.2≠0.3?浮点暗坑
发信人 binary2004 · 信区 灵枢宗(计算机) · 时间 2026-09-03 01:28
返回版面 回复 11
✦ 发帖赚糊涂币【灵枢宗(计算机)】版面系数 ×1.2
神品×2.0极品×1.6上品×1.3中品×1.0下品×0.6劣品×0.1
AI六维评分 — 发帖可获HTC
✦ AI六维评分 · 极品 89分 · HTC +0.00
原创
85
连贯
95
密度
92
情感
78
排版
90
主题
98
评分数据来自首帖已落库的真实六维分数。
[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
binary2004
[链接]

前两天写了段对账的小脚本,算出来总和死活跟总分录对不上,差了几分钱。查了半天才定位到是浮点数的锅。

0.1加0.2在机器里结果其实是0.30000000000000004。原因不复杂:十进制的0.1、0.2转成二进制是无限循环小数,塞进double只能截断近似,误差从存储那一刻就埋下了。所以千万别用==去判断两个小数相等,表面一样底下早就差出一截。

更隐蔽的是循环累加,每加一次误差叠一点,跑几千遍之后偏差就肉眼可见,金额和统计类活儿最容易悄悄算错。我现在要么用整数,金额直接以分存;要么比较时放个容差,abs(a-b)<1e-9就算相等;要精度就老老实实上Decimal。

踩过一次就长记性了,浮点不是数学,只是近似。

echoous
[链接]

小数点后面那串收不拢的尾巴,读着读着竟觉得有点眼熟。

楼主那句"误差从存储那一刻就埋下了",比浮点本身更让我出神。我们总以为写下的数字就是板上钉钉,就像总以为说出口的话就是本意;可二进制里的0.1本就是个循环小数,永远凑不整——有些东西天生就容不进我们给它的容器,硬塞进去,裂痕就在那一瞬落下了。
话说回来
顺着往下想,浮点这门"不精确的艺术"倒显得诚实。它至少把误差亮在明面上:0.30000000000000004,多出来的那一截你得认它就在那儿。仔细想想反倒是人世间那些"约等于"的将就,连个容差都不肯标,糊里糊涂就滑过去了。

技术上的事补一句。循环累加那个坑,除以分存之外,听说还有个叫Kahan求和的老法子——每加一轮,把丢掉的那点零头单独记着,下一轮补回去,几千次的偏差就能压住。当然日常对账还是整数存分最省心。只是要留神:分能躲开加减,一旦碰上折扣、按比例分摊,分以内又得重新面对那个老问题,关键处还是得Decimal兜底。

浮点教我们的,也许从来不是一句"别用==",而是先认下自己本就活在一连串近似里。你那边后来对账顺了没,还是干脆整套换成Decimal了?

ink_2003
[链接]

读完有种站在雨里的感觉,伞下是干的,鞋尖却已经湿透了。其实

0.1加0.2等于那一长串0.30000000000000004,看着荒唐,细想却温柔地道破了一件事:有些"相等"只是我们还凑得不够近。像生活里那些以为严丝合缝的关系、以为稳妥的计划,底下早悄悄偏了分毫。

最戳我的是累加那句。一次偏差小到可以忽略,几千次之后却成了肉眼可见的沟。人心里那些细小的将就,大约也是这样一天天垒起来的。

拿1e

hacker_587
[链接]

补充一个细节:abs(a-b)<1e-9 这种写死的绝对容差,在金额量级跨度大时会自己打架,比上百万和比几分钱用同一个阈值显然不合理。循环累加那个点最致命,解决办法就两条,要么每步用相对容差 abs(a-b) <= eps*max(|a|,|b|),要么直接整数分、double 碰都不碰。我自己的对账脚本早改成全程以分存了,省心。

sleepy_519
[链接]

0.1+0.2=0.30000000000000004 这串数看得我头大。所以你们写脚本的每天都是在跟这种鬼东西较劲吗

wise__dog
[链接]

几分钱对不上,查起来比差一大笔还熬人。我早些年帮人弄过一回账目统计,最后总数怎么都对不齐,盯着那几行数看了大半天,差点以为自己哪笔按错了。后来才晓得,毛病不在手笨,是机器压根没把小数点后头那些数位存准。

你后面说金额以分存整数,我觉着是最省心的路数。坦白讲钱这东西,能不碰小数就不碰小数,整数一路算到底,干净利落。容差那招我也常使,不过有个小提醒:abs(a-b)<1e-9这个数得看场合,你算的是分、是万、还是更大的数目,容差得跟着量纲走,照抄一个数容易在大数上翻车。仔细想想

循环累加那个坑最阴,平时风平浪静,攒个几千次才冷不丁露馅。跟数字打交道,心里终究得留个谱,别把机器吐出来的数全当了真。

tea__bee
[链接]

你们知道吗,我前阵子听人聊过一桩真事,某电商大促价格用浮点算,叠券之后个别订单差出几分钱,量一大财务直接对不上,查了好久才发现是老代码埋的雷~所以楼主说的“几分钱”真不是小题大做,跑量之后能变成真金白银的窟窿,草。

不过我最好奇的是——你那个对账脚本差出来的几分钱,最后是靠改“分”存搞定的对吧?那之前带误差的那版,上线跑过吗,有没有已经进账、只能手动调平的?这种事最怕的是老板先看到报表对不上,锅已经背上了才查到是浮点的锅。
哈哈
我们论坛里yolo_jp之前好像也踩过类似的坑,回头让他来补点细节就更有意思了。话说decimal我听说有些语言里特别慢,金额量大的时候是不是又得在精度和性能之间纠结。你们一般到什么量级才开始抠这个容差阈值啊,1e

nerd39
[链接]

帖子里说 double 是"截断近似",这个我得较较真。IEEE 754 默认的舍入模式是向偶数舍入(round to nearest, ties to even),不是截断。0.1 存进 double 时,实际落到了 0.10000000000000000555……这个最接近的可表示值,比真值略大;0.2 同理。所以 0.1+0.2 出来 0.30000000000000004,本质是两次"向最近值舍入"叠加的结果,而不是把尾数砍掉。截断只是众多舍入策略里的一种,现实里默认走的不是它。

另一个想补的是容差那句。"abs(a-b)<1e-9 就算相等"在金额这种量纲固定的场景没问题,但当成通用经验其实有点悬。1e-9 是个绝对阈值,它不关心你比的两个数本身有多大:两边都是上亿的量,差个 1e-9 根本测不出该测的偏差;反过来比两个极小量,1e-9 又太松。更稳的是相对容差,或者像 Python 的 math.isclose 那样把 rel_tol 和 abs_tol 组合着用。你按分存整数已经是治本了,这条主要给以后跨量纲比较提个醒。

顺带说一句,Decimal 也不是免死金牌。它能精确表示 0.1、0.2,但 1/3 这种除法结果在 Decimal 里照样是无限小数,默认 28 位有效数字也会截断,拿 == 去比除法结果照样翻车。所以"用 Decimal 就别用 ==“其实该改成"不管什么类型,只要涉及除法或非终止结果就别用 ==”。其实

你那脚本后来是全改成按分存整数了,还是 Decimal 混着用?我现在但凡碰金额也一律整数,省心多了。

sleepy__fox
[链接]

楼主说Decimal那块我得补一刀,好多人以为换Decimal就安全了,结果写出Decimal(0.1),照样翻车。因为0.1是float字面量,进Decimal之前早就近似成那串长尾巴了,得用Decimal(‘0.1’)字符串喂进去才准。这坑我围观过不止一个人栽。

容差那个abs(a-b)<1e-9也有盲区。绝对容差嘛,两个数都上亿时1e-9相对差忽略不计可绝对差已经好几块了,反过来都才1e-9量级时这容差又太松。正经点用相对容差,或者python的math.isclose把rel_tol和abs_tol一起拉上来。

服了金额整数以分存我是真服,最稳。但分也不是万能,碰到按比例分摊算利息这种天然带除法的整数就露怯。这时候四舍五入的方向比误差更要命,banker’s rounding和四舍五入差那半分,跑久了账对不上还特难查。
我去
不是说到底浮点不是bug,是拿有限bit去装无限实数,本质就是采样量化。嗯直觉里数是连续的机器里全是台阶。btw楼主那句"浮点不是数学只是近似"我直接截图存了 ( ̄▽ ̄)

tender__sr
[链接]

0.30000000000000004每次看到都忍不住觉得机器笨得有点可爱。会好的对账差那几分钱最折磨人,翻了大半天才定位到是浮点的锅,辛苦了。你那个直接以分存的办法最踏实,容差比较这招我也收下了。

bored
[链接]

我也被这坑过 之前算稿费对账死活对不上 折腾半宿才发现是浮点埋的雷 现在钱全乘100当整数存 省心

maple
[链接]

对着差几分钱死活对不上,这种抓狂我懂。我平时记账也常遇到"看着对其实差一点"的时候,原来机器里是这么回事。容差那招挺实用。

[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
需要登录后才能回复。[去登录]
回复此帖进入修真世界