佐证材料。
# Decimal 类型小调研
Decimal 是有行业标准的:decimal64 (opens new window) 和 decimal128 (opens new window) 都是 IEEE 754 标准定义中的类型,是为金融、税收场景准备的可精确表示十进制数的类型,2008 年才有标准。
It is intended for applications where it is necessary to emulate decimal rounding exactly, such as financial and tax computations.
Double 其实是 IEEE 的 binary64 (opens new window) 类型,其为快速、更高精度的常规计算准备,1985 年就有标准。
Double 和 Decimal 简单对比如下:
Double | decimal64 | decimal128 | |
---|---|---|---|
有效数字 | 16 | 16 | 34 |
表示范围 | ±0.000000000000001×10^−308^ 到 ±1.797693134862315×10^308^, NaN、+Inf、-Inf | ±0.000000000000000×10^−383^ 到 ±9.999999999999999×10^384^. ±0000000000000000×10^−398^ 到 ±9999999999999999×10^369^. NaN、+Inf、-Inf | $10^{-6143}, 10^{6144}$ NaN、+Inf、-Inf |
场景 | 起始数字就是不精确的,如度量的结果 | 起始数字就是精确的,如金融税收 | 同左 |
解决什么:Decimal 帮助我们解决十进制小数的精确存储问题:0.1
就是 0.1
,而不是什么其他近似数。
不解决什么:Decimal 不帮我们解决 Double 有效数字不够用的问题。有效数字不够用可考虑 decimal128 或者 binary128。
而唯一阻止我们使用 decimal64
而使用 Double 的,可能就是计算效率了。
# 有效数字够用吗
2021 年全球 GDP 是 96.51 万亿美元,约 $10^{13}$,它本身仅有 4 个有效数字(9、6、5、1,后面的指数部分不算)。 如真实数据是 96512345678912.34 美元或 9651234567891234 美分,则有 16 个有效数字。
- 换成越南货币,约 $10^{18}$ 越南盾,可认为有$19–21$个有效数字(越南盾之下还有越南枢),一般订单计价不会超过这个数字。 (根据 全球货币排行榜 (opens new window),越南盾已是最不值钱的货币之一。)
人类财富总和约 $10^{15}$ 美元。来源:
全球最富有1%的人口掌握了价值110万亿美元的财富。
宇宙粒子总数 $10^{80}$。
世界人口 70 亿,如真实数据精确到个位,则约 10 个有效数字。
因此可以认为 16 个有效数字可以满足大部分财务、税收场景。如果有更大的需求,可以默认使用 decimal128。
Long 呢? 双精度整数类型 long 的范围是 $-10^{18}–10^{18}$,约 19 位有效数字。如在金融领域,不按元而按分或厘存储,则大部分的存储、少部分计算场景都可以用 Long 来表达,且 Long 的计算速度比 Double 更快。但注意,
- 财务中经常要计算的复利等,其计算过程无法用 Long 描述。
- Long 没有指数部分,因此最大也只能表示 10^18 左右的数字。
# 有效数字与计算误差
当运算中数据的有效数字逼近存储极限时,计算误差会大幅上升,甚至不可用。
这篇叫 Performance Analysis of BigDecimal Arithmetic Operation in Java 的论文 (opens new window)(尽管写作稀烂)除了速度,还测试了计算误差。
Double 本身只有 16 位有效数字,
- 因此在 10 位精度的 100*100 维矩阵乘法,Double 的误差还是比较小的。计算结果(矩阵内的 10000 个数字)总体误差 $10^{-5}–10^{-3}$。
- 但 20 位精度的 100*100 维矩阵乘法,Double 的误差就上天了,达到 $555,204–18,805,729$。
decimal64 的测试数据应该与 Double 相同,因为它们的有效数字相同;文中与 Double 对比的是更高精度的 BigDecimal 类型。
另外 MySQL Decimal 的实现方法 (opens new window) 一文声称 MySQL 自己的二元算数运算在 precision 高的时候会有问题,如
……计算过程中如果发现整数部分太大会动态地挤占小数部分……
除法计算中间结果不受 scale = 31 的限制, 除法中间结果的 scale 一定是 9 的整数倍
# 运算速度
2008 年时有人说 double 比 decimal 快 100 倍,问他是否应该切换到 double 上: Decimal vs Double Speed - Stack Overflow (opens new window)。
评论劝他回头是岸,说 Double 产生的金融计算问题太多了,千万别;且新硬件有在支持 decimal 计算。
这篇博客 (opens new window)自己测试是 21 倍。
那篇博客 (opens new window)测试结果也差不多是 21 倍。
这篇叫 Performance Analysis of BigDecimal Arithmetic Operation in Java 的论文 (opens new window)(尽管写作稀烂)测出来是 10-100 倍(10 位精度~20 位精度)。
我自己也用 Java 的 BigDecimal 试了试,涉及平方根、幂运算、乘法、加法、除法,BigDecimal128 的执行用时大概是 Double 的 13 倍,BigDecimal64 的执行用时大概是 Double 的 7 倍。
- 当然中间有个 trick,因为 BigDecimal 不支持浮点数幂运算,所以幂运算部分我转成 Double 来做了。
- 这也给出了优化思路不是吗?很多场景的计算结果本来就没那么精确了,转成 Double 做就行了(不然可能也没办法)。
- 当然中间有个 trick,因为 BigDecimal 不支持浮点数幂运算,所以幂运算部分我转成 Double 来做了。
这个“硬件有在支持”,2022 年回头来看好像还是几乎不支持?
我们还可以看看更为乐观的数据。
# 对速度的展望
【C 语言】根据 Compiler Exploitation of Decimal Floating-Point Hardware (opens new window),如有硬件指令支持,速度能提升 27 倍(似乎与上述 20 倍较为吻合)起步呢(用例为 a[i] += b[i] * c[i];
循环)。
no opt | -O2 | -O3 | |
---|---|---|---|
C + decNumber lib | 1 | 1.26x | 2x |
C + DFP 指令 | 27 x | 39x (1.82x 横向) | 59x (4.37x 横向) |
但 The 'telco' benchmark (opens new window) 财务场景测试、一些其他微测试显示速度差异在 5 倍以内(该场景提醒读者它“不适合作为十进制实现的基准;现代应用通常需要更高的精度以及对齐、除法、转换和四舍五入等操作,而这些操作并没有出现在本基准中”)。
Intel 的这个库 (opens new window)速度也可以测一下?
【Swift】而 2 年前还有网友自己给 Swift 实现了 Decimal (opens new window) 并做了测试,给出了让人十分舒服的结果:
Type | Duration (secs) |
---|---|
Decimal64 | 1.026 |
DecimalFP64 | 1.087 |
Double | 1.381 |
Decimal | 5.137 |
Double (generic) | 1.823 |
什么?你们这个 Decimal64
比 Double
还快?有没有壮丁帮忙试一下的?
【JS】Js 的不同实现 (opens new window)速度也相差很多,如 Jampary 快于 BigNumberJs 快于 DecimalJs 远远快于 BigJs。
【Java】Java 的 BigDecimal 在有效数字位数不多的情况下还是挺快的 (opens new window)啊。epam/DFP (opens new window) 这个三方库的运算速度快吗?需要一个 Java 壮丁帮忙测试一下。
【硬件加速】未来有通用的硬件加速机制,或使用 GPU 做计算都是可能的。
# decimal64
还是 decimal128
?
根据 Performance Analysis of Decimal Floating-Point Libraries and Its Impact on Decimal Hardware and Software Solutions (opens new window) 的图 2,如使用特别恰当的实现,decimal128
相比 decimal64
仅慢 20%-50%。(当然我自己的那个简单测试的结果显示慢了 1 倍。)
# 竞品中的数值类型
Mendix 提供了 (opens new window) Decimal、Integer、Long,没有提供 Double。Decimal 的范围比较小,类似定点 Decimal 的范围。
- Decimal 的小数点前最多可有 20 位,小数点后最多可有 8 位。
OutSystems 提供了 (opens new window) Decimal、Currency、Integer、Long,没有提供 Double。Decimal 的范围比较小,类似定点 Decimal 的范围。
小数位数最多 8 位。最大值 $-2^{96}$,最小值 $2^{96} - 1$。
推测 Currency 就是 Decimal,因为网页上对 Currency 类型的备注是 “See Decimal type”。
Power Apps 只提供了 (opens new window) Number、Currency,它们都是 IEEE Double 浮点类型。Currency 只是多了 currency-formatting options。注意,Double 的问题也不少,例如本页 Excel 的 DELTA 函数的问题 (opens new window)。
我们轻舟应该可以做得更好。
# 数据库中的 Decimal
即便有 IEEE 标准,各数据库却有自己不同的实现,猜测可能是因为 IEEE 的 decimal 类型标准在 2008 年才出现、相对较晚,所以此前不同数据库为了金融场景已经自定义了自己的 decimal 类型,实现上有出入。(作为对比,double 类型早在 1985 年就已有标准。)小节末有个结论。
根据 Oracal 21c 官方文档 (opens new window),NUMBER [ (p [, s]) ]
定义如下:
Number having precision p and scale s. The precision p can range from 1 to 38.
The scale s can range from -84 to 127.
Both precision and scale are in decimal digits.
A NUMBER value requires from 1 to 22 bytes.
2
3
4
因此 Oracle 的 NUMBER 的有效数字是十进制的 38 位,可以精确表示类似 12345678901234567890123456789012345678 这样的数字。其 scale 是 -84–127。
- Oracle 的 scale -84–127 指的应该是在 precision p 的基础上,小数点再往左(正数 scale)或往右移动(负数 scale)。例如
Number(1234, -2)
应该是123400
而Number(1234, 2)
应该是12.34
。
参考:Oracle NUMBER Data Type (opens new window)。
根据 MySql 8.0 官方文档 (opens new window)
The declaration syntax for a DECIMAL column is DECIMAL(M,D).
The ranges of values for the arguments are as follows:
M is the maximum number of digits (the precision). It has a range of 1 to 65.
D is the number of digits to the right of the decimal point (the scale). It has a range of 0 to 30 and must be no larger than M.
If D is omitted, the default is 0. If M is omitted, the default is 10.
2
3
4
5
所以 MySql 的 DECIMAL 的有效数字是 65 位。其 scale 最多是 0–30(其实是 0–precision)。
- MySql 的 scale 也是指小数点的位置:
DECIMAL(65, 20)
指的是整数部分有 45 位,然后是小数点,小数点后面是 20 位。可看出此DECIMAL
的范围大概是 $-10^{65} – 10^{65}$(DECIMAL(65, 0)
),对于一般的(财务)计算也是足够的。
数值数据类型
精确数值数据类型包括:NUMERIC、DECIMAL、DEC 类型、NUMBER 类型、INTEGER 类型、INT 类型、BIGINT 类型、TINYINT 类型、BYTE 类型、SMALLINT 类型。
NUMERIC 数据类型用于存储零、正负定点数。其中:精度是一个无符号整数,定义了总的数字数,精度范围是 1 至 38。
2
3
4
所以其有效数字是 38 位,并且猜测其很可能在模仿 Oracle。
根据 Db2 11.5 官方文档 (opens new window)
DECIMAL or NUMERIC
The maximum precision is 31 digits.
The scale, which is the number of digits in the fractional part of the number, cannot be negative or greater than the precision.
2
3
4
所以有效数字是 31 位,scale 最多是 31(其实是 precision)。
(需要确认这里的 digits 是不是十进制数。估计是。)
另外,Db2 也提供了浮点 Decimal 类型。
根据 Decimal(P,S),Decimal32(S),Decimal64(S),Decimal128(S) | ClickHouse Docs (opens new window)
ClickHouse 等用于数据分析的列式数据库支持 decimal128,没有提供定点 Decimal。
根据 Model Monetary Data — MongoDB Manual (opens new window),MongoDB 等用的也是 decimal128,没有提供定点 Decimal。
【小结论】老旧数据库提供的 Decimal 存储格式都是定点 Decimal,且有效数字都比 decimal64 多,有些甚至远远超过 decimal128。而部分新数据库厂商直接使用了浮点 decimal64、decimal128 作为存储格式,不提供定点 Decimal 格式。
# 基于调研的建议
建议
- 合并现有的 Decimal 和 Double,内存中统一使用定长的 decimal128 或 decimal64。在低代码产品中声明只支持 34 或 16 个有效数字,并不兼容所有数据库的极限存储。如使用 decimal128:
- 以 MySQL 为例,其声明的
DECIMAL(m, n)
需满足m < 34
。 - 则低代码平台中可强制小数位数不超过 16 位,最大值不超过 $10^{33}$,最小值不小于 $-10^{33}$,且小数位数与最大值的指数相加不超过 34(即 decimal128 的上限)。(一说国际通行的金融规定是不少于 4 位小数即可。)
- 小数位数、最大值、最小值仅对存储生效,对计算过程不生效。
- 计算过程按 decimal128 标准进行舍入,慢就慢吧,主要是怕数字逼近存储极限。
- 未来可根据用户输入的最大值、小数位数决定是否使用 decimal64,做优化。
- 部分计算可以优化成使用 Double 或 Quadruple。
- 以 MySQL 为例,其声明的
- 与数据库通信时,自动转为相应数据库的 Decimal 类型,如
DECIMAL(16, 4)
。 - 未来或许需要提供一个高性能计算库。我们可以把
+ - * /
等最常用的中缀运算符分配给 Decimal 类型,而把前缀函数add(1.5, mul(2.1, 3.4))
等分配给高性能计算库。 - 删除
Integer
,只保留Long
。 - 是否可以提供一个全局的“高级功能”选项,这样可以保留部分复杂的类型,仅在开启该选项时暴露给用户。
# Decimal 类型规约
# decimal128
版
使用 IEEE 的 decimal128 作为 NASL 的 Decimal 类型。
对 Decimal 类型做如下约定:
- 有效数字最多为 34 位。判断方法:将一个数字转换为科学计数法后,后缀零之前的数字最多有 34 个。
- 例如 9.876543210123456789098765432101234000 * 10^3000^ 的有效数字即为 34 个。(此有效数字与数学上的有效数字略有区别。)
- 表示范围:
- 如只使用整数部分,可表示 ±0000000000000000000000000000000000×10^−6176^ 到 ±9999999999999999999999999999999999×10^6111^。
- 如包含小数,可表示 ±0.000000000000000000000000000000000×10^−6143^ 到 ±9.999999999999999999999999999999999×10^6144^。
- 注意,因为有效数字只有 34 位,因此它并不会覆盖 10^−6176^ 到 10^6111^ 间的所有数字。当有效数字达到 34 个时,例如给定 9.876543210123456789098765432101234 * 10^3000^ 时,并不能精确表示 9.876543210123456789098765432101234 * 10^3000^ + 1 这个数, 而只能表示 9.876543210123456789098765432101235 * 10^3000^(有效数字 + 1) 或 9.876543210123456789098765432101234 * 10^3001^ (指数 + 1)这样的数。
- 当运算结果的有效数字超过 34 位时,将无法保证该结果是精确的。
在数据实体中声明 Decimal 类型时,
设小数位数为 $n$,整数位数为 $m$,总体上满足 $m + n = 31$(这是因为 Db2 只支持 31 位)。小数位数由用户设置,整数位数由 $31 - n$ 计算得到
默认小数位数为 3 位。小数位数最少为 0 位,最多为 16 位。
最小值、最大值受小数位数影响。小数位数为 $n$ 时,
- 最大值为 $99..9.99..9$,其中小数点前面为 $m$ 个 9,小数点后面为 $n$ 个 9,即 $10^{m} - 10^{-n}$。
- 最小值为 $-99..9.99..9$,其中小数点前面为 $m$ 个 9,小数点后面为 $n$ 个 9,即 $-(10^{m} - 10^{-n})$。
小数位数、最大值、最小值仅对存储生效,对计算过程不生效。计算过程总是使用浮点 decimal128 类型,使用最大精度。转为字符串时也不会补充后缀零。
与数据库通信时,自动转为相应数据库的
Decimal(31, n)
类型。
# decimal64
版
使用 IEEE 的 decimal64 作为 NASL 的 Decimal 类型。
对 Decimal 类型做如下约定:
- 有效数字最多为 16 位。判断方法:将一个数字转换为科学计数法后,后缀零之前的数字最多有 16 个。
- 例如 9.876543210123456000 * 10^300^ 的有效数字即为 16 个。(此有效数字与数学上的有效数字略有区别。)
- 表示范围:
- 如只使用整数部分,可表示 ±0000000000000000×10^−398^ 到 ±9999999999999999×10^369^。
- 如包含小数,可表示 ±0.000000000000000×10^−383^ 到 ±9.999999999999999×10^384^。
- 注意,因为有效数字只有 16 位,因此它并不会覆盖 10^−398^ 到 10^369^ 间的所有数字。当有效数字达到 16 个时,例如给定 1234567890123456 * 10^300^ 时,并不能精确表示 1234567890123456 * 10^300^ + 1 这个数, 而只能表示 1234567890123457 * 10^300^(有效数字 + 1) 或 1234567890123456 * 10^301^ (指数 + 1)这样的数。
- 当运算结果的有效数字超过 16 位时,将无法保证该结果是精确的。
在数据实体中声明 Decimal 类型时,
设小数位数为 $n$,整数位数为 $m$,总体上满足 $m + n = 16$。小数位数由用户设置,整数位数由 $16 - n$ 计算得到
默认小数位数为 3 位。小数位数最少为 0 位,最多为 8 位。
最小值、最大值受小数位数影响。小数位数为 $n$ 时,
- 最大值为 $99..9.99..9$,其中小数点前面为 $m$ 个 9,小数点后面为 $n$ 个 9,即 $10^{m} - 10^{-n}$。
- 最小值为 $-99..9.99..9$,其中小数点前面为 $m$ 个 9,小数点后面为 $n$ 个 9,即 $-(10^{m} - 10^{-n})$。
小数位数、最大值、最小值仅对存储生效,对计算过程不生效。计算过程总是使用浮点 decimal128 类型,使用最大精度。转为字符串时也不会补充后缀零。
与数据库通信时,自动转为相应数据库的
Decimal(16, n)
类型。
未来优化:部分算数运算可以优化成使用 Double 或 Quadruple,比如指数不为整数的幂运算、三角函数等本来就需要对结果进行舍入的运算。
# 附录:一些重要材料
The ‘telco’ benchmark:https://speleotrove.com/decimal/telco.html
Performance Analysis of Decimal Floating-Point Libraries and Its Impact on Decimal Hardware and Software Solutions:http://iccd.et.tudelft.nl/2009/proceedings/465Anderson.pdf
Decimal64 performance,2009 IBM:https://speleotrove.com/decimal/dpdoub.html
Performance Analysis of BigDecimal Arithmetic Operation in Java :https://pdfs.semanticscholar.org/cba1/14835121a1d8700e2514029df120be9124a5.pdf
Compiler Exploitation of Decimal Floating-Point Hardware:https://studylib.net/doc/7962970/compiler-exploitation-of-decimal-floating-point-hardware
网友自制 Java 的 epam/DFP 库 https://github.com/epam/DFP
网友自制 Swift 的 Decimal 库:https://github.com/dehesa/Decimals
网友自制 js 的 Decimal 库:https://mikemcl.github.io/decimal.js/
Intel 官制 C 的 Decimal 库:https://www.intel.com/content/www/us/en/developer/articles/tool/intel-decimal-floating-point-math-library.html