0.1 + 0.2 ≠ 0.3。这个等式在小学数学课上会被打叉,但在计算机里它是对的。
2019年,一位开发者决定亲手实现浮点运算。他以为这是"给够时间就能搞定"的问题——毕竟浮点无处不在,能有多难?
结果他遭遇了"职业生涯最彻底的技术溃败"。五年后他卷土重来,这次带了纸笔,关掉电脑,花了10天人工推演。他的结论很扎心:真正理解浮点的人只有3种——硬件工程师、标准委员会成员,以及"被它折磨过的人"。
第一章:我们所谓的"会用",其实是幻觉
作者复盘败因时提到一个致命错觉:把"能调用浮点API"当成了"理解浮点"。这就像会开车的人以为自己懂内燃机——大多数人确实不需要懂,直到引擎在高速上突然熄火。
浮点的表面规则很简单。IEEE 754单精度格式把32比特切成三块:1位符号(S)、8位指数(E)、23位尾数(T)。公式写成 (−1)^S × 2^(E−127) × (1 + T/2^23),看起来像是高中数学能搞定的事。
但魔鬼藏在"规范化"里。为了让每个数有唯一表示,IEEE 754规定尾数必须满足 1 ≤ 1 + T/2^23 < 2,也就是隐含那个前导1。这个设计省了一位存储,却引入了第一个认知陷阱:你以为存的是0.1,实际存的是最接近0.1的二进制分数。
作者用10天纸笔推演后发现,自己过去五年的"浮点经验"几乎全是黑箱操作。调用sqrt()和知道平方根怎么被逐比特逼出来,隔着一整座计算机体系结构的冰山。
第二章:那些"不可能"的bug,都是 feature
浮点最让人崩溃的特性,恰恰来自它的设计优点。
比如NaN(非数)的传播机制。按照标准,任何涉及NaN的操作都返回NaN,除非特别说明。这听起来合理——直到你的金融系统在链条第七层突然吐出一堆NaN,而源头是三个月前某次除零被静默处理。
作者提到IEEE 754的" holy grail"地位:它用 excruciating detail(令人发指的细节)规定了行为,让不同硬件能给出一致结果。但这种一致性是有代价的。为了保证可移植性,标准牺牲了一些直觉——比如为什么浮点异常默认不触发中断,而是静默返回特殊值?
答案藏在历史里。1985年标准制定时,硬件浮点单元还是奢侈品。大多数系统用软件模拟,而软件处理中断的成本高到无法接受。于是"静默失败"成了默认,这个临时方案沿用至今,成了无数调试噩梦的源头。
作者的一个发现很有意思:IEEE 754的"正确"是数学正确,不是人类正确。0.1在十进制是有限小数,在二进制却是无限循环。标准选择了"最近可表示值"的舍入策略,这意味着每次十进制转浮点,你都在接受一个精心计算的谎言。
第三章:从"被它打败"到"与它共处"
五年后的复仇并没有带来胜利。作者说这次他"aim to deeply grasp",但读完整个系列你会发现,深度理解带来的不是掌控感,而是敬畏。
他花了大量篇幅处理一个被大多数教程跳过的问题:浮点的"异常"到底有多少种?IEEE 754定义了5种:无效操作、除零、上溢、下溢、不精确。但现代语言和运行时往往只暴露其中2-3种,剩下的被默默吞掉。
这种简化是进步还是隐患?作者没有直接回答,但他记录了一个细节:当他终于手工算出某个边界条件下的舍入结果,和硬件输出逐比特比对成功时,"没有喜悦,只有后怕"——如果差一个比特,整个金融系统的对账就会崩掉。
文章结尾,作者列了一个"推荐阅读配乐":微分音数学摇滚(microtonal math rock)。这种音乐故意打破十二平均律,在钢琴缝之间找音符。他说这很配浮点——我们都在用离散的台阶,逼近一个连续的世界。
那么回到开头的问题:0.1 + 0.2 到底等于多少?在你的Python解释器里敲一下,然后把那个长长的尾数,和IEEE 754标准文档的舍入规则对照看看——你会发现,连"错误"都是被精确设计过的。
作者最后留了一个开放性的观察:他最初以为理解浮点需要数学天赋,五年后发现需要的是"愿意和枯燥共处的时间"。在这个追求即时反馈的行业里,这可能是最反直觉的结论。你的上一次深度技术学习,花了多少天关掉IDE只用纸笔?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.