Skip to content
返回博客
教程

浮点数精度丢失:为什么 0.1 + 0.2 不等于 0.3

浮点数精度丢失是怎么让 0.1 + 0.2 变成 0.30000000000000004 的?讲透 IEEE 754 舍入规则,以及在 JS、Python、SQL 中安全比较浮点数的方法,附免费在线转换器。

12 分钟阅读

浮点数精度完全指南:为什么 0.1 + 0.2 ≠ 0.3

0.1 + 0.2 的结果是 0.30000000000000004,因为二进制浮点数里既不存在 0.1,也不存在 0.2。double 只能保存 m × 2ⁿ 形式的值,而十分之一在二进制下是无限循环小数,硬件只好退一步,存下最接近的那个可表示邻居。0.1 背后的 double 精确等于 0.10000000000000000555111512312578270211815834045410156250.2 背后的是 0.200000000000000011102230246251565404236316680908203125。把这两个已经存进去的值相加,精确和落在两个可表示的 double 之间,IEEE 754 舍入到更近的那一个,而它比 0.3 高出一丝。

答案就这么简单。浮点数精度(floating point precision)是有限的,十进制小数很少能落在二进制网格上,而每一次运算都会再舍入一次。平时说 0.1 + 0.2 不等于 0.3,指的就是这件事。

这不是 JavaScript 的怪癖,也不是 CPU 的缺陷。Python、Java、C、Go、Rust、Swift 以及 SQL 的 FLOAT 列都会给出同样的结果,因为它们跑的都是 IEEE 754。把任意数值粘进免费的 IEEE 754 浮点数转换器,就能逐位看到它的比特和精确存储值。

剩下的都是细节:误差从哪里来,你的语言为什么有时把它藏起来,不用 == 该怎么比较浮点数,以及当精度直接关系到钱的时候该选哪种数值类型。

60 秒讲清浮点数精度

几乎所有的精度问题都能归到下面三件事上:

  1. 二进制浮点数只能表示 m × 2ⁿ 形式的数,也就是若干个 2 的幂之和。
  2. 0.10.20.3 这类十进制小数不是这种形式,所以在写入时就已经被舍入了。
  3. 每一次算术运算,都会把结果再一次舍入到最接近的可表示值。
十进制值能否精确表示原因
0.52⁻¹
0.252⁻²
0.752⁻¹ + 2⁻²
2.52 + 2⁻¹
1002⁵³ 以下的任意整数
0.1不能1/10,分母含因子 5
0.2不能1/5,同样的问题
0.3不能3/10,同样的问题

一条经验法则:把分数约到最简,分母若是 2 的纯幂,这个值就是精确的。只要还剩下因子 5,它就只能被近似存储。

为什么 0.1 无法被精确存储

二进制小数点后的每一位分别代表 1/2、1/4、1/8、1/16、1/32……用它们去凑 0.1,永远凑不出来。1/16 = 0.0625,加上 1/32 得 0.09375,再加上 1/256 得 0.09765625,越来越近,却始终不精确。在二进制里,十分之一写作 0.0001100110011…0011 无限循环下去。

十进制也有同样的毛病,只是换了一批分数。1/3 在十进制下是 0.333…,任何有限位数都写不准它。没人会说这是十进制的 bug。二进制只是把这条界线画在了别处,而 1/10 恰好落在了不利的一侧。Python 官方文档的《浮点数算术:问题与限制》对同一个展开过程有更详尽的推导。

二进制不过就是 2 进制,十六进制和八进制背后是同一套位值算术。进制转换器能展示一个值在不同进制之间如何变换,我们的进制转换完全指南则深入讲了整数那一侧。小数才是二进制开始让人头疼的地方。

double 到底存了什么

64 位 double 分成三段:1 位符号位、11 位阶码、52 位尾数。尾数带一个从不存储的隐含前导 1,所以实际有 53 位有效数字,约合 15.95 位十进制的浮点数精度。阶码采用 1023 的偏移量存储。

让任何语言打印出比默认更多的位数,近似值就露面了:

>>> 0.1
0.1
>>> f"{0.1:.20f}"
'0.10000000000000000555'
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
(0.1).toFixed(20);      // '0.10000000000000000555'
(0.1).toPrecision(20);  // '0.10000000000000000555'

