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

前阵子帮人看一段对账脚本,死活对不上,差那么几分钱。翻了半天才发现,他把每笔金额都用浮点数累加,末了拿 == 去跟预期总额比。问题就出在 0.1+0.2 这个老生常谈上。IEEE754 底下它算出来是 0.30000000000000004,看着相等,比起来几乎永远为假。

这种坑最阴的地方在于它不报错。程序跑得欢,结果悄悄偏。单笔看着没事,几百笔累加下来误差就溜进分位了,月底查账的人对着差额干瞪眼。我年轻时也在类似的判断里栽过,后来学乖了:涉及钱一律用整数分存,或者定点数;非要比较浮点就写 abs(a-b) < 1e-9 这种容差,别直接等号。

说到底,二进制表示小数本就有截断,这是底层的脾气,不是你代码写错了。知道它在,绕着走就行。

lazy
[链接]

整数分存是把双刃剑,它把坑从「浮点表示」挪到了「除法」上。好家伙

加减乘(乘整数倍费率)都没事,可一旦要按比例拆账——一笔钱按 3:7 分,或者扣个 0.05% 手续费——整数分除完必有余数。这余数怎么吞,四舍五入还是银行家舍入,得在业务层钉死规矩写进注释,不然换个人接手又是一个对不上的坑。

abs(a-b)<1e-9 也是偷懒写法,只在两个数量级接近时靠谱。真要一笔 0.01、一笔 1e8 放一块比,1e-9 直接失灵。正经浮点相等得上 ULP 或者相对误差。当然日常对账容差糊一下完全够用,别当万能钥匙就行。

我现在看到代码里用 == 比浮点就手痒想改,哈哈。你们线上这种对账最后靠啥兜底,测试卡边界还是人工复核?

maple__dog
[链接]

你讲那个"不报错"最戳我。程序跑得欢、结果悄悄偏,这种事最磨人,盯着屏幕半天都找不出毛病在哪儿。嗯嗯

我对你那句容差比较有点小保留。abs(a-b) < 1e-9 拿来救急确实省事,可一碰到钱我就犯嘀咕——账上差一分就是差一分,不该靠"差不多"蒙过去。真要动金额,整数分存着最干净,心里也踏实。tolerance 那种更适合物理仿真那种躲不开浮点的场合。

不过你说"底层的脾气,不是你代码写错",这话真暖。新手最容易自己吓自己,以为又是自己笨。你这篇写得这么清楚,省得多少人对着差额干瞪眼了。辛苦辛苦。

logic90
[链接]

补个数据。楼主说"几百笔累加误差就溜进分位",这个量级我有点存疑。双精度尾数53位,单笔0.1的表示误差约5e-17,几百笔累加下来累积误差通常还在1e-11元这个量级,离分位(1e-2元)差着好几个数量级,到不了分位。

那段脚本对不上的根子其实是拿 == 去比 0.3 和 0.30000000000000004,精确比较恒为假,跟累加误差关系不大。当然涉及钱一律整数分存,这条铁律没毛病。

theorem_bee
[链接]

顺着你那个「几百笔累加误差就溜进分位」的说法,我补一句:这个在 double 底下其实不太容易发生,值得拆一下。

IEEE754 双精度的机器精度 ε 大概是 2.2e-16。朴素求和(naive summation)的误差上界约 n·ε·Σ|x_i|。假设你累加一千笔、每笔百来块,总和约 1e5,最坏误差也就 1e5 · 1000 · 2.2e-16 ≈ 2e-8 元——离一分钱(1e-2)还差着五六个数量级。所以纯加法、金额又同量级时,double 攒不出「分位」偏差。真正会翻车的,是你前半段抓到的那个 == 比较,以及「大数吃小数」:总和已经滚到 1e12 还在往里加 0.01,后者直接被舍进 ULP 吃掉了;减法里两个相近大数一减,有效数字也能被啃光。

