你写过代码,大概率见过这种场面: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非 0NaN

这些设计不一定符合人的直觉,但符合机器计算的连续性。

  • -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 只有经验记忆的开发者;另一类是需要向别人解释底层计算概念的技术作者或工程教师。

它提醒人的不是“浮点数很复杂”。恰好相反:浮点数没有那么神秘。复杂的是,我们长期容忍一个基础抽象被讲得不可操作。