Decimal(0.1) 是最诚实的视角:它把你手里已有的那个 double 直接转换,不再重新舍入,把全部 55 位数字都打印出来。IEEE 754 浮点数转换器的精度面板会对你输入的任意数字打印同样的展开,旁边还标出相对于输入值的带符号舍入误差。

0.30000000000000004 背后的精确算术

结果偏偏落在这个数上,而不是附近别的某个可表示值,这一步是可以算出来的。把两个存储值精确相加,不做任何舍入,得到:

0.1 →  0.1000000000000000055511151231257827021181583404541015625
0.2 →  0.200000000000000011102230246251565404236316680908203125
sum →  0.3000000000000000166533453693773481063544750213623046875

这个和本身并不是一个可表示的 double。它落在二进制网格上两个相邻值之间:

below:  0.299999999999999988897769753748434595763683319091796875   (prints as 0.3)
above:  0.3000000000000000444089209850062616169452667236328125     (prints as 0.30000000000000004)

量一下间隔:精确和比下方邻居高 2⁻⁵⁵2.776e-17,比上方邻居低的也是 2⁻⁵⁵。两边一样近,它正好落在中点上,是一次不折不扣的平局。

舍入到最近值在这里没有距离可依据,于是 IEEE 754 启用它的决胜规则:向偶数舍入(round-half-to-even),也就是挑尾数最后一位为 0 的那个候选。下方邻居的有效数字是 5404319552844595,是奇数;上方邻居是 5404319552844596,是偶数。偶数的那个胜出,得到位模式 0x3FD3333333333334,比字面量 0.3 所映射的 double 高一个 ULP,打印出来正是 0.30000000000000004

为什么偏爱偶数?如果每次遇到中点都往上舍,长串求和就会朝一个方向系统性偏移。交替倒向偶数邻居,能让这种漂移在大量运算之后仍然接近零。这就是会计常用的银行家舍入,只不过被写进标准、做进了硅片。互联网上最出名的那个浮点舍入误差之所以以 ...04 收尾,原因就在这里。

对比一个完全不需要舍入的求和:

0.5 + 0.25 === 0.75;   // true
0.1 + 0.2 === 0.3;     // false

0.50.250.75 分别是 2⁻¹、2⁻² 和 2⁻¹ + 2⁻²。三个都精确,它们的和也精确,相等判断的表现和小学算术给你的直觉完全一致。转换器的邻居面板会显示你输入值前后的可表示值以及 ULP 间隔,你可以自己看看 0.3 附近的网格长什么样。

你的语言为什么有时把误差藏起来

让整件事显得像闹鬼的正是这一点:0.1 打印出来是 0.1,而 0.1 + 0.2 打印出十七位。同一种存储格式,输出却天差地别。

现代运行时用的是**最短往返(shortest round-trip)**格式化:输出能被解析回同一个 double 的最短十进制字符串。"0.1" 已经能唯一往返到 0.1 所存的那个值,所以你看到的就是它。而 0.1 + 0.2 得到的 double 和 "0.3" 映射到的不是同一个,于是格式化器必须不断加数字,直到字符串不再有歧义,而这需要整整十七位。这条技术脉络始于 David Gay 1990 年的正确舍入工作,随后 Grisu 和 Ryu 让它快到足以进入每一个标准库。你的语言没有撒谎,它给出的是能唯一标识该值的最短标签。

各语言的表现

语言默认浮点类型0.1 + 0.2 打印结果精确十进制方案
JavaScriptnumber(只有 double)0.30000000000000004内置没有——用整数分或第三方库
Pythonfloat(double)0.30000000000000004decimal.Decimalfractions.Fraction
Javadouble0.30000000000000004BigDecimal(从字符串构造)
C#double0.30000000000000004decimal——128 位,十进制
Gofloat640.30000000000000004(需经变量)math/big.Rat
Rustf640.30000000000000004rust_decimal crate
SQLFLOAT / DOUBLE PRECISION0.30000000000000004NUMERIC / DECIMAL

其中有两行藏着陷阱。

Go 会以任意精度折叠常量。字面量表达式在编译期被精确求值,之后才做转换,所以常量 0.1 + 0.2 在变成 float64 之前就已经精确等于 0.3

package main

import "fmt"