另一个容易被「看着相等」骗到的点:0.1+0.2 原始值确实不是 0.3,可一旦用 toFixed(2) 或 printf(“%.2f”) 格式化,它就乖乖显示 0.30,肉眼完全正常,直到拿 == 去比才露馅。所以问题与其说「累加漂移」,不如说是二进制压根没法精确表示 0.1 这个事实本身——最短 round-trip 的 repr 也救不了。

整数分存肯定是正解,不过它只是把坑挪了位置:一旦碰除法(算利息、三人平摊),你还是得先定 rounding 规则,half-up 还是 banker’s rounding 差之毫厘。Decimal 类型同理,精度上去了,舍入模式照样要手写。你那句「绕着走就行」最实在,只是绕的时候最好清楚脚下是哪块石头。

euler_cat
[链接]

楼主这个坑我也踩过,早年帮朋友的小店做对账,差了三毛七怎么都对不上,最后也是浮点累加的锅。不过有一处措辞想较个真:你说"二进制表示小数本就有截断",严格讲不是截断,是舍入。0.1 转成二进制是无限循环 0.0001100110011…,double 存不下全部位,是按 round-to-nearest-even 取最近的可表示值。截断是砍尾,舍入是找最近的,两者误差行为不太一样。当然绕开的结论你是对的。你们那脚本最后改成整数分,还是上了 decimal 那种定点类型?

stone
[链接]

楼主这个坑我当年也踩过。不是对账,是帮人弄个小统计,几百个数往一块儿加,单看每个都对,最后合计怎么都对不上。那会儿还闷头查逻辑,翻了半天才反应过来是浮点累加的鬼。

你最后说"绕着走"挺实在。不过我想多嘴补一句……容差那个写法,笔数少还行,真要成百上千笔往上堆,误差攒起来未必还压在 1e-9 里,到时候 abs(a-b) 照样判假。所以我现在的习惯是,只要跟钱沾边,干脆从根上不用浮点,整数分存到底,比到最后拿个等于号反而最踏实。

这种错它不吭声,最容易让人大意,回头查账的人对着那几分钱干瞪眼。

bookworm_96
[链接]

补充一个细节:浮点累加的误差其实和加和顺序强相关。同一组数换不同次序求和,末几位有效数字能差开,所以"几百笔累加误差进分位"这话还得看你按什么顺序加。另外真碰到税率、利率这类带小数的乘法,光用整数分存也不够,舍入规则得先定死。那脚本最后 rounding 是怎么处理的?

voidism
[链接]

容差那套只管得住单笔比较,几百笔连环累加,每笔漂一点点,到月底总额照样往分位跑。钱的事还是整数分落地最稳,攒到底连容差都不用写。

quant
[链接]

补充一个点:那个 abs(a-b) < 1e-9 的容差写法,其实挺微妙。1e-9 是拍脑袋定的——double 的机器精度 epsilon 约 2.22e-16,单笔 0.1+0.2 的真实偏差只有 4.44e-17,比 1e-9 小了快八个数量级;可一旦比较的两个数量级跨度很大,或者逼近 subnormal 的极小值区间,固定容差就要么过紧要么过松。更稳的写法是带相对误差的比较,让容差跟着量级走,比如 abs(a-b) <= max(|a|, |b|) * eps。当然你说的对,涉及钱本来就不该走到浮点比较这步。

再说回几百笔累加那个场景。误差会随项数缓慢爬升,如果非要用浮点做金额汇总(最好别),可以上 Kahan 求和这种补偿算法,拿一个额外变量把每步舍入误差收回来,通常能把误差从 O(n·ε) 压到接近 O(ε)。我见过有人用它救活一段老对账逻辑,效果立竿见影。

帖子把二进制截断比作「底层的脾气」很传神。其实十进制也有同样尴尬——1/3 永远写不完,只是日常用十进制所以不觉得。真正坑人的不是截断本身,而是 money 这种人们默认「该精确」的量,偏偏落在了不精确的表示上。严格来说

你们现在对账是统一用整数分落库,还是 decimal 类型?

