I have the following code in a Python file called benchmark.py
.
source = """
for i in range(1000):
a = len(str(i))
"""
import timeit
print(timeit.timeit(stmt=source, number=100000))
When I tried to run with multiple python versions I am seeing a drastic performance difference.
C:\Users\Username\Desktop>py -3.10 benchmark.py
16.79652149998583
C:\Users\Username\Desktop>py -3.11 benchmark.py
10.92280820000451
As you can see this code runs faster with python 3.11 than previous Python versions. I tried to disassemble the bytecode to understand the reason for this behaviour but I could only see a difference in opcode names (CALL_FUNCTION
is replaced by PRECALL
and CALL
opcodes).
I am quite not sure if that's the reason for this performance change. so I am looking for an answer that justifies with reference to cpython source code.
python 3.11
bytecode
0 0 RESUME 0
2 2 PUSH_NULL
4 LOAD_NAME 0 (range)
6 LOAD_CONST 0 (1000)
8 PRECALL 1
12 CALL 1
22 GET_ITER
>> 24 FOR_ITER 22 (to 70)
26 STORE_NAME 1 (i)
3 28 PUSH_NULL
30 LOAD_NAME 2 (len)
32 PUSH_NULL
34 LOAD_NAME 3 (str)
36 LOAD_NAME 1 (i)
38 PRECALL 1
42 CALL 1
52 PRECALL 1
56 CALL 1
66 STORE_NAME 4 (a)
68 JUMP_BACKWARD 23 (to 24)
2 >> 70 LOAD_CONST 1 (None)
72 RETURN_VALUE
python 3.10
bytecode
2 0 LOAD_NAME 0 (range)
2 LOAD_CONST 0 (1000)
4 CALL_FUNCTION 1
6 GET_ITER
>> 8 FOR_ITER 8 (to 26)
10 STORE_NAME 1 (i)
3 12 LOAD_NAME 2 (len)
14 LOAD_NAME 3 (str)
16 LOAD_NAME 1 (i)
18 CALL_FUNCTION 1
20 CALL_FUNCTION 1
22 STORE_NAME 4 (a)
24 JUMP_ABSOLUTE 4 (to 8)
2 >> 26 LOAD_CONST 1 (None)
28 RETURN_VALUE
PS: I understand that python 3.11 introduced bunch of performance improvements but I am curios to understand what optimization makes this code run faster in python 3.11