CPython和PyPy的十进制运算性能

6
我希望运行一些包含数百万个十进制表示的数据点的100k+模拟。我选择十进制而不是浮点数来获得浮点精度并易于对逻辑进行单元测试(因为使用浮点数时0.1 + 0.1 + 0.1不等于0.3...)。我希望能够通过使用PyPy加速模拟。但在测试过程中,我发现PyPy不能很好地处理 decimal.Decimal 或者 _pydecimal.Decimal,而且速度明显比使用C语言实现decimal.Decimal算术运算的CPython解释器慢。所以我复制/粘贴了整个代码库,并将所有Decimal替换为float,这样使用PyPy比CPython快了60-70倍,牺牲了精度。是否有任何解决方案可以在PyPy中同时具有Decimals精度和性能优势?我“可以”维护两个代码库:float用于批量运行100k个模拟,Decimal用于稍后检查有趣的结果 - 但这需要维护两个代码库... 这里有一些我在Raspberry Pi 4(Ubuntu Server 20.10,4 x 1.5GHZ ARM Cortex-A72,8GB RAM)上进行验证的简单测试:
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")

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")

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")

结果

测试 Python 3.8.6 (GCC 10.2.0) Python 3.6.9 -PyPy 7.3.1 with GCC 10.2.0
test_decimal 5.1131 秒 55.0829 秒
test_pydecimal 315.4012 秒 40.1771 秒
test_float 2.5607 秒 0.1273 秒

编辑 #1:

  • 更新了示例(使用预计算乘数,在 print之外测量时间)和结果表格:PyPy 和 CPython 在 Decimals 上的性能总体比较结果保持不变。
  • 模拟主要由基本的算术操作(加、减、乘、除)组成,作用于随着时间变化的时间序列数据。

我没有进行过性能分析,但是 print() 可能会影响你的结果。我敢打赌将 decimal.Decimal 转换为 str 比转换为 float 需要更多的努力。尝试在没有 print() 的情况下进行时间实验。重要的是要理解,你也在计时 print(),这不是正确计时数据操作的方法,除非你真的想计时 print()。计时 print() 不可靠的原因之一是由于缓冲。 - Michael Ruth
使用变量预计算 float('1.000001'),在 Python 中 val 的值不变,但执行速度提高了 4 倍,在 PyPy 中则提高了 63 倍。顺便问一下,您需要什么精度级别和进行什么样的操作? - Jérôme Richard
@JérômeRichard 感谢您的建议 - 我已经更新了带有预计算 val 的示例,并更新了结果表并添加了有关计算的信息。精度为 8 就足够了。 - user10370644
2个回答

4
您可以使用双倍精度(double-double precision)比任意精度算术(即Decimal)更快地实现您想要的,并且比双精度(即float)更准确。 通常情况下,双倍精度略微不如四倍精度准确,但后者在大多数平台上通常不会得到本机支持。
Python软件包doubledouble实现了这一点,并且与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))

以下是如何使用它的方法:

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")

以下是我的机器上的结果:

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

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


太棒了 - 谢谢!我使用你的 ddFromStr 基于 __init__ 子类化了 DoubleDouble,现在基本上我有了一个即插即用的替代品,不需要改变我的代码库。 - user10370644

0
从这个PyPy问题中可以看出,在PyPy中,_pydecimaldecimal的结果应该是等效的,因为它们使用相同的代码路径。在带有JIT的PyPy中,_pydecimal的乘法/除法比CPython中基于C的版本慢大约8倍,而加法/减法则大致相等。

这指的是旧版PyPy和CPython实现 - 并没有回答是否有一种“方法”可以在PyPy中获得十进制精度并获得速度优势。 - user10370644
那个问题的答案今天仍然相关:目前没有办法使PyPy在这种比较中更快。您可以赞助一位PyPy开发人员作为顾问来解决这个问题,请在pypy-dev邮件列表上联系我们。 - mattip

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接