CPython 和 PyPy 小数运算性能

6 python math floating-point performance pypy

我想使用数百万个数据点(以小数表示)运行 100k+ 次模拟。我选择小数而不是浮点数是为了浮点精度和易于对我的逻辑进行单元测试(因为0.1 + 0.1 + 0.1浮点数不等于 0.3...)。

我希望通过使用 PyPy 来加快模拟速度。但在我的测试过程中,我遇到了 PyPy 无法处理decimal.Decimal甚至_pydecimal.Decimal根本无法处理的情况,并且比 CPython 解释器(使用 C 进行decimal.Decimal算术)慢得多。因此,我复制/粘贴了整个代码库,并将所有Decimals 替换为floats,性能得到了巨大的提升:PyPy 比 CPython 快 60-x70 倍 - 但牺牲了准确性。

是否有任何解决方案可以在 PyPy 中使用小数精度并具有性能优势?我“可以”维护两个代码库:float用于批量运行 100k 模拟,Decimal用于稍后检查有趣的结果 - 但这需要维护两个代码库的开销......

以下是我在Raspberry Pi 4 (Ubuntu Server 20.10, 4 x 1.5GHZ ARM Cortex-A72, 8GB RAM)重现时运行的一些简单测试:

test_decimal.py

import time
from decimal import Decimal

start = time.time()
val = Decimal('1.0')
mul = Decimal('1.000001')
for i in range(10 * 1000 * 1000):
    val *= mul
end = time.time()
print(f"decimal.Decimal: {val:.8f} in {round(end-start,4)} sec")
Run Code Online (Sandbox Code Playgroud)

test_pydecimal.py

import time
from _pydecimal import Decimal

start = time.time()
val = Decimal('1.0')
mul = Decimal('1.000001')
for i in range(10 * 1000 * 1000):
    val *= mul
end = time.time()
print(f"pydecimal.Decimal: {val:.8f} in {round(end-start,4)} sec")
Run Code Online (Sandbox Code Playgroud)

test_float.py

import time
from decimal import Decimal

start = time.time()
val = float('1.0')
mul = float('1.000001')
for i in range(10 * 1000 * 1000):
    val *= mul
end = time.time()
print(f"float: {val:.8f} in {round(end-start,4)} sec")
Run Code Online (Sandbox Code Playgroud)

结果

测试 Python 3.8.6(海湾合作委员会10.2.0) Python 3.6.9 -PyPy 7.3.1 和 GCC 10.2.0
测试十进制 5.1131秒 55.0829 秒
test_pydecimal 315.4012秒 40.1771 秒
测试浮动 2.5607秒 0.1273秒

编辑#1:

  • 更新了示例(使用预先计算的乘法器,测量 之外的时间 print)和结果表:PyPy 和 CPython 在小数上的总体比较保持不变。
  • 模拟主要由对具有变化值的时间序列数据进行基本数学运算(加、减、乘、除)组成。

Jér*_*ard 4

您可以使用双双精度来实现您想要的效果,其速度比任意精度算术(即 )快得多Decimal,并且比双精度(即float)更准确。双双精度通常比四精度稍差,但大多数平台通常不支持后者。

doubledouble Python 包实现这一点并且与 PyPy 兼容。它不支持字符串解析和格式化,但您可以使用以下两种缓慢的方法来实现:

from decimal import Decimal
from doubledouble import DoubleDouble

def ddFromStr(s):
    hi = float(s)
    lo = float(Decimal(s) - Decimal(hi))
    return DoubleDouble(hi, lo)

def ddToStr(dd):
    return str(Decimal(dd.x) + Decimal(dd.y))
Run Code Online (Sandbox Code Playgroud)

以下是如何使用它:

start = time.time()
val = ddFromStr('1.0')
mul = ddFromStr('1.000001')
for i in range(10 * 1000 * 1000):
    val *= mul
end = time.time()
print(f"doubledouble.DoubleDouble: {ddToStr(val)} in {round(end-start,4)} sec")
Run Code Online (Sandbox Code Playgroud)

以下是我机器上的结果:

CPython:
  float: 22026.35564471 in 0.6692 sec
  decimal.Decimal: 22026.35566283 in 1.4355 sec
  doubledouble.DoubleDouble: 22026.35566283 in 11.62 sec

PyPy:
  float: 22026.35564471 in 0.011 sec
  decimal.Decimal: 22026.35566283 in 16.3268 sec
  doubledouble.DoubleDouble: 22026.355662823 in 0.1184 sec
Run Code Online (Sandbox Code Playgroud)

正如您所看到的, PyPy 上的doubledouble比CPython 上的包要快得多,Decimal而在这种情况下,两者提供了同样准确(截断)的结果。