func main() {
	fmt.Println(0.1 + 0.2) // 0.3   — constant folded exactly, then converted
	a, b := 0.1, 0.2
	fmt.Println(a + b)      // 0.30000000000000004
	fmt.Println(a+b == 0.3) // false
}

Java 的 BigDecimal 一旦喂给它 double,就会连误差一起继承。构造函数会忠实地转换它拿到的每一个比特:

System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625

System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
// 0.3

SQL 的差异不在方言,而在列类型:

-- PostgreSQL: a bare decimal literal is NUMERIC, which is exact
SELECT 0.1 + 0.2 = 0.3;                          -- t

-- Cast to binary floating point and equality fails
SELECT 0.1::float8 + 0.2::float8 = 0.3::float8;  -- f

-- SQLite: REAL is a double, and the printed value hides it
SELECT 0.1 + 0.2;        -- 0.3
SELECT 0.1 + 0.2 = 0.3;  -- 0  (false)

盯着 SQLite 这两行多看一会儿。打印结果说是 0.3,比较结果却说不是,而两者都没错。

比较浮点数:为什么 == 会失败,以及 Number.EPSILON 不是容差

0.1 + 0.2 === 0.3 为假,因为左边是另一个 double。常见的建议是改用容差比较,而流传最广的那个版本恰恰是错的:

Math.abs(a - b) < Number.EPSILON;   // ⚠️ not a general-purpose comparison

Number.EPSILON 等于 2.220446049250313e-16,也就是 2⁻⁵²。MDN 的定义是:1 与大于 1 的最小 double 之间的差值。这句定义的重点在「1」:它量的是 1.0 处的网格间距,不是通用的误差预算。浮点数的间距每跨过一个 2 的幂就翻倍,所以离开被量的那个邻域,固定阈值到哪儿都是错的。

在 1.0 附近它碰巧管用:

Math.abs((0.1 + 0.2) - 0.3);                  // 5.551115123125783e-17
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON; // true

把同一个计算放大十亿倍,它立刻就崩了:

const a = (0.1 + 0.2) * 1e9;
const b = 0.3 * 1e9;

a === b;                          // false
Math.abs(a - b);                  // 5.960464477539063e-8
Math.abs(a - b) < Number.EPSILON; // false — yet a and b are adjacent doubles

1e9 附近,相邻 double 之间的间距大约是 1.19e-7。由于 1e9 落在 [2²⁹, 2³⁰) 区间内,那里的 ULP 是 2²⁹⁻⁵² = 2⁻²³ ≈ 1.19e-7,比 Number.EPSILON 大了约 5 亿倍。在这个量级上,任何真实的舍入误差都远远超过阈值,于是判断永远返回 false。而到了 1e-20 那一端,同一个常量又宽松得离谱,会把明显不同的值判成相等。

绝对容差、相对容差与 ULP 距离

三种做法,适用的场合并不一样:

  • 绝对容差|a − b| <= atol。事先知道数量级时它是对的,而且只有它能拿来和零比较,因为零附近的相对容差恒等于 0。
  • 相对容差|a − b| <= rtol × max(|a|, |b|)。能随数据跨数量级缩放,但在零附近退化。
  • ULP 距离:两个位模式之间隔着多少个可表示值。最精确,也最难读。

把前两者结合起来,就得到了所有正经数值库都在用的形式:

function nearlyEqual(a, b, rtol = 1e-9, atol = 1e-12) {
  if (a === b) return true;            // handles Infinity === Infinity
  const diff = Math.abs(a - b);
  return diff <= Math.max(rtol * Math.max(Math.abs(a), Math.abs(b)), atol);
}

nearlyEqual(0.1 + 0.2, 0.3);                  // true
nearlyEqual((0.1 + 0.2) * 1e9, 0.3 * 1e9);    // true
nearlyEqual(0, 1e-15);                        // true
nearlyEqual(1, 1.0001);                       // false

Python 标准库里就带了这个函数,NumPy 的 allclose 用的也是同一个公式:

import math
math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9, abs_tol=1e-12)  # True
math.isclose(1e9, 1e9 + 1e-7, rel_tol=1e-9)                # True

想要严格版本,就把比特映射成单调的整数序,然后相减:

const view = new DataView(new ArrayBuffer(8));

function toOrdinal(x) {
  view.setFloat64(0, x);
  const i = view.getBigInt64(0);
  return i >= 0n ? i : -(1n << 63n) - i;   // make negatives order correctly
}

