← 实验室
ASTERXOR / 实验室

为什么电脑有时连简单算术都“不认账”

有限的二进制数装不下所有十进制小数。知道自己选了哪种运算,小误差就更容易理解。

一排蓝点中,两个相邻点被突出显示。
局部数值网格的抽象图;真实间距随数值量级改变。
实验室ASTERXOR / LEON LEUNG

在本地Python实验中,0.1 + 0.2并不精确等于0.3。

打印结果是0.30000000000000004,看起来像基础算术出错。实际发生的是:机器把两个已存近似值相加,再舍入到另一个可表示数。屏幕上的十进制写法藏住了底层二进制网格。理解网格,比认定电脑不会算数更有用。

屏幕上的数字,可能只是附近某个值的简短名字。
THE REPRESENTATION

有些分数在二进制里写不完

十进制中的三分之一需要无限循环展开。二进制也有写不完的分数:十分之一不能写成有限个可用二次幂之和。常见Python浮点使用binary64,尾数和指数都有有限空间,0.1和0.2必须舍入到附近可表示值。

字面量0.3另外舍入。把已存的0.1和0.2相加后,结果落在已存0.3上方的相邻可表示数。本次差约5.5511 × 10^-17,虽然微小,精确相等比较仍能发现。

加法结果比字面量0.3高一个可表示步长。
FIG 01以已存浮点0.3为基准,用相邻数间距表示偏移。
THE PRINTED VALUE

格式化不会修复底层数值

Python常规显示选用能转换回同一存储值的简短十进制写法。因此,看见0.1不证明机器存了精确的有理数十分之一。减少显示位数可以让和看起来像0.3,却没有改变底层数值。

比较结果时就要分清这个区别。对于近似测量或数值计算,明确选择的容差可能比精确相等更符合问题。但容差需要单位和情境:适合巨大工程量的阈值,可能完全不适合微弱信号;接近零时,只用相对容差也需谨慎。

A MOVING GRID

数值越大,格子越宽

相邻浮点数间距随量级增大。1附近约2.2204 × 10^-16,10^16附近则为2。在后者加1,会落在相邻可表示数之间,本例的舍入把10^16 + 1又送回10^16。

所以用这些浮点运算计算(10^16 + 1) - 10^16,得到0而非1。减掉大数无法找回之前已经丢失的小贡献。这里不是多一个尾数位,而是预期答案整个消失,尽管每一步都遵守表示规则。

KEEP SMALL TERMS

加法顺序和方法都重要

脚本还用math.fsum处理[10^16, 1, -10^16],结果为1。它比简单连续相加更仔细地保留信息。若把同样数值表示为Python整数,也能精确求和为1,因为整数不受固定53位有效数字限制。

这不代表一个求和函数解决所有浮点问题。之前的舍入、接近的近似值相减、溢出和算法结构都可能重要。本例只指出一种丢失,以及在这里避开它的方法。数值可靠性属于整个计算过程,而非最后显示几位。

朴素浮点相加丢失小项,math.fsum与整数运算保留它。
FIG 02序列包含两个抵消的大值和一个大小为1的小贡献。
CHOOSE THE MODEL

十进制输入可以提出不同问题

用字符串“0.1”和“0.2”构造Decimal,在当前十进制精度设置下得到精确“0.3”。字符串很重要:若从已经舍入的float构造Decimal,会连同二进制近似一起保留。正确表示从输入边界就开始了。

Decimal也有有限精度上下文,并不能精确表示所有除法。选择取决于要保留的数量和规则:计数适合整数,明确的十进制规则适合十进制表示,许多近似计算则适合二进制浮点。理解取舍,比要求一种数值类型包办一切更实际。

留一个想法带走

值得问的不是算术有没有用,而是算术当时究竟能使用哪些数。