Suppose that there is a function costly_function_a(x)
such that:
- it is very costly in terms of execution time;
- it returns the same output whenever the same
x
is fed to it; and - it does not perform "additional tasks" besides returning an output.
In these conditions, instead of calling the function twice in a row with the same x
, we can store the result in a temporary variable, then use that variable to do these computations.
Now suppose that there are some functions (f(x)
, g(x)
and h(x)
in the example below) that call costly_function_a(x)
, and that some of these functions may call each others (in the example below, g(x)
and h(x)
both call f(x)
). In this case, using the simple method mentioned above still results in repeated calls to costly_function_a(x)
with the same x
(see OkayVersion
below). I did found a way to minimize the number of calls, but it is "ugly" (see FastVersion
below). Any ideas of a better way to do this?
#Dummy functions representing extremely slow code.
#The goal is to call these costly functions as rarely as possible.
def costly_function_a(x):
print("costly_function_a has been called.")
return x #Dummy operation.
def costly_function_b(x):
print("costly_function_b has been called.")
return 5.*x #Dummy operation.
#Simplest (but slowest) implementation.
class SlowVersion:
def __init__(self,a,b):
self.a = a
self.b = b
def f(self,x): #Dummy operation.
return self.a(x) + 2.*self.a(x)**2
def g(self,x): #Dummy operation.
return self.f(x) + 0.7*self.a(x) + .1*x
def h(self,x): #Dummy operation.
return self.f(x) + 0.5*self.a(x) + self.b(x) + 3.*self.b(x)**2
#Equivalent to SlowVersion, but call the costly functions less often.
class OkayVersion:
def __init__(self,a,b):
self.a = a
self.b = b
def f(self,x): #Same result as SlowVersion.f(x)
a_at_x = self.a(x)
return a_at_x + 2.*a_at_x**2
def g(self,x): #Same result as SlowVersion.g(x)
return self.f(x) + 0.7*self.a(x) + .1*x
def h(self,x): #Same result as SlowVersion.h(x)
a_at_x = self.a(x)
b_at_x = self.b(x)
return self.f(x) + 0.5*a_at_x + b_at_x + 3.*b_at_x**2
#Equivalent to SlowVersion, but calls the costly functions even less often.
#Is this the simplest way to do it? I am aware that this code is highly
#redundant. One could simplify it by defining some factory functions...
class FastVersion:
def __init__(self,a,b):
self.a = a
self.b = b
def f(self, x, _at_x=None): #Same result as SlowVersion.f(x)
if _at_x is None:
_at_x = dict()
if 'a' not in _at_x:
_at_x['a'] = self.a(x)
return _at_x['a'] + 2.*_at_x['a']**2
def g(self, x, _at_x=None): #Same result as SlowVersion.g(x)
if _at_x is None:
_at_x = dict()
if 'a' not in _at_x:
_at_x['a'] = self.a(x)
return self.f(x,_at_x) + 0.7*_at_x['a'] + .1*x
def h(self,x,_at_x=None): #Same result as SlowVersion.h(x)
if _at_x is None:
_at_x = dict()
if 'a' not in _at_x:
_at_x['a'] = self.a(x)
if 'b' not in _at_x:
_at_x['b'] = self.b(x)
return self.f(x,_at_x) + 0.5*_at_x['a'] + _at_x['b'] + 3.*_at_x['b']**2
if __name__ == '__main__':
slow = SlowVersion(costly_function_a,costly_function_b)
print("Using slow version.")
print("f(2.) = " + str(slow.f(2.)))
print("g(2.) = " + str(slow.g(2.)))
print("h(2.) = " + str(slow.h(2.)) + "\n")
okay = OkayVersion(costly_function_a,costly_function_b)
print("Using okay version.")
print("f(2.) = " + str(okay.f(2.)))
print("g(2.) = " + str(okay.g(2.)))
print("h(2.) = " + str(okay.h(2.)) + "\n")
fast = FastVersion(costly_function_a,costly_function_b)
print("Using fast version 'casually'.")
print("f(2.) = " + str(fast.f(2.)))
print("g(2.) = " + str(fast.g(2.)))
print("h(2.) = " + str(fast.h(2.)) + "\n")
print("Using fast version 'optimally'.")
_at_x = dict()
print("f(2.) = " + str(fast.f(2.,_at_x)))
print("g(2.) = " + str(fast.g(2.,_at_x)))
print("h(2.) = " + str(fast.h(2.,_at_x)))
#Of course, one must "clean up" _at_x before using a different x...
The output of this code is:
Using slow version.
costly_function_a has been called.
costly_function_a has been called.
f(2.) = 10.0
costly_function_a has been called.
costly_function_a has been called.
costly_function_a has been called.
g(2.) = 11.6
costly_function_a has been called.
costly_function_a has been called.
costly_function_a has been called.
costly_function_b has been called.
costly_function_b has been called.
h(2.) = 321.0
Using okay version.
costly_function_a has been called.
f(2.) = 10.0
costly_function_a has been called.
costly_function_a has been called.
g(2.) = 11.6
costly_function_a has been called.
costly_function_b has been called.
costly_function_a has been called.
h(2.) = 321.0
Using fast version 'casually'.
costly_function_a has been called.
f(2.) = 10.0
costly_function_a has been called.
g(2.) = 11.6
costly_function_a has been called.
costly_function_b has been called.
h(2.) = 321.0
Using fast version 'optimally'.
costly_function_a has been called.
f(2.) = 10.0
g(2.) = 11.6
costly_function_b has been called.
h(2.) = 321.0
Note that I do not want to "store" the results for all values of x
used in the past (because this would require too much memory). Moreover, I do not want to have a function returning a tuple of the form (f,g,h)
because there are cases where I only want f
(so there is no need to evaluate costly_function_b
).