function ulpDistance(a, b) {
  const d = toOrdinal(a) - toOrdinal(b);
  return d < 0n ? -d : d;
}

ulpDistance(0.1 + 0.2, 0.3);                // 1n
ulpDistance((0.1 + 0.2) * 1e9, 0.3 * 1e9);  // 1n
ulpDistance(-0, 0);                         // 0n

顺带一提,精确的 == 也不是永远都错。对于 2⁵³ 以下的整数值 double、对于你赋值之后从未参与运算的常量、以及判断一个值是否精确为零,它都没问题。

当精度关乎金钱:选对数值类型

货币和二进制浮点数是一对坏搭档,原因不在于单次误差有多大。这些误差是系统性的、可复现的,而且在审计人员发现之前一直隐形:

19.99 * 100;              // 1998.9999999999998
Math.round(19.99 * 100);  // 1999

(1.005).toFixed(2);       // '1.00'  — 1.005 is stored as 1.00499999999999989...

第二个例子每年都要坑倒一批团队。toFixed 并没有舍错,是交到它手里的那个值本来就小于 1.005。

整数最小单位

存分,不要存元。1999 表示 19.99 美元,算术全跑在整数上,只在展示的那一刻才做除法。

const priceCents = 1999;             // $19.99
const subtotal = priceCents * 3;     // 5997 — exact

const totalCents = 1000;             // $10.00 split three ways
const share = Math.floor(totalCents / 3);   // 333
const remainder = totalCents - share * 3;   // 1 cent to allocate

有两点要注意。超过 Number.MAX_SAFE_INTEGER9007199254740991,即 2⁵³ − 1)之后,JavaScript 的整数就不再精确,这时要换 BigInt。另外,整数并不能替你决定舍入策略:除法、百分比和税额分摊,仍然需要一条明确的规则来决定余下的那一分钱归谁。

十进制类型

十进制类型(decimal)存的是十进制数位,所以十进制小数能精确落位。构造时一定要用字符串,绝不要用浮点数,否则你还没开始就已经把二进制误差继承了过来:

from decimal import Decimal

Decimal("0.1") + Decimal("0.2") == Decimal("0.3")   # True
Decimal(0.1)                                        # 0.1000000000000000055511151231257827021181583404541015625
Console.WriteLine(0.1m + 0.2m);          // 0.3
Console.WriteLine(0.1m + 0.2m == 0.3m);  // True

Java 有 BigDecimal,C# 有原生的 128 位 decimal,SQL 有 NUMERIC(12,2)。它们都能修好十进制小数,却没有一个能修好 1/3,因为十进制类型仍然是一种浮点格式,它只是把基数从 2 换成了 10。

决策矩阵

使用场景推荐类型原因
金额、账单、税务整数最小单位或十进制类型十进制算术精确,舍入可审计
科学计算、物理double(FP64)15–16 位绰绰有余;库支持最好
几何、图形float 或 double + 容差本来就是近似;用 epsilon 比较
机器学习训练bfloat16FP32 的范围,一半的内存
机器学习推理、存储FP16数值已归一化时能多留几位尾数
计数器、ID、主键64 位整数或 BigInt它们本就不该放进浮点数

误差累积与灾难性抵消

一次舍入误差是 1e-17,无关痛痒。一万次就成了一张工单:

let total = 0;
for (let i = 0; i < 10000; i++) total += 0.1;

total;         // 1000.0000000001588
total - 1000;  // 1.588205122970976e-10

漂移大致随运算次数增长。在这种规模的循环里它看不出来;在每晚对数百万行做聚合时就不一样了。

灾难性抵消

更麻烦的失效来自减法。两个几乎相等的数相减,相同的高位数字全被抵消掉,剩下的结果由此前已经累积的误差主导。绝对误差并没有变大,但相对误差爆炸了,因为结果已经变得极小,而误差还是原来那么大。

教科书上的单遍方差公式 E[x²] − (E[x])² 会一头撞进这个坑:

const data = [1e9, 1e9 + 1, 1e9 + 2];
const mean = data.reduce((s, x) => s + x, 0) / data.length;

// One-pass: E[x²] − (E[x])²
data.reduce((s, x) => s + x * x, 0) / data.length - mean * mean;  // 0

