CPython和PyPy小数运算性能
CPython and PyPy Decimal operation performance
我想 运行 一些 100k+ 模拟,其中包含数百万个数据点,这些数据点表示为小数。我选择小数而不是浮点数以获得浮点精度和单元测试我的逻辑的便利性(因为 0.1 + 0.1 + 0.1
不等于 0.3 与浮点数...)。
我希望通过使用 PyPy 来加速模拟。但是在我的测试过程中,我发现 PyPy 根本不能很好地处理 decimal.Decimal
甚至 _pydecimal.Decimal
- 并且比 CPython 解释器(使用 C 进行 decimal.Decimal
算术)慢得多。所以我 copy/pasted 我的整个代码库并将所有 Decimal
替换为 float
并且性能提升是巨大的: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
之外测量时间)和结果 table:PyPy 和 CPython 在 Decimals 上的性能总体比较保持不变。
- 模拟主要由具有变化值的时间序列数据的基本数学运算(加、减、乘、除)组成。
由此 issue in PyPy,_pydecimal
和 decimal
结果在 PyPy 中应该是等价的,因为它们使用相同的代码路径。 Multiplication/division in _pydecimal
on PyPy with JIT 比 CPython 中基于 C 的版本慢大约 8 倍,addition/subtraction 大致相同。
您可以使用 双双精度 比任意精度算法(即 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
包快得多,而两者在这种情况下提供同样准确(截断)的结果。
我想 运行 一些 100k+ 模拟,其中包含数百万个数据点,这些数据点表示为小数。我选择小数而不是浮点数以获得浮点精度和单元测试我的逻辑的便利性(因为 0.1 + 0.1 + 0.1
不等于 0.3 与浮点数...)。
我希望通过使用 PyPy 来加速模拟。但是在我的测试过程中,我发现 PyPy 根本不能很好地处理 decimal.Decimal
甚至 _pydecimal.Decimal
- 并且比 CPython 解释器(使用 C 进行 decimal.Decimal
算术)慢得多。所以我 copy/pasted 我的整个代码库并将所有 Decimal
替换为 float
并且性能提升是巨大的: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
之外测量时间)和结果 table:PyPy 和 CPython 在 Decimals 上的性能总体比较保持不变。 - 模拟主要由具有变化值的时间序列数据的基本数学运算(加、减、乘、除)组成。
由此 issue in PyPy,_pydecimal
和 decimal
结果在 PyPy 中应该是等价的,因为它们使用相同的代码路径。 Multiplication/division in _pydecimal
on PyPy with JIT 比 CPython 中基于 C 的版本慢大约 8 倍,addition/subtraction 大致相同。
您可以使用 双双精度 比任意精度算法(即 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
包快得多,而两者在这种情况下提供同样准确(截断)的结果。