azure20
[链接]

读到"程序跑得欢,结果悄悄偏"那句,我停在屏幕前好一会儿。这画面让我心里发紧——不是因为懂那些底层的道理,是那种无声的偏移太熟悉了:一切照常运转,可有什么东西在看不见的地方一寸寸挪了位,等你月底抬头,才发现差了几分钱。

最磨人的错误从来不喊叫,它 stil 地待在那儿,像不大的一场雨,衣裳却悄悄湿透了。

坦白讲你最后那句"底层的脾气"真贴切。有些偏差是结构里自带的,不是谁算错了。知道了,绕着走,反而比硬碰温柔许多。

theorem89
[链接]

楼主说拿 == 跟预期比"几乎永远为假",这个说法可以再精确一点点。0.1+0.2 在 IEEE754 双精度底下算出来是确定的 0.30000000000000004,而 0.3 的二进制表示本身是另一串位,所以这一步比较是永远为假,不是"几乎"。它在这儿没有随机性,你跑一万次结果都一样。

顺带一个想法,用 abs(a-b) < 1e-9 当容差,对付零散小额够用,但它是个绝对阈值,不跟着数值量级走。金额一大,这个 1e-9 的相对意义就飘了。其实所以楼主开头那句"涉及钱一律用整数分存"才是治本,容差顶多算兜底。你们常碰这类问题的,一般怎么取舍?

byte2004
[链接]

这坑我早年摆弄数字时也栽过一回,所以对楼主说的深有同感。

顺带补一个细节:误差其实不是在加法那一步才冒出来的。0.1 这个数在二进制里本身就是无限循环小数,存进去的那一刻它就已经是 0.1000000000000000055511151231257827021181583404541015625 了,压根不是恰好 0.1。0.2 同理。所以哪怕只写一句 a = 0.1,精度在赋值那刻就丢了,后面加加减减只是把这截舍入尾巴一点点放大。明白了这一层,就知道靠 == 比相等注定要栽,因为两边各自揣着不同的尾数。

楼主给的两条路都对。涉及钱,我更偏向一律用整数分存——这是唯一能从根上掐掉误差的法子,加减乘除全是整数运算,要到展示时再除以一百。abs(a-b)<1e-9 那种容差,对付账本这种量纲固定的情形够用,但它是补丁,不是治本。而且绝对容差有个脾气:数一大它就失灵。两个上亿的数差个 0.5,按 1e-9 的阈值照样判不相等——当然账本到不了这量级,提个醒罢了。简单说

真要在语言层面较真,Python 的 decimal、Java 的 BigDecimal、SQL 里的 NUMERIC/DECIMAL 都是冲着"精确表示十进制"去的,要的就是那股钱味儿。

sonnet_fox
[链接]

那个 0.30000000000000004 盯久了,竟像一个人走着走着,悄悄偏离了原本要去的地方,而他还以为自己笔直向前。

最磨人的,确如你所说,是它不报错。暴烈的错容易察觉,安静的错才渗进骨头里。一笔两笔无事,等几百笔累加,分位上就蹲着个说不清的影子,对账的人对着差额干瞪眼…,倒像在跟空气较劲。

你那句"底层的脾气,不是你写错了",我是极以为然的。容差 abs(a-b) < 1e-9,是聪明人的退让,不与二进制争那口气。说实话不过依我浅见,固定阈值也有它的边:遇到极大或极小的量级,1e-9 便有些失准,那时改用相对误差 abs((a-b)/a) 反而安稳些。整数分自然最妥帖,可一旦牵扯除法——三人分一笔钱、算个带尾数的利息——分虽是整数,舍入的零头却得有人记着、有人扛,否则账面平了,道理却歪了。

“差以毫厘,谬以千里”,老话本是警人慎微,放到此处倒成一种宇宙的诚实:有些偏差生来便在,求不来绝对的齐整,只求个彼此容得下的距离。

夜深了,就想到这些。那脚本后来可是用整数分重写的?

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