// Two-pass: centre the data first
data.reduce((s, x) => s + (x - mean) ** 2, 0) / data.length;      // 0.6666666666666666

真实的总体方差是 2/3。单遍公式返回的是精确的零:两个操作数都在 1e18 量级,它们之间的差值躲在最后一个存储位之下,于是答案整个错了,而不只是偏了一点。Welford 的在线算法通过增量更新均值和平方偏差和来绕开这个问题。

b² ≫ 4ac 时,二次方程求根公式有同样的弱点,修法也是同一路子:

const a = 1, b = 1e8, c = 1;
const disc = Math.sqrt(b * b - 4 * a * c);

(-b + disc) / (2 * a);   // -7.450580596923828e-9  — about 25% wrong
(2 * c) / (-b - disc);   // -1e-8                  — correct

两行算的是同一个根。第一行让两个几乎相等的数相减;第二行把代数式重排,让这样的减法根本不必发生。Goldberg 的《每位计算机科学家都应了解的浮点算术知识》至今仍是关于抵消现象和误差界的权威论述。

Kahan 求和

Kahan 的补偿求和会把每次加法丢掉的低位比特记下来,在下一轮迭代时补回去:

function kahanSum(values) {
  let sum = 0;
  let compensation = 0;
  for (const value of values) {
    const y = value - compensation;
    const t = sum + y;
    compensation = (t - sum) - y;   // the bits that fell off
    sum = t;
  }
  return sum;
}

kahanSum(new Array(10000).fill(0.1));   // 1000  — exactly

Python 直接把这件事替你做好了。math.fsum([0.1] * 10_000) 返回 1000.0;自 CPython 3.12 起,内置的 sum() 也用上了 Neumaier 补偿,所以 sum([0.1] * 10_000) 是精确的,而显式写 += 的循环仍然会漂到 1000.0000000001588

长串求和、财务聚合和数值积分,都该用上补偿。只有几个值、或者已经换成精确类型时,就不必了。

那些打破假设的特殊值

IEEE 754 为一批不遵守你惯常预期的值保留了位模式:

NaN === NaN;          // false — mandated by the standard
Number.isNaN(NaN);    // true
[NaN].indexOf(NaN);   // -1   (uses ===)
[NaN].includes(NaN);  // true (uses SameValueZero)

-0 === 0;             // true
Object.is(-0, 0);     // false
1 / -0;               // -Infinity

1e308 * 10;           // Infinity — overflow never throws
Infinity - Infinity;  // NaN
Number.MIN_VALUE;     // 5e-324 — the smallest subnormal double

NaN 与任何值比较都不相等,包括它自己,这是设计使然:一次失败的计算永远不会伪装成有效结果。用 Number.isNaN() 或 Python 的 math.isnan() 去检测它。

溢出更安静,也更危险:它返回 ±Infinity 然后继续往下跑,一路向下游传播,直到有人发现图表上全是空白。负零是一个独立的位模式,但在 === 下仍然等于 +0;它来自负值的下溢,或者 -1 * 0,只有除法或 Object.is 才能把它揪出来。

次正规数填补了零与最小正规数之间的空隙。当阶码字段全为 0 时,隐含的前导 1 被丢弃,尾数逐渐向零收缩,这保证了两个不相等的浮点数之差永远不会舍入成精确的零。代价是精度递减,而且在某些硬件上会撞上陡峭的性能悬崖。IEEE 754 浮点数转换器里的特殊值快捷按钮可以一键载入 ±0、±Infinity、NaN 和最小次正规数,让你直接检视每一种位模式。

float、double、FP16 与 bfloat16 的取舍

同一个标准,在范围和浮点数精度之间给出了不同的预算分配:

格式总位数阶码位尾数位约合十进制位数最大有限值典型用途
binary16 (FP16)16510~3.365504GPU 推理、紧凑存储
bfloat161687~2.4~3.39 × 10³⁸机器学习训练
binary32 (float)32823~7.2~3.40 × 10³⁸图形、传感器、GPU
binary64 (double)641152~15.9~1.80 × 10³⁰⁸其余场景的默认选择

