你写过代码,大概率见过这种场面:0.2 看起来就是 0.2,进了 float,出来却可能是 0.20000000298023223876953125。
很多人第一反应是:语言有坑,CPU 有坑,浮点数有坑。
Bartosz Ciechanowski 新发的长文《Exposing Floating Point》和配套网站 float.exposed,正是冲着这个误会来的。它不把浮点数讲成黑箱,而是把每一位摊开:符号位、指数、尾数、舍入、+0/-0、infinity、NaN,全都能看见。
这篇文章讨论的对象很明确:IEEE 754 binary16、binary32、binary64,也就是工程里常说的 half、float、double。它不是在解释所有数字系统,也不是在说所有语言标准都强制这么做。比如 C/C++ 标准并不技术性要求 float/double 必须使用 IEEE 754,只是现实设备里,这已经是事实上的主流。
它把浮点数讲成了可检查的位
这件事先压成一张卡片:
| 你关心什么 | 关键信息 | 我的判断 |
|---|---|---|
| 文章讲什么 | 从十进制、二进制科学计数法讲到 IEEE 754 编码 | 不靠口诀,回到规则 |
| float.exposed 是什么 | 一个可交互查看浮点数位级结构的网站 | 把抽象规则变成可检查对象 |
| 讨论范围 | binary16 / binary32 / binary64 | 不要泛化成所有数字表示 |
| 核心矛盾 | 有限位数表示无限实数集合 | 误差不是失败,是代价 |
float 的结构尤其值得记住。
它一共 32 位:1 位符号,8 位指数,23 位显式尾数。规范化二进制科学计数法里,非零数总是以 1 开头。这个开头的 1 可以不存,计算时再补回来。
所以 float 的有效精度不是 23 位,而是 24 位。
这点很容易讲错。
浮点数本质上就是二进制科学计数法,只是加了两道硬限制:尾数位数有限,指数范围有限。以 float 为例,常规指数范围大致是 -126 到 +127。数太大,装不下;太小,也装不下。
0.2 的问题就在这里。
它在二进制里不是有限展开,而是循环展开。float 只有 24 位有效精度,后面的位只能舍入。于是它不能精确表示 0.2,只能在相邻可表示数里选一个更合适的。
这不是 JavaScript 的错,也不是 Python 的错,更不是 IEEE 754 的失败。十进制里的 1/3 写不完,大家很容易接受;二进制里的 0.2 写不完,很多人就开始怀疑人生。
关键限制也要说清。不是所有十进制小数都不能被二进制精确表示。能不能精确,取决于它的二进制展开是否有限。0.5 可以,0.25 可以,0.2 不行。
特殊值不是补丁,是编码空间的安排
IEEE 754 里那些看起来怪的东西,比如 +0、-0、infinity、NaN,也不是拍脑袋塞进去的补丁。
它们来自保留的指数编码。
以 float 为例,8 位指数有 256 种编码。常规数字用掉中间部分。指数全 0 和全 1 被拿来处理特殊情况。
| 指数编码 | 尾数 | 含义 |
|---|---|---|
| 全 0 | 全 0 | +0 或 -0 |
| 全 0 | 非 0 | 非规范化数 |
| 全 1 | 全 0 | 正负无穷 |
| 全 1 | 非 0 | NaN |
这些设计不一定符合人的直觉,但符合机器计算的连续性。
- -0 能保留“从负方向下溢到 0”的信息。
- infinity 让溢出和某些除零结果仍有定义。
- NaN 给“这个结果不是正常数”留了位置。
好的标准,往往不是让世界干净,而是让脏东西有地方放。
float.exposed 的价值就在这里。它不是再写一篇“浮点数入门”,而是把藏在编译器、CPU、运行时和教材脚注里的规则,放到屏幕上。你可以改一位,看指数怎么变;改尾数,看相邻可表示数怎么挪;切到特殊编码,看 NaN 和 infinity 为什么出现。
位级结构一旦可视化,很多争论会自动变短。
它也有边界。可视化能帮你理解表示和编码,但不会替你决定业务该用 float、double、decimal 还是整数分。金融计算、计费系统、库存结算这种场景,真正该问的不是“浮点数准不准”,而是“误差能不能进入业务账本”。很多时候答案很简单:不能,就别用二进制浮点数直接记钱。
真正欠账的是工程表达
我更在意的不是这篇文章又科普了 IEEE 754,而是它暴露了工程教育里的老毛病:底层规则天天在用,却长期被讲成玄学。
很多开发者知道“浮点数不准”,但不知道“不准”具体来自哪里。于是经验变成口诀,口诀变成恐惧。最后只剩一句模糊建议:别直接比较浮点数。
这句话没错,但太偷懒。
更有用的表达应该是:哪些十进制小数能被二进制有限表示,哪些不能;有效精度有多少;指数范围在哪里;舍入发生在什么位置;特殊值如何编码;哪些优化可能改变严格语义。
前者让人背禁忌。后者让人做判断。
对写过代码、但只靠经验处理 float 的开发者,这件事的动作很具体:
- 比较浮点数时,用容差,不要拿
==赌运气。 - 调试异常结果时,先看表示范围、舍入和特殊值,不要先怪语言。
- 做金额、积分、账务、库存时,优先考虑整数分、定点数或 decimal,而不是把 double 当万能容器。
对技术作者和工程教师,动作也很具体:少讲“浮点数很复杂”,多让读者看见一串 bit 怎么变成一个数。把 0.2、有效精度、指数保留编码讲透,比堆十条禁忌更管用。
“工欲善其事,必先利其器。”这里的器,不只是编译器和调试器,也是解释工具。float.exposed 做对的地方,是把 IEEE 754 从标准、教材、Stack Overflow 问答,变成普通工程师可以直接检查的对象。
技术史里这种事并不少见。电力进入工厂以后,真正改变生产的,不只是发电机,还有电表、保险丝、开关和布线规范。不完全一样,但道理相通:抽象要进入日常,必须长出可观察的界面。
浮点数也是一样。
模型越底层,越不能只靠敬畏维持秩序。你越把它讲得神秘,工程现场越会用迷信填空。一个好可视化工具的意义,就是把“听说如此”变成“原来如此”。
接下来最该看的,也不是 float.exposed 能不能变成热门网站。真正值得看的是,这类解释方式能不能进入日常教学、代码评审和调试流程。只要浮点数还停留在“别问,记住就行”,同样的坑就会换着语言继续出现。
这篇文章适合两类人:一类是写过代码、但对 float 只有经验记忆的开发者;另一类是需要向别人解释底层计算概念的技术作者或工程教师。
它提醒人的不是“浮点数很复杂”。恰好相反:浮点数没有那么神秘。复杂的是,我们长期容忍一个基础抽象被讲得不可操作。
