Search code examples
pythonexecution-time

Is there a performance difference between omitting and including a return statement within a python function?


Assuming we have a function that updates a bunch of internal values within a class like so:

class MyClass:
    def __init__():
        self.counter = 0
        self.condition_1 = True
        self.condition_2 = False
    def update():
        if self.condition_1:
            if self.condition_2:
                self.counter += 2
                # return or not?
            else:
                self.counter += 1
                # return or not?
        else:
            self.counter -= 1
            # return or not?

Would the update function be executed faster with or without a return statement within it (after updating variables)? Or would it be 100% the same? (unlikely for me)

I know this sounds like a trivial/dumb question to ask without context, but consider that this function is being called repeatedly thousands of times so slight increase in performance within the function can have a large impact on how fast the whole program takes to execute.

In my real program, the conditions within the update function are very complex and more nested; the program processes a lot of data as well.


Solution

  • You can look at the produced bytecode to answer this:

    With explicit returns:

      4           0 LOAD_CONST               1 (0)
                  2 STORE_FAST               0 (counter)
      5           4 LOAD_FAST                0 (counter)
                  6 LOAD_CONST               1 (0)
                  8 COMPARE_OP               2 (==)
                 10 POP_JUMP_IF_FALSE       22 (to 44)
      6          12 LOAD_FAST                0 (counter)
                 14 LOAD_CONST               2 (1)
                 16 COMPARE_OP               2 (==)
                 18 POP_JUMP_IF_FALSE       16 (to 32)
      7          20 LOAD_FAST                0 (counter)
                 22 LOAD_CONST               3 (2)
                 24 INPLACE_ADD
                 26 STORE_FAST               0 (counter)
      8          28 LOAD_CONST               0 (None)
                 30 RETURN_VALUE
     10     >>   32 LOAD_FAST                0 (counter)
                 34 LOAD_CONST               2 (1)
                 36 INPLACE_ADD
                 38 STORE_FAST               0 (counter)
     11          40 LOAD_CONST               0 (None)
                 42 RETURN_VALUE
     13     >>   44 LOAD_FAST                0 (counter)
                 46 LOAD_CONST               2 (1)
                 48 INPLACE_SUBTRACT
                 50 STORE_FAST               0 (counter)
     14          52 LOAD_CONST               0 (None)
                 54 RETURN_VALUE
    

    Without explicit returns:

      4           0 LOAD_CONST               1 (0)
                  2 STORE_FAST               0 (counter)
      5           4 LOAD_FAST                0 (counter)
                  6 LOAD_CONST               1 (0)
                  8 COMPARE_OP               2 (==)
                 10 POP_JUMP_IF_FALSE       22 (to 44)
      6          12 LOAD_FAST                0 (counter)
                 14 LOAD_CONST               2 (1)
                 16 COMPARE_OP               2 (==)
                 18 POP_JUMP_IF_FALSE       16 (to 32)
      7          20 LOAD_FAST                0 (counter)
                 22 LOAD_CONST               3 (2)
                 24 INPLACE_ADD
                 26 STORE_FAST               0 (counter)
                 28 LOAD_CONST               0 (None)
                 30 RETURN_VALUE
     10     >>   32 LOAD_FAST                0 (counter)
                 34 LOAD_CONST               2 (1)
                 36 INPLACE_ADD
                 38 STORE_FAST               0 (counter)
                 40 LOAD_CONST               0 (None)
                 42 RETURN_VALUE
     13     >>   44 LOAD_FAST                0 (counter)
                 46 LOAD_CONST               2 (1)
                 48 INPLACE_SUBTRACT
                 50 STORE_FAST               0 (counter)
                 52 LOAD_CONST               0 (None)
                 54 RETURN_VALUE
    

    Although it's hard to see manually, they result in identical bytecode in this particular case, with Python 3.10 (verified with diffchecker; although any text comparison tool would work). The only difference is the loading of None is associated with a line with the explicit return version.

    Both result in the same compiled bytes:

    >>> dis.Bytecode(update).codeobj.co_code
     b'd\x01}\x00|\x00d\x01k\x02r\x16|\x00d\x02k\x02r\x10|\x00d\x027\x00}\x00d\x00S\x00|\x00d\x037\x00}\x00d\x00S\x00|\x00d\x038\x00}\x00d\x00S\x00'
    

    Test code:

    import dis
    
    def update():
        counter = 0
        if counter == 0:
            if counter == 1:
                counter += 2
                return
            else:
                counter += 1
                return
        else:
            counter -= 1
            return
    
    dis.dis(update)