阶码位买的是范围,尾数位买的是精度。FP16 把预算压在精度上,上限只到 65504,和真实的梯度值离得太近,溢出成 Infinity 是家常便饭。bfloat16 做了相反的取舍,保住 FP32 的八位阶码,只留七位尾数,于是那些会让 FP16 溢出的值可以安然通过,训练时也很少需要损失缩放。

默认就用 double。只有在测出内存或带宽瓶颈之后,再降到更窄的格式,并且在转换器里把同一个值按各种格式分别输入一遍,看清楚变窄到底代价几何。

浮点数精度的实用准则

  • 绝不要对计算得来的浮点数用 ==,改用按数据量级调好的「绝对 + 相对」组合容差。
  • 不要把 Number.EPSILON 当阈值,它描述的只是 1.0 处的网格。
  • 钱要么存成整数最小单位,要么用十进制类型,而且十进制值一律从字符串构造。
  • 长串求和要做补偿,用 Kahan、Neumaier 或 math.fsum
  • 两个几乎相等的量要相减时,先重排公式,而不是围着它去收紧容差。
  • 给人看的数字要显式格式化,用 toFixed、f-string 或 printf,别把默认的 repr 直接丢给用户。
  • 跨服务边界传数字,用字符串或整数。经过 double 的 JSON 往返是静默有损的。
  • 货币字段选 NUMERIC,不要选 FLOAT。上线之后再改,意味着一次数据迁移加一次对账。
  • 当一个值看起来不可能时,回到比特层面去想。我们的位运算完全指南靠的是同一套习惯,它能把浮点数的玄学变成你可以亲手验算的算术。

常见问题

浮点数运算是不是坏掉了?

浮点数运算没坏,它严格遵循 IEEE 754。这个标准用有限位的二进制来表示实数,而 0.1 这类十进制小数没有精确的二进制形式,于是只能存下最接近的可表示值。真正坏掉的是「每个十进制小数都装得下」这个预期。

为什么 Python 把 0.1 打印成 0.1,却把 0.1 + 0.2 打印成 0.30000000000000004?

Python 的 repr 用的是最短往返格式化:打印能被解析回同一个 double 的最短十进制字符串。"0.1" 已经唯一标识了它的 double。而 0.1 + 0.2 得到的 double 和 "0.3" 映射到的不是同一个,所以格式化器必须输出全部十七位才能不产生歧义。

为什么不该用 Number.EPSILON 比较两个浮点数?

Number.EPSILON(约 2.22e-16)是 1 与下一个 double 之间的间隔,不是通用的误差预算。浮点数的间距每跨过一个 2 的幂就翻倍,所以在 1e9 附近,相邻 double 之间相差大约 1.19e-7。那里任何真实的舍入误差都超过 EPSILON,比较结果永远为假。应该用相对容差或组合容差。

如果最后再舍入,能不能用浮点数处理金额?

在最后才舍入救不了浮点数金额。误差会在加法、乘法和多步分摊之间累积,而你在哪一步舍入会改变总额。19.99 * 100 已经算出 1998.9999999999998。审计需要的是可复现的精确算术:整数分、decimalBigDecimal 或 SQL 的 NUMERIC

哪些十进制小数能被 double 精确存储?

只有那些能写成 m / 2ⁿ 的值,其中 m 和 n 为整数且在该格式的精度范围内。这包括 0.5、0.25、0.125、0.75 和 2.5,以及 2⁵³ 以内的每一个整数。先把分数约到最简:分母里只要还留着因子 5,比如 0.1、0.2、0.3 或 0.7,这个值就只能是近似的。

为什么 0.1 + 0.2 === 0.3 是 false,而 0.5 + 0.25 === 0.75 是 true?

因为 0.5、0.25 和 0.75 分别是 2⁻¹、2⁻² 和 2⁻¹ + 2⁻²,三个都能精确表示,它们的和也不需要舍入。而 0.1、0.2、0.3 各自都是近似存储的,把前两个的和舍入之后,落点比 0.3 所映射的 double 高一个 ULP。

每种编程语言都会这样吗?

只要建立在 IEEE 754 二进制浮点之上,任何语言都会有同样的表现:JavaScript、Python、Java、C、C++、Go、Rust、Swift,以及 SQL 的 FLOAT。差别在于默认打印精度,以及是否自带精确的十进制类型,比如 C# 的 decimal 或 Python 的 decimal 模块。

标签: floating-point ieee-754 javascript python numbers