【问题标题】:CPython and PyPy Decimal operation performanceCPython 和 PyPy 十进制运算性能
【发布时间】:2021-03-15 23:00:55
【问题描述】:

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

我希望通过使用 PyPy 来加速模拟。但是在我的测试过程中,我发现 PyPy 根本无法处理 decimal.Decimal 甚至 _pydecimal.Decimal - 并且比 CPython 解释器(它使用 C 来处理 decimal.Decimal 算术)慢得多。所以我复制/粘贴了我的整个代码库,并用floats 替换了所有Decimals,性能提升巨大:PyPy 比CPython 快x60-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")

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

结果

Test 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 sec 55.0829 sec
test_pydecimal 315.4012 sec 40.1771 sec
test_float 2.5607 sec 0.1273 sec

编辑#1:

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

【问题讨论】:

  • 我没有对其进行分析,但print() 可能会扭曲您的结果。我敢打赌,将decimal.Decimal 转换为strfloat 需要更多的努力。尝试不使用print() 的计时实验。重要的是要了解您也在计时 print(),这不是正确的数据操作计时方法,除非您真的想要计时 print()。计时print() 不可靠的原因之一是由于缓冲。
  • 在变量中预先计算 float('1.000001') 时,val 是相同的,但 Python 的执行速度快 4 倍,PyPy 的执行速度快 63 倍...顺便说一下,精度是多少?你想做什么样的手术?
  • @JérômeRichard 感谢您的输入 - 我使用预先计算的 val 更新了示例,并更新了结果表并添加了有关计算的信息。 8 的精度就足够了。

标签: python performance math floating-point pypy


【解决方案1】:

您可以使用 double-doubleprecision 比任意精度算术(即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))

这里是如何使用它:

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 包快得多,而在这种情况下,两者提供同样准确(截断)的结果。

【讨论】:

  • 这太棒了 - 谢谢!我根据您的ddFromStrDoubleDouble 子类化为__init__ - 现在我基本上有一个直接替换,不需要更改我的代码库
【解决方案2】:

从这个issue in PyPy_pydecimaldecimal 结果在 PyPy 中应该是等效的,因为它们使用相同的代码路径。使用 JIT 在 PyPy 上 _pydecimal 中的乘法/除法比 CPython 中基于 C 的版本慢约 8 倍,加法/减法大致相当。

【讨论】:

  • 这指的是较旧的 PyPy 和 CPython 实现 - 如果有一种“方法”可以在 PyPy 中获得小数精度并具有速度优势,则不回答
  • 那个问题的答案在今天仍然很重要:在这个比较中,目前没有办法让 PyPy 更快。您可以赞助 PyPy 开发人员作为顾问来解决这个问题,请联系 pypy-dev 邮件列表。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-17
  • 1970-01-01
  • 2011-10-27
  • 1970-01-01
  • 2018-06-29
相关资源
最近更新 更多