I have a simulation in which the enduser can provide arbitrary many function which get then called in the inner most loop. Something like:
class Simulation:
def __init__(self):
self.rates []
self.amount = 1
def add(self, rate):
self.rates.append(rate)
def run(self, maxtime):
for t in range(0, maxtime):
for rate in self.rates:
self.amount *= rate(t)
def rate(t):
return t**2
simulation = Simulation()
simulation.add(rate)
simulation.run(100000)
Being a python loop this is very slow, but I can't get to work my normal approaches to speedup the loop.
Because the functions are user defined, I can't "numpyfy" the innermost call (rewriting such that the innermost work is done by optimized numpy code).
I first tried numba, but numba doesn't allow to pass in functions to other functions, even if these functions are also numba compiled. It can use closures, but because I don't know how many functions there are in the beginning, I don't think I can use it. Closing over a list of functions fails:
@numba.jit(nopython=True)
def a()
return 1
@numba.jit(nopython=True)
def b()
return 2
fs = [a, b]
@numba.jit(nopython=True)
def c()
total = 0
for f in fs:
total += f()
return total
c()
This fails with an error:
[...]
File "/home/syrn/.local/lib/python3.6/site-packages/numba/types/containers.py", line 348, in is_precise
return self.dtype.is_precise()
numba.errors.InternalError: 'NoneType' object has no attribute 'is_precise'
[1] During: typing of intrinsic-call at <stdin> (4)
I can't find the source but I think the documentation of numba stated somewhere that this is not a bug but not expected to work.
Something like the following would probably work around calling functions from a list, but seems like bad idea:
def run(self, maxtime):
len_rates = len(rates)
f1 = rates[0]
if len_rates >= 1:
f2 = rates[1]
if len_rates >= 2:
f3 = rates[2]
#[... repeat until some arbitrary limit]
@numba.jit(nopython=True)
def inner(amount):
for t in range(0, maxtime)
amount *= f1(t)
if len_rates >= 1:
amount *= f2(t)
if len_rates >= 2:
amount *= f3(t)
#[... repeat until the same arbitrary limit]
return amount
self.amount = inner(self.amount)
I guess it would also possible to do some bytecode hacking: Compile the functions with numba, pass a list of strings with the names of the functions into inner
, do something like call(func_name)
and then rewrite the bytecode so that it becomes func_name(t)
.
For cython just compiling the loop and multiplications will probably speedup a bit, but if the user defined functions are still python just calling the python function will probably still be slow (although I didn't profile that yet). I didn't really found much information on "dynamically compiling" functions with cython, but I guess I would need to somehow add some typeinformation to the user provided functions, which seems.. hard.
Is there any good way to speedup loops with user defined functions without needing to parsing and generating code from them?