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.
Answers:
You can look at the produced bytecode to answer this:
With explicit return
s:
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 return
s:
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)
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.
You can look at the produced bytecode to answer this:
With explicit return
s:
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 return
s:
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)