CPython and PyPy Decimal operation performance

Viewed 724

I would like to run some 100k+ simulations with some millions of data points, which are represented as decimals. I choose decimals over floats for floating point accuracy and ease of unit testing my logic (since 0.1 + 0.1 + 0.1 does not equal 0.3 with floats...).

My hope was to speed up the simulations by using PyPy. But during my testing I encountered that PyPy does not handle decimal.Decimal or even _pydecimal.Decimal well at all - and gets dramatically slower than the CPython interpreter (which uses C for decimal.Decimal arithmetics). So I copy/pasted my whole codebase and replaced all Decimals with floats and the performance increase was huge: x60-x70 times faster with PyPy than CPython - with the sacrifice of accuracy.

Are there any solutions to use Decimals precision in PyPy with the performance benefit? I "could" maintain two codebases: float for batch running the 100k simulations, Decimal for inspecting the interesting results later - but this bears the overhead of maintaining two codebases...

Here are some simple tests I ran on a Raspberry Pi 4 (Ubuntu Server 20.10, 4 x 1.5GHZ ARM Cortex-A72, 8GB RAM) for reproduction:

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

Results

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

Edit #1:

  • Updated the examples (use precomputed multiplicator, measure time outside of print) and the results table: The overall comparison between PyPy and CPython performance on Decimals stays the same.
  • The simulation mostly consists of the basic math operations (add, subtract, multiply, divide) over time-series data with changing values.
2 Answers

You can use double-double precision to achieve what you want significantly faster than arbitrary-precision arithmetic (ie. Decimal) and more accurate than double precision (ie. float). Double-double precision is generally slightly less accurate than quad-precision, but the latter is generally not natively supported on most platforms.

The doubledouble Python package implement this and is compatible with PyPy. It does not support string parsing and formatting but you can implement this using the two slow following methods:

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

Here is how to use it:

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

Here are the results on my machine:

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

As you can see, the doubledouble package on PyPy is significantly faster than the Decimal package on CPython while the two provide equally accurate (truncated) results in this case.

From this issue in PyPy, the _pydecimal and decimal results should be equivalent in PyPy, since they are using the same code path. Multiplication/division in _pydecimal on PyPy with the JIT is about 8x slower than the C-based version in CPython, addition/subtraction is roughly equivalent.

Related