Erlang中任意精度的算术


@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(_)-> %% prec = 1 symbols after coma X = eapa_int:with_val(1, <<"0.1">>), Y = eapa_int:with_val(1, <<"0.2">>), <<"0.3">> = eapa_int:to_float(1, eapa_int:add(X, Y)). 

此测试也成功,表明问题已解决。
让我们继续实验,为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. %% has to be 1.0 

结果是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的功能包括:


  1. 缺乏IEEE754编码的影响
  2. 大量支持
  3. 可配置的精度高达126位小数。 (在当前实施中)
  4. 自动缩放
  5. 支持所有数字基本操作
  6. 或多或少的完整测试,包括基于属性的测试。

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将处理有理数和复数,以及对任意精度的整数,浮点数,复数,有理类型的本机访问,将在以下出版物中介绍。 不要切换!




相关资料:


Source: https://habr.com/ru/post/zh-CN428111/


All Articles