
@rawpixel
甚至学童也意识到存在各种数字系统,并且在二进制数字系统中并非每个有限的小数部分都是有限的事实。 很少有人认为由于这个事实,对float和double的运算是不精确的。
如果我们谈论Erlang,则与许多其他语言一样,它实现了float的IEEE754标准,而Erlang中的标准Integer类型是使用任意精度算法实现的。 但是,我不仅希望有bigint,而且还希望能够以必要的精度使用有理数,复数和浮点数进行运算。
本文仅对浮点数的编码理论进行了简要介绍,并给出了新兴效果的最醒目的例子。 通过过渡到定点表示来提供必要的操作精度的解决方案被设计为EAPA库(Erlang任意精度算法),旨在满足在Erlang / Elixir上开发的金融应用程序的需求。
标准,标准,标准...
如今,二进制浮点算术的主要标准是IEEE754,该标准广泛用于工程和编程中。 它定义了四种演示格式:
- 单精度32位
- 双精度64位
- 单扩展精度> = 43位(很少使用)
- 双扩展精度> = 79位(通常使用80位)
和四种舍入模式: - 四舍五入,趋于最接近的整数。
- 舍入趋于零。
- 舍入趋于+∞
- 向-∞舍入
大多数现代微处理器都是使用IEEE754格式的实数表示形式的硬件实现来制造的。 表示格式限制了数字的大小限制,并且舍入模式会影响准确性。 程序员通常无法更改硬件的行为并无法实现编程语言。 例如,正式的Erlang实现在64位计算机上以3个字存储浮点数,在32位计算机上以4个字存储浮点数。
如上所述,IEEE754格式的数字是一个有限集,无限的实数集映射到该有限集,因此原始数字可能会以IEEE754格式出现错误。
当显示在有限集合上时,大多数数字具有稳定且相对较小的误差。 因此,对于浮点数,它是11.920928955078125e-6%,而对于两倍数-2.2204460492503130808472633361816e-14%。 在程序员的生活中,要解决的大多数日常任务使我们可以忽略此错误,尽管应该注意的是,即使在简单的任务中,您也可以踩踏耙子,因为对于浮点数和两倍数,绝对误差的大小分别可以达到10 31和10 292 ,从而导致计算困难。
效果图
从一般信息到业务。 让我们尝试重现Erlang中新兴的效果。
以下所有示例均设计为ct测试。
舍入和精度损失
让我们从经典开始-两个数字相加:0.1 + 0.2 = ?:
t30000000000000004(_)-> ["0.30000000000000004"] = io_lib:format("~w", [0.1 + 0.2]).
添加的结果与直观预期的结果略有不同,并且测试成功通过。 让我们尝试获得正确的结果。 使用EAPA重写测试:
t30000000000000004_eapa(_)->
此测试也成功,表明问题已解决。
让我们继续实验,为1.0添加一个非常小的值:
tiny(_)-> X = 1.0, Y = 0.0000000000000000000000001, 1.0 = X + Y.
如您所见,我们的增长没有引起注意。 我们正在尝试解决该问题,同时说明该库的功能之一-自动缩放:
tiny_eapa(_)-> X1 = eapa_int:with_val(1, <<"1.0">>), X2 = eapa_int:with_val(25, <<"0.0000000000000000000000001">>), <<"1.0000000000000000000000001">> = eapa_int:to_float(eapa_int:add(X1, X2)).
位网格溢出
除了与小数相关的问题外,溢出也是一个显而易见的重要问题。
float_overflow(_) -> 1.0 = 9007199254740991.0 - 9007199254740990.0, 1.0 = 9007199254740992.0 - 9007199254740991.0, 0.0 = 9007199254740993.0 - 9007199254740992.0, 2.0 = 9007199254740994.0 - 9007199254740993.0.
从测试中可以看到,在某些时候,差异不再等于1.0,这显然是一个问题。 EAPA也解决了这个问题:
float_overflow_eapa(_)-> X11 = eapa_int:with_val(1, <<"9007199254740992.0">>), X21 = eapa_int:with_val(1, <<"9007199254740991.0">>), <<"1.0">> = eapa_int:to_float(1, eapa_int:sub(X11, X21)), X12 = eapa_int:with_val(1, <<"9007199254740993.0">>), X22 = eapa_int:with_val(1, <<"9007199254740992.0">>), <<"1.0">> = eapa_int:to_float(1, eapa_int:sub(X12, X22)), X13 = eapa_int:with_val(1, <<"9007199254740994.0">>), X23 = eapa_int:with_val(1, <<"9007199254740993.0">>), <<"1.0">> = eapa_int:to_float(1, eapa_int:sub(X13, X23)).
危险减少
以下测试表明危险减少的发生。 在此过程中,结果值远小于输入值的操作的计算精度将发生灾难性的下降。 在我们的例子中,减法的结果为1。
我们显示在Erlang中存在此问题:
reduction(_)-> X = float(87654321098765432), Y = float(87654321098765431), 16.0 = XY.
结果是16.0,而不是预期的1.0。 让我们尝试解决这种情况:
reduction_eapa(_)-> X = eapa_int:with_val(1, <<"87654321098765432">>), Y = eapa_int:with_val(1, <<"87654321098765431">>), <<"1.0">> = eapa_int:to_float(eapa_int:sub(X, Y)).
Erlang中浮点运算的其他功能
让我们从忽略负零开始。
eq(_)-> true = list_to_float("0.0") =:= list_to_float("-0.0").
只想说EAPA保留了这种行为:
eq_eapa(_)-> X = eapa_int:with_val(1, <<"0.0">>), Y = eapa_int:with_val(1, <<"-0.0">>), true = eapa_int:eq(X, Y).
因为它是有效的 Erlang没有清晰的语法和NaN和无穷的处理方式,因此产生了许多功能,例如:
1> math:sqrt(list_to_float("-0.0")). 0.0
接下来的一点是处理大小数字的功能。 让我们尝试为小孩子复制:
2> list_to_float("0."++lists:duplicate(322, $0)++"1"). 1.0e-323 3> list_to_float("0."++lists:duplicate(323, $0)++"1"). 0.0
对于大量:
4> list_to_float("1"++lists:duplicate(308, $0)++".0"). 1.0e308 5> list_to_float("1"++lists:duplicate(309, $0)++".0"). ** exception error: bad argument
以下是一些有关小数字的示例:
6> list_to_float("0."++lists:duplicate(322, $0)++"123456789"). 1.0e-323 7> list_to_float("0."++lists:duplicate(300, $0)++"123456789"). 1.23456789e-301
8> 0.123456789e-100 * 0.123456789e-100. 1.524157875019052e-202 9> 0.123456789e-200 * 0.123456789e-200. 0.0
上面的例子证实了Erlang项目的真实性:在IEEE754中不能算钱。
EAPA(Erlang任意精度算法)
EAPA是用Rust编写的NIF扩展。 目前,EAPA存储库提供了最简单方便的eapa_int接口来处理定点数字。 eapa_int的功能包括:
- 缺乏IEEE754编码的影响
- 大量支持
- 可配置的精度高达126位小数。 (在当前实施中)
- 自动缩放
- 支持所有数字基本操作
- 或多或少的完整测试,包括基于属性的测试。
eapa_int
接口:
with_val/2
将浮点数转换为固定表示,可以安全地在json,xml中使用。to_float/2
以给定的精度将定点数转换为浮点数。to_float/1
将定点数转换为浮点数。add/2
两个数字的和sub/2
差异mul/2
乘法divp/2
除法min/2
最小值max/2
最大值eq/2
检查数字是否相等lt/2
检查数字是否少于lte/2
检查小于等于gt/2
检查数字是否更大gte/2
检查大于等于
EAPA代码可在存储库https://github.com/Vonmo/eapa中找到
什么时候应该使用eapa_int? 例如,如果您的应用程序需要花钱,或者您需要方便,准确地对92233720368547758058079223372054754775807.92233720368547758079223372036854775807等数字执行计算操作,则可以安全地使用EAPA。
像任何解决方案一样,EAPA是一种折衷方案。 我们通过牺牲内存和计算速度来获得必要的精度,在实际系统上收集的性能测试和统计数据表明,大多数操作都在3-30μs的范围内执行。 选择EAPA固定点接口时,也必须考虑这一点。
结论
当然,在Erlang或Elixir上解决此类问题并非总是必要的,但是当出现问题且找不到合适的工具时,您必须发明解决方案。
本文旨在与社区分享该工具和经验,希望该库对于某些人有用并有助于节省时间。
您如何看待Erlang的钱?
PS将处理有理数和复数,以及对任意精度的整数,浮点数,复数,有理类型的本机访问,将在以下出版物中介绍。 不要切换!
相关资料: