1. 二进制小数

在十进制中,123.45 可以表示为 1 × 10² + 2 × 10¹ + 3 × 10⁰ + 4 × 10⁻¹ + 5 × 10⁻² = 123 ⁴⁵⁄₁₀₀,小数点的位置决定了数字的权重,左边的数是 10 的正幂,右边的数是 10 的负幂。

类似,二进制数 101.11 也可以表示为 1 × 2² + 0 × 2¹ + 1 × 2⁰ + 1 × 2⁻¹ + 1 × 2⁻² = 4 + 0 + 1 + ¹⁄₂ + ¹⁄₄ = 5 ³⁄₄,小数点向左移动一位相当于这个数被 2 除,小数点向右移动一位相当于这个数乘以 2

在有限的长度下,十进制无法准确表示像 ¹⁄₃³⁄₅ 这样的数,二进制也无法准确表示像 ¹⁄₅ 这样的数,增加数字的长度可以提高表示的精度。

2. IEEE 745 标准

IEEE 745 定义了在计算机中浮点数的表示及其运算的标准,标准指定了两种基本浮点格式:单精度和双精度,两种扩展浮点格式:单精度扩展和双精度扩展。

浮点格式是一种数据结构,用于指定包含浮点数的字段、这些字段的布局及其算术解释。浮点存储格式指定如何将浮点格式存储在内存中,具体选择哪种存储格式由实现工具决定,JavaScript 采用的是 IEEE 745 双精度浮点格式。

IEEE 浮点标准用 V = (−1)ˢ × 2ᴱ × M 的形式来表示一个数:

  • 符号(sign)s 决定这个数是正数 s = 0 还是负数 s = 1
  • 指数(exponent)E 的作用是对浮点数的加权,权重是 2E 次幂。
  • 尾数(significand)M 是一个二进制小数。

如下图所示,表示浮点数的位划分为三个字段:

01.png

  • 一个符号位 s
  • k 位的指数字段 exp = eᵏ⁻¹ ... e¹e⁰ 编码指数 E
  • n 位的小数字段 frac = f⁻¹f⁻² ... f⁻ⁿ 编码尾数 M

图中给出了两种最常见的格式,32 位的单精度格式:1 位符号位、k = 8 的指数字段、n = 23 的小数字段,64 位的双精度格式:1 位符号位、k = 11 的指数字段、n = 52 的小数字段。

3. IEEE 浮点格式转为十进制数

浮点格式要形成最终表示的值 V = 2ᴱ × M,需要对各个字段作如下处理:

  • 指数 E = e - Biase 表示为 eᵏ⁻¹ ... e¹e⁰Bias 是一个等于 2ᵏ⁻¹ - 1 的偏移量。
  • 小数字段 frac = f⁻¹f⁻² ... f⁻ⁿ,尾数 M = 1 + frac

另外还有两种情况:

  • 当指数字段全为 0 时,指数值是 1 - Bias,尾数值是 M = frac
  • 当指数字段全为 1 时,小数字段全为 0 表示无穷,小数字段不等于 0 表示 NaN

下面以 8 位浮点格式为例介绍如何将其转为十进制数。

如:0 0110 110,其中有 k = 4 的指数字段和 n = 3 的小数字段,各个部分的处理如下:

  • 偏移量 Bias = 2⁴⁻¹ - 1 = 7
  • 指数字段 e = 0 × 2³ + 1 × 2² + 1 × 2¹ + 0 × 2⁰ = 6
  • 指数 E = 6 - 7 = -1
  • 小数字段 frac = 1 × 2⁻¹ + 1 × 2⁻² + 0 × 2⁻³ = ³⁄₄
  • 尾数 M = 1 + ³⁄₄ = ⁷⁄₄

最终值:V = 2ᴱ × M = 2⁻¹ × ⁷⁄₄ = ¹⁄₂ × ⁷⁄₄ = ⁷⁄₈ = 0.875

4. 舍入运算

表示方法限制了浮点数的范围和精度,所以浮点运算只能近似的表示实数运算。

IEEE 浮点格式定义了 4 种不同的舍入方式:

  • Round-to-even,向偶数舍入,将数字向上或向下舍入,使得结果的最低有效数字是偶数,如以十进制数为例:1.52.5 都将舍入为 2
  • Round-toward-zero,向零舍入,把正数向下舍入,负数向上舍入。
  • Round-down,向下舍入,把正数、负数都向下舍入。
  • Round-up,向上舍入,把正数、负数都向上舍入。

5. JavaScript 中的浮点数

JavaScript 中的浮点数有两种来源,由服务端返回的数据或者通过运算产生的结果,从服务端返回数据到前端展现会经过下面几个环节:

  1. +-----------------------------------------+
  2. | v
  3. +--------------------+ +------------------+ +----------+ +-----------+ +-------------+
  4. | HTTP Response Body | --> | 解析 JSON 字符串 | --> | 四则运算 | --> | 渲染页面 | --> | CanvasSVG |
  5. +--------------------+ +------------------+ +----------+ +-----------+ +-------------+
  6. |
  7. |
  8. v
  9. +-----------+
  10. | TEXT_NODE |
  11. +-----------+

在这个过程中,需要解决 JSON 解析丢失精度的问题、高精度浮点数运算的问题、高精度浮点数展示的问题。

6. JSON 解析

JavaScript 中通常会用 number 数据类型来保存数值、进行数值的运算,但由于 IEEE 浮点格式的限制,用 number 类型保存一个大浮点数时,精度会丢失。

  1. const num = 106119.43448475052345678
  2. // 106119.43448475053

同样,我们常用的 JSON.parse 方法解析 JSON 字符串时,大浮点数的精度也会丢失。

  1. const data = JSON.parse('{"values": [106119.43448475052345678,755180144.3253138888456,3086.27072845812345678]}');
  2. // {
  3. // values: [
  4. // 106119.43448475053,
  5. // 755180144.3253139,
  6. // 3086.2707284581234
  7. // ]
  8. // }

要确保从服务端获取的大浮点数精度不丢失,就不能用 JSON.parse 方法来解析 HTTP Response Body 里的 JSON 字符串,也不能直接用 number 类型来保存数值。

解析 JSON 时可以把数值保存为 string 类型:

  1. const data = {
  2. values: [
  3. '106119.43448475052345678',
  4. '755180144.3253138888456',
  5. '3086.27072845812345678'
  6. ]
  7. };

JSON 解析可以使用第 3 方库,如:https://github.com/sidorares/json-bigint

7. JavaScript 高精度数的运算

要对高精度数进行运算,首先要将高精度数转为便于运算的结构,如:

  1. // '106119.43448475052345678'
  2. let num = {
  3. // 数值拆成数组
  4. c: [106119, 43448475052345, 67800000000000],
  5. // 标识数值的指数
  6. e: 5,
  7. // 标识数值的正负
  8. s: 1
  9. };
  10. // '3086.27072845812345678'
  11. let num2 = {
  12. c: [3086, 27072845812345, 67800000000000],
  13. e: 3,
  14. s: 1
  15. };

转为如上结构后,再对数组中的各段数值进行运算。

高精度数值运算可以使用第 3 方库,如:http://mikemcl.github.io/bignumber.js/

8. 高精度浮点数在网页中的展示

浮点数在页面上的展示通常有两种方式,创建为一个文本节点、通过 CanvasSVG 画成图表。

创建为文本节点的来展示没有什么问题,如:

  1. document.createTextNode('106119.43448475052345678');
  2. // 或
  3. let div = document.createElement('div');
  4. div.innerText = '106119.43448475052345678';

画图表通常会用到第 3 方库,如:EChartsG2 等等,各种图表的具体实现上会依赖数值的比较、四则运算等等,目前 EChartsG2 还没有对大浮点数的支持。

参考资料

皮成,2018.07.10