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

Question:

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.

Asked By: Random

||

Answers:

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'dx01}x00|x00dx01kx02rx16|x00dx02kx02rx10|x00dx027x00}x00dx00Sx00|x00dx037x00}x00dx00Sx00|x00dx038x00}x00dx00Sx00'

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)
Answered By: Carcigenicate
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.