Optimizing Performance#
Pint can impose a significant performance overhead on computationally-intensive problems. The following are some suggestions for getting the best performance.
Note
Examples below are based on the IPython shell (which provides the handy %timeit extension), so they will not work in a standard Python interpreter.
Use magnitudes when possible#
It’s significantly faster to perform mathematical operations on magnitudes (even though you’re still using pint to retrieve them from a quantity object).
In [1]: from pint import UnitRegistry
In [2]: ureg = UnitRegistry()
In [3]: q1 = ureg('1m')
In [4]: q2 = ureg('2m')
In [5]: %timeit (q1 - q2)
8.24 us +- 44.5 ns per loop (mean +- std. dev. of 7 runs, 100,000 loops each)
In [6]: %timeit (q1.magnitude - q2.magnitude)
214 ns +- 2.39 ns per loop (mean +- std. dev. of 7 runs, 1,000,000 loops each)
This is especially important when using pint Quantities in conjunction with an iterative solver, such as the brentq method from scipy:
In [7]: from scipy.optimize import brentq
In [8]: def foobar_with_quantity(x):
...: # find the value of x that equals q2
...:
...: # assign x the same units as q2
...: qx = ureg(str(x)+str(q2.units))
...:
...: # compare the two quantities, then take their magnitude because
...: # brentq requires a dimensionless return type
...: return (qx - q2).magnitude
...:
In [9]: def foobar_with_magnitude(x):
...: # find the value of x that equals q2
...:
...: # don't bother converting x to a quantity, just compare it with q2's magnitude
...: return x - q2.magnitude
...:
In [10]: %timeit brentq(foobar_with_quantity,0,q2.magnitude)
286 us +- 9.05 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
In [11]: %timeit brentq(foobar_with_magnitude,0,q2.magnitude)
1.14 us +- 21.3 ns per loop (mean +- std. dev. of 7 runs, 1,000,000 loops each)
Bear in mind that altering computations like this loses the benefits of automatic unit conversion, so use with care.
A safer method: wrapping#
A better way to use magnitudes is to use pint’s wraps decorator (See Wrapping and checking functions). By decorating a function with wraps, you pass only the magnitude of an argument to the function body according to units you specify. As such this method is safer in that you are sure the magnitude is supplied in the correct units.
In [12]: import pint
In [13]: ureg = pint.UnitRegistry()
In [14]: import numpy as np
In [15]: def f(x, y):
....: return (x - y) / (x + y) * np.log(x/y)
....:
In [16]: @ureg.wraps(None, ('meter', 'meter'))
....: def g(x, y):
....: return (x - y) / (x + y) * np.log(x/y)
....:
In [17]: a = 1 * ureg.meter
In [18]: b = 1 * ureg.centimeter
In [19]: %timeit f(a, b)
219 us +- 1.95 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
In [20]: %timeit g(a, b)
30.6 us +- 1.73 us per loop (mean +- std. dev. of 7 runs, 10,000 loops each)
Speed up registry instantiation#
When the registry is instantiated, the definition file is parsed, loaded and some pre-calculations are made to speed-up certain common operations. This process can be time consuming for a large definition file such as the default one (and very comprehensive) provided with pint. This can have a significant impact in command line applications that create and drop registries.
Since version 0.19, part of this process can be cached resulting in a 5x to 20x performance improvement for registry instantiation using an included version of flexcache. This feature is experimental and therefore disabled by default, but might be enable in future versions.
To enable this feature just use the cache_folder argument to provide (as a str or pathlib.Path) the location where the cache will be saved.
>>> import pint
>>> ureg = pint.UnitRegistry(cache_folder="/my/cache/folder")
If you want to use the default cache folder provided by the OS, use :auto:
>>> import pint
>>> ureg = pint.UnitRegistry(cache_folder=":auto:")
Pint use an external dependency of platformdirs to obtain the correct folder, for example in macOS is /Users/<username>/Library/Caches/pint
In any case, you can check the location of the cache folder.
>>> ureg.cache_folder
Note
Cached files are stored in pickle format with a unique name generated from hashing the path of the original definition file. This hash also includes the platform (e.g. ‘Linux’), python implementation (e.g. ‘CPython’), python version, pint version and the non_int_type setting of the UnitRegistry to avoid mixing incompatible caches. If the definition file includes another (using the @import directive), this latter file will be cached independently. Finally, when a definition file is loaded upon registry instantiation the RegistryCache is also cached. The cache is invalidated based on the content hash. Therefore, if you modify the text definition file a new cache file will be generated. Caching by content hash allows sharing the same cache across multiple environments that use the same python and pint versions. At any moment, you can delete the cache folder without any risk.