Question: Python 3.12 was released two days ago with several new features and improvements. It claims that it's faster than ever, so I decided to give it a try. I ran a few of my scripts with the new version, but it was slower than before. I tried various approaches, simplifying my code each time in an attempt to identify the bottleneck that was causing it to run slowly. However, I have been unsuccessful so far. Finally, I decided to test a simple for loop like the following: import time def calc(): for i in range(100_000_000): x = i * 2 t = time.time() calc() print(time.time() - t)  On my machine, it took 4.7s on Python 3.11.5 and 5.7s on Python 3.12.0. Trying on other machines had similar results. So why is it slower in the latest version of Python?

Answer: I am able to reproduce the observed behavior between CPython 3.11.2 and CPython 3.12.0rc2 on Debian Linux 6.1.0-6 using an Intel i5-9600KF CPU. I tried to use a low-level profiling approach so to find the differences. Put it shortly: your benchmark is very specific and CPython 3.12 is less optimized for this specific case. CPython 3.12 seems to manage object allocations, and more specifically range a bit differently. CPython 3.12 appears to create a new object from the constant 2 for every iteration of the loop as opposed to CPython 3.11. Moreover, the main evaluation function do an indirect function pointer call which is particularly slow in this case. Anyway, you should not use (C)Python in such a use-case (this is stated in the CPython doc).  Under the hood Here is the results I get (pretty stable between multiple launches): 3.11: 2.026395082473755 3.12: 2.4122846126556396  Thus, CPython 3.12 is roughly 20% slower than CPython 3.11 on my machine. Profiling results indicates that half the overhead comes from an indirect function pointer call in the main evaluation function of CPython 3.12 which was not present in CPython 3.11. This function call is expensive on most modern processors. Here is the assembly code of the hot part:  │ ↓ je 4b8 1,28 │ mov (%rdi),%rax 0,33 │ test %eax,%eax 0,61 │ ↓ js 4b8 0,02 │ sub $0x1,%rax 2,80 │ mov %rax,(%rdi) │ ↓ jne 4b8 0,08 │ mov 0x8(%rdi),%rax 16,28 │ → call *0x30(%rax) <---------------- HERE │ nop 1,53 │ 4b8: mov (%rsp),%rax │ lea 0x4(%rax),%rcx │ movzbl 0x3(%rax),%eax 0,06 │ mov 0x48(%r13,%rax,8),%rdx 1,82 │ mov (%rdx),%eax 0,04 │ add $0x1,%eax │ ↓ je 8800  While the assembly code of the same function in CPython 3.11 is similar, it does not have such expensive call. Still, there are many similar indirect function calls like this already in CPython 3.11. My hypothesis is that such a call is more expensive in CPython 3.12 because it is less predictable by the hardware prediction unit (possibly because the same instruction calls multiple functions). For more information about that, please read this great post. I cannot say much more about this part since the assembly code is really HUGE (and it turns out the C code is also pretty big). The rest of the overhead seems to come from the way object and more specifically constant are managed in CPython 3.12. Indeed, In CPython 3.11, PyObject_Free calls are slow (because you spent all your time creating/deleting objects), while in CPython 3.12, such a call is not even visible in the profiler, but there is instead PyLong_FromLong which is quite slow (not visible in CPython 3.11). The rest of the (many other) functions only takes less than 25~30% and look similar in the two versions. Based on that, we can conclude that CPython 3.12 creates a new object from the constant 2 for each iteration of the loop as opposed to CPython 3.11. This is clearly not efficient in this case (one should keep in mind that CPython is an interpreter and not a compiler though so it is not surprising it does not perform optimizations on this). There is a simple way to check that: store 2 in a variable before the loop and use this variable in the loop. Here is the corrected code: import time def calc(): const = 2 for i in range(100_000_000): x = i * const t = time.time() calc() print(time.time() - t)  Here are the timings of the corrected code: 3.11: 2.045902967453003 3.12: 2.2230796813964844  The CPython 3.12 version is now substantially faster than before while the other version is almost unaffected by the modification of the code. At first glance, it tends to confirm the last hypothesis. That being said, the profiler still report many calls to PyLong_FromLong in the modified code! It turns out this change removed the issue related to the indirect function pointer call discussed in the beginning of this section! My hypothesis is that the PyLong_FromLong calls are coming from a different way to manage the objects generated from range (i.e. i). The following code tends to confirm that (note the code require ~4 GiB of RAM due to the list so it should not be used in production but only for testing purposes): import time def calc(): const = 2 TMP = list(range(100_000_000)) t = time.time() for i in TMP: x = i * const print(time.time() - t) calc()  Here are results on my machine: 3.11: 1.6515681743621826 3.12: 1.7162528038024902  The gap is closer than before and the timings of the loop is smaller since objects are all pre-computed in the list before. Profiling results confirm PyLong_FromLong is not called in the timed loop. Thus, range is slower in this case in CPython 3.12. The rest of the overhead is small (<4%). Such a performance gap can come from compiler optimizations or even very tiny changes in the CPython source code. For example, simple things like the address of conditional jumps can significantly impact performance results on many Intel CPUs (see: JCC erratum). Tiny details like this matters and compilers are not perfect. This is why such a variation of performance is common and rather expected so it's not worth investigating. By the way, if you care about performance, then please use Cython or PyPy for such a computing code.