Python math.fsum:大数已经抵消,为什么普通累计仍丢掉最后一点残差

昨天 3阅读

五个浮点数包含一对大数、一对正负一,以及一个很小的剩余量。逐步累加得到负一,内置sum得到零,math.fsum却保住了那个小量。同一批输入产生三种结果,关键在于中途丢掉的低位是否仍有地方保存,以及后来大数抵消时能否再参与求和。

本例在Linux、CPython 3.12.14实跑,无第三方依赖,只做有限的内存计算。保存为demo.py,运行python3 demo.py。Python 3.12已改进浮点sum算法,所以本文特意将它与普通循环分别打印,没有把两种累加过程视为同一个实现。

Python math.fsum:大数已经抵消,为什么普通累计仍丢掉最后一点残差

AI模型生成概念插图:大块互相平衡之后,小珠仍需专门收集才能保住;不是数值计算软件截图。

完整程序与本地结果

完整可运行程序

import math
from fractions import Fraction

values = [1e16, 1.0, 1e-16, -1e16, -1.0]
naive = 0.0
for value in values:
    naive += value
ordinary = sum(values)
accurate = math.fsum(values)
reference = float(sum(Fraction.from_float(value) for value in values))
assert naive == -1.0
assert ordinary == 0.0
assert accurate == reference == 1e-16
print("step-by-step:", naive)
print("built-in sum:", ordinary)
print("math.fsum:", accurate)
print("exact-input reference:", reference)

rounded_first = [1e16 + 1.0, -1e16]
assert rounded_first[0] == 1e16
assert math.fsum(rounded_first) == 0.0
print("already rounded input:", math.fsum(rounded_first))
print("empty fsum:", math.fsum([]))
assert math.fsum([]) == 0.0

try:
    math.fsum([1e308, 1e308, -1e308])
except OverflowError:
    print("intermediate overflow: OverflowError")
else:
    raise AssertionError("this input must overflow on this runtime")

本次实际输出(以下为结果,不是程序)

step-by-step: -1.0
built-in sum: 0.0
math.fsum: 1e-16
exact-input reference: 1e-16
already rounded input: 0.0
empty fsum: 0.0
intermediate overflow: OverflowError

先固定输入,再比较累计过程

values中的大数互相抵消,一与负一也互相抵消,按已进入内存的数值精确相加,剩下的是那个很小的浮点输入。普通循环每一步只保留一个浮点累计值,正一与更小的量先后加到大数上时可能丢失,最后留下了不应独自保留的负一。

内置sum在当前版本输出零,比这个循环有所改善,却仍没有保住最小残差。fsum通过跟踪多个中间部分和减少精度损失,本例输出1e-16。此结果是固定输入在本机运行的证据,不能扩展为每一组数据都必定与sum不同。

参考答案应来自同一份浮点输入

程序用Fraction.from_float将每个现有float精确转换成有理数,再完成精确求和,最后只转换一次回float。这样得到的reference与fsum一致。这个对照检查的是求和过程,而不是假设十进制字面量在读入时都获得了数学上完全精确的表示。

如果改用十进制字符串直接构造分数,参考问题就会发生变化:比较的是原始十进制理想值,而不是程序真正收到的二进制浮点输入。排查数值差异时,先说明参考答案对应哪一层,才能区分输入表示误差与中途累计误差。

已经丢掉的信息不能在求和末尾补回来

rounded_first先计算1e16加一,结果已被舍入为1e16。随后把它和负大数交给fsum,只能得到零。fsum没有收到独立的一,因此无法知道此前还有一个小量被舍去。这说明把所有求和函数替换掉,并不能自动修复前面的乘除或预聚合误差。

实际管线若先按批次使用精度不足的方法汇总,再把各批结果交给fsum,总结果仍然可能缺少信息。应检查原始项目如何进入累计器,尤其是各分组已经发生的舍入;必要时让更准确的累计覆盖真正需要保证的完整范围。

准确求和仍有类型与范围边界

空输入得到0.0,说明结果仍是浮点数。程序另用三个接近上限的大浮点数触发中间溢出,即使数学上的最终结果仍可能位于范围内,当前执行也会抛OverflowError。不能将更准确求和理解成任意精度或无限范围的数值系统。

官方文档还提示底层浮点实现可能影响最低有效位。涉及跨平台严格验收时,应记录解释器、平台与输入,并依据实际需求定义允许误差。本文的断言用于复现这一明确环境下的例子,没有声称所有实现均逐位给出相同结果。

在大数抵消的任务中建立对照

许多测量修正、余额差值或误差汇总同时包含正负项,累计总量可能很大,最终剩余量却很小。这时可以先抽取一组可公开的小输入,分别比较循环、sum与fsum,再用精确参考确认偏差在哪里产生,避免仅凭输出更接近零就判断更正确。

对于真正要求十进制精确规则的业务,应先选择匹配的数据表示与舍入策略,而不是把浮点函数当作通用补救。已经是整数的精确计数也不需要先转换成float再求和,转换反而可能引入新的精度边界。

验收时同时保留正常与失效样本

除了本文的抵消例,还应准备量级相近的普通数据、全部为空、已经舍入的输入以及预期溢出的输入。前两类证明常规出口,后两类明确能力边界。结果中保留异常类别,比把失败统一写成零更容易发现数据问题。

性能测试也应在正确性约定之后进行。精确参考使用分数只是为了验证这五个固定输入,不代表应把大型生产管线全部改成分数运算。先确定误差会影响哪项业务决定,再选择合适的累计方法与验证成本。

参考资料

资料核验日期:2026年10月2日。以上输出来自固定输入的本地实跑,程序退出码为0。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。