The Python interpreter core loop sounds like the perfect problem for AlphaEvolve. Or it's open source equivalent OpenEvolve if DeepMind doesn't want to speed up Python for the competition.
TLDR: The tail-calling interpreter is slightly faster than computed goto.
> I used to believe the the tailcalling interpreters get their speedup from better register use. While I still believe that now, I suspect that is not the main reason for speedups in CPython.
> My main guess now is that tail calling resets compiler heuristics to sane levels, so that compilers can do their jobs.
> Let me show an example, at the time of writing, CPython 3.15’s interpreter loop is around 12k lines of C code. That’s 12k lines in a single function for the switch-case and computed goto interpreter.
> […] In short, this overly large function breaks a lot of compiler heuristics.
> One of the most beneficial optimisations is inlining. In the past, we’ve found that compilers sometimes straight up refuse to inline even the simplest of functions in that 12k loc eval loop.
Really nice results on MSVC. The idea that tail calls effectively reset compiler heuristics and unblock inlining is pretty convincing. One thing that worries me though is the reliance on undocumented MSVC behavior — if this becomes widely shipped, CPython could end up depending on optimizer guarantees that aren’t actually stable. Curious how you’re thinking about long-term maintainability and the impact on debugging/profiling.
I remember a former colleague, (may he RIP) ported a similar optimization to our fork of Python 2.5, circa 2007. We were running Linux on PPC and it gave us that similar 10-15% boost at the time.
Apparently(?) this also needs to be attached to the function declarator and does not work as a function specifier: `static void *__preserve_none slowpath();` and not `__preserve_none static void *slowpath();` (unlike GCC attribute syntax, which tends to be fairly gung-ho about this sort of thing, sometimes with confusing results).
Yay to getting undocumented MSVC features disclosed if Microsoft thinks you’re important enough :/
Im a bit out of the loop with this, but hope its not like that time with python 3.14, when it was claimed a geometric mean speedup of about 9-15% over the standard interpreter when built with Clang 19. It turned out the results were inflated due to a bug in LLVM 19 that prevented proper "tail duplication" optimization in the baseline interpreter's dispatch loop. Actual gains was aprox 4%.
Edit: Read through it and have come to the conclusion that the post is 100% OK and properly framed: He explicitly says his approach is to "sharing early and making a fool of myself," prioritizing transparency and rapid iteration over ironclad verification upfront.
One could make an argument that he should have cross-compiler checks, independent audits, or delayed announcements until results are bulletproof across all platforms. But given that he is 100% transparent with his thinking and how he works, it's all good in the hood.
I've never seen this kind of benchmark graph before, and it looks really neat! How was this generated? What tool was used for the benchmarks?
(I actually spent most of Sep/Oct working on optimizing the Immer JS immutable update library, and used a benchmarking tool called `mitata`, so I was doing a lot of this same kind of work: https://github.com/immerjs/immer/pull/1183 . Would love to add some new tools to my repertoire here!)
Is there a Clang based build for Windows? I’ve been slowly moving my Windows builds from MSVC to Clang. Which still uses the Microsoft STL implementation.
So far I think using clang instead of MSVC compiler is a strict win? Not a huge difference mind you. But a win nonetheless.
I have quetion - slightly off topic, but related. I was wandering why is pyhton interpreter so much slower than V8 javascript interpreter when both javascript and python are dynamic interpreted languages.
I don't understand this focus on micro performance details... considering that all of this is about an interpretation approach which is always going to be slow relatively speaking. The big speed up would be to JIT it all, then you dont need to care about structuring of switch loops etc
My understanding is that also this tail call based interpretation is also kinder to the branch predictor. I wonder if this explains some of the slow downs - they trigger specific cases that cause lots of branch mispredictions.
After years of admonition discouraging me, I’m using Python for a Windows GUI app over my usual C#/MAUI. I’m much more familiar with Python and the whole VS ecosystem is just so heavy for lightweight tasks. I started with tkinter but found it super clunky for interactions I needed heavily, like on field change, but learning QT seemed like more of a lift than I was interested in. (Maybe a skill issue on both fronts?) Grabbed wxglade and drag-and-dropped an interface with wxpython that only has one external dependency installable with pip, is way more convenient than writing xaml by hand, and ergonomically feels pretty pythonic compared to QT. Glad to see more work going into the windows runtime because I’ll probably be leaning on it more.
If anyone stumbles upon this in a search for Python UI libaries, also check out Textural. It's a TUI setup that can also be displayed in web browsers because it uses CSS for styling. At least for fairly uncomplicated UIs, it seems pretty simple, and has good event hook support. It has some features like event bubbling, and reactivity, which smack of JS front-end framework workflows on the data side, for better or worse.
Python's recent developments have been monumental, new versions now easily best the PyPy performance charts on M4 MacBook Air, idk if this has something to do with optimizations by Apple but coming from Linux I was surprised
Matt Godbolt was saying recently that using tail-calls for an interpreter suits the branch predictor inside the cpu. Compared to a single big switch / computed jump.
I would have thought it actually helps the branch target predictor rather than the branch predictor. If you assume a simple predictor where the predicted target is just the last taken one then it's going to be wrong almost every time for a single switch. It will only be right for repeats of the exact same instruction.
If you have a separate switch at the end of each instruction then it will be right any time an instruction is followed by the same instruction as last time, which can probably happen quite a lot for short loops.
36 comments
[ 4.5 ms ] story [ 62.0 ms ] threadLooks like it refers to this:
https://youtu.be/pUj32SF94Zw
(wish it's a link in the article)
I've asked Ken. He said he'll update the article.
> I used to believe the the tailcalling interpreters get their speedup from better register use. While I still believe that now, I suspect that is not the main reason for speedups in CPython.
> My main guess now is that tail calling resets compiler heuristics to sane levels, so that compilers can do their jobs.
> Let me show an example, at the time of writing, CPython 3.15’s interpreter loop is around 12k lines of C code. That’s 12k lines in a single function for the switch-case and computed goto interpreter.
> […] In short, this overly large function breaks a lot of compiler heuristics.
> One of the most beneficial optimisations is inlining. In the past, we’ve found that compilers sometimes straight up refuse to inline even the simplest of functions in that 12k loc eval loop.
I'd have expected it to be hand rolled assembly for the major ISAs, with a C backup for less common ones.
How much energy has been wasted worldwide because of a relatively unoptimized interpreter?
Apparently(?) this also needs to be attached to the function declarator and does not work as a function specifier: `static void *__preserve_none slowpath();` and not `__preserve_none static void *slowpath();` (unlike GCC attribute syntax, which tends to be fairly gung-ho about this sort of thing, sometimes with confusing results).
Yay to getting undocumented MSVC features disclosed if Microsoft thinks you’re important enough :/
Edit: Read through it and have come to the conclusion that the post is 100% OK and properly framed: He explicitly says his approach is to "sharing early and making a fool of myself," prioritizing transparency and rapid iteration over ironclad verification upfront.
One could make an argument that he should have cross-compiler checks, independent audits, or delayed announcements until results are bulletproof across all platforms. But given that he is 100% transparent with his thinking and how he works, it's all good in the hood.
(I actually spent most of Sep/Oct working on optimizing the Immer JS immutable update library, and used a benchmarking tool called `mitata`, so I was doing a lot of this same kind of work: https://github.com/immerjs/immer/pull/1183 . Would love to add some new tools to my repertoire here!)
So far I think using clang instead of MSVC compiler is a strict win? Not a huge difference mind you. But a win nonetheless.
If you have a separate switch at the end of each instruction then it will be right any time an instruction is followed by the same instruction as last time, which can probably happen quite a lot for short loops.
"apology peice" and "tail caling"