Search code examples
pythonscientific-computing

Performance impact on trying to call methods on None in a loop


Let's imagine the following code

from pymaybe import maybe

def compute_something():
return np.sin(2)

def foo(observer=None):
    observer = maybe(observer)
    for i in range(0, int(1e3)):
         something = compute_something()
         observer.observe(something)  # Comment this line for comparison

So basically I am attempting some kind of inversion of control where I pass an object implementing an interface (here a single method observe), but I want to leave the possibility that the user just pass nothing (default argument value is thus is defined as maybe(None).

The question is: should I expect some performance impact?

I used timeit to compare the two (with and without the call to observe):

Without a call to observe:

1.77 ms ± 5.38 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

With a call to observe:

3.27 ms ± 22.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

The impact seems to be quite minimal and I assumed that pymaybe is just a try block, so I compared with the following code as well:

def foo(observer=None):
    observer = None
    for i in range(0, int(1e3)):
         something = compute_something()
         try:
             observer.observe(something)
         except:
            pass
foo()

With the following results:

With a call to observe:

2.73 ms ± 66.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

So I conclude the following (no surprise): - pymaybe adds some overhead by being one additional layer around a try block - the try block itself has a non negligible impact on the performance

If my assumptions/conclusions above are correct, what would be a way to further improve the performance?

Shall I duplicate the code?

Is there a way to "remove" calls to methods on None at runtime (in a loop where the value won't change during the execution of the loop)?


Solution

  • The first approach would be to use an if statement, as Scott Hunter has suggested in the comments:

    def foo(observer=None):
        for i in range(0, int(1e3)):
             something = compute_something()
             if observer:
                 observer.observe(something)
    

    You could also use a wrapper around observer.observe that defaults to doing nothing:

    def foo(observer=None):
        try:
            observe = observer.observe
        except AttributeError:
            def observe(x):
                pass
        for i in range(0, int(1e3)):
             something = compute_something()
             observe(something)
    

    In my experiments, both approaches have similar running times.