PEP 836: JIT Go Brrr: The Path to a Supported JIT Compiler for CPython

Hello!

@savannahostrowski @brandtbucher and I have drafted a PEP for the roadmap to an officially supported JIT in CPython, including the holistic criteria that the JIT should meet, in response to the Steering Council’s announcement on the JIT pause.

The second paragraph of the abstract says:

This PEP proposes a path for the JIT to become a supported, non-experimental part of CPython if it meets measurable performance, compatibility, tooling, platform, distribution, security, and maintenance goals. The initial performance target is at least 20% geometric mean improvement on pyperformance for the JIT + free-threaded build compared to the non-JIT free-threaded build’s interpreter, measured as the mean across the supported Tier 1 platforms, by the first beta release of Python 3.17. The target is set as a minimum bar for continued in-tree development of the JIT.

Please give it a read. Thank you!

Note: Savannah is writing this PEP in her capacity as a core developer, not in her Steering Council capacity.

47 Likes

To get the ball rolling…, if (hopefully not!) the method-frontend does not meet expectations and there is a need to rollback to the trace-recorder, what happens then? A lot of the PEP is based on switching the frontend for better maintainability, easier teaching and testing (which is crucial).

The rollback to trace recording will reintroduce the problem of complexities in understanding and maintainability.

2 Likes

… there is a need to rollback to the trace-recorder, what happens then?

I would like to think that we (the core team) at the time will be able to make the decision earlier rather than later. We have a few options to go with this:

  1. Attract transferable expertise (what the method frontend is supposed to do, and I think the experts pool for tracing is smaller, considering we have only 2–3 people who really know how to work on it, while the current middle-end has a ton of contributors).
  2. Build up our own experts for our tracing JIT. This is what we’ve been doing for the past few years. We just teach ourselves and learn along the way and experiment. This is also partially what took us 3 years to evolve the trace frontend from the projecting to recording one. At the time when I wrote the trace recording frontend, I didn’t even know if it was going to work, it was just a hunch/bet. I’m not really in favour of this, considering all the experimentation slows down development velocity, and we shouldn’t be subjecting CPython’s main branch to experiments.

That said, whichever route (tracing or method) we ultimately go down, I’m committed to working on improving it to be the best possible for CPython.

6 Likes

That’s very reassuring to hear!

Hi, thank you for the PEP. I wanted to ask some questions out of curiosity mostly. JIT compilers are not an area in which I have any kind of experience so forgive me in advance.

In this thread there was much discussion concerning the future of the JIT and some have proposed tackling it at C-API level. As I understand it would put too much meat on the fire to work on that direction as well, but do you believe a reworked/redesigned C-API could potentially help achieving better results in terms of performance for a future JIT?

Concerning type hints: is your expectation that type-annotated Python code may introduce additional metadata information that the JIT might use to yield better performance?

Have you explored the current state of the art for existing JIT projects, such as numba, to leverage their experience for better designing the future JIT? Especially concerning interactions with extensions.

Thanks in advance!

Speaking from what I know: Type hints won’t help much as it doesn’t provide much data. Type Hints are just hints, so they may not be true/correct. Not to mention that the specialising adaptive interpreter already provides runtime type information, with additional information too!

Correct. Without enforcing type hints (like I proposed on the other thread) the JIT cannot make a general assumption about what types are (really) expected, and it must still continue to trace the data types and put guards where it’s needed.

I hope that this PEP unblocks the situation. While I fully understand the desire (and need!) to make a clarification and put more precise constraints/requirements/goals on the JIT, I don’t get why the PEP should be paused in the meanwhile, since it is also implemented by core CPython developers which are controlling the changes that are introduced on the main branch.

@jacopoabramo

but do you believe a reworked/redesigned C-API could potentially help achieving better results in terms of performance for a future JIT?

Personally yes, but that is out of scope for this PEP as you have mentioned.

@cdimauro

don’t get why the PEP should be paused in the meanwhile, since it is also implemented by core CPython developers which are controlling the changes that are introduced on the main branch.

To be clear: I fully understand the Steering Council’s decision to pause JIT development while we figure something out. It’s important to also get a feel of community consensus and also the SC’s approval. I think we should wait a bit to see what the community thinks, and also in case differing opinions/proposals arise. That is the whole point of the PEP process after all!

1 Like

I think the 20% performance goal for 3.17 along with making sure the JIT works with free-threading and other tooling makes me a +1 on what the PEP proposes.

15 Likes

A few of points/doubts on the PEP.

First, there’s no clear statement about the memory consumption when using the JIT (compared to non-JIT, of course). It’s mentioned that the initial implementation might use more memory, but there’s no goal neither KPI(s) defined for the long run → stabilization → CPython 3.17.

Regarding this:
However, we do not plan to concentrate dedicated cycles to improving 32-bit Windows performance and would like to exclude the platform from our goals as PyPI statistics suggest 32-bit Windows builds are a vanishingly small number of downloads.
why continue spending time on a 32-bit platform/architecture? It’s more than 20 years that all sold PCs are 64-bit. Windows 11 is 64-bit only. Even if there is a percentage of people which still need to use 32-bit Python versions, IMO it doesn’t deserve investing time/resources on it.
I’m not stating, with this, that 32-bit architectures/platforms should be entirely dropped: they can be kept, but without focus from CPython developers.

Finally, this:
Some Single Static Assignment (SSA) form properties over the stack. This does not mean we need to rewrite our IR to SSA form, but rather, the optimizer should have some SSA properties. We believe this aligns more closely with other compilers (e.g. Cinder, PyPy, Chrome’s V8, CRuby’s YJIT/ZJIT), and makes understanding how to optimize in the JIT easier and more powerful. The current JIT optimizer already nearly supports this, and only requires minimal changes to have SSA properties. An IR with proper stack discipline already has many useful properties that are analogous to SSA form. SSA form will basically come for free for stack variables.
might suggest a change on the CPython parser as well, introducing X = Y BINOP Z and X = UNOP Y bytecodes to help the JIT and gain some speed also on ceval (like I did time ago on WPython).

1 Like

@cdimauro thanks for raising those points.

First, there’s no clear statement about the memory consumption when using the JIT (compared to non-JIT, of course).

Good point, I realised the partial counterpart, PEP 779, mentions memory consumption on FT. I’ll take it up with the other authors on having an upper limit for additional memory use.

why continue spending time on a 32-bit platform/architecture?

This is an artefact of PEP 11, where 32-bit WIndows is still a tier 1 platform. See discussion here Consider downgrading Windows 32-bit from Tier-1 to Tier-2 or Tier-3? (in Python 3.13?) - #58 by AraHaan .

might suggest a change on the CPython parser as well, introducing X = Y BINOP Z and X = UNOP Y bytecodes to help the JIT and gain some speed also on ceval (like I did time ago on WPython).

Actually, I think that point was communicated badly on our part. We’re not suggesting to change the IR or current JIT much. We’re just saying that the current JIT already has most of these properties, so they naturally fall out! I’ve since raised a PR to clarify this PEP 836: Clarify that only the frontend is changing by Fidget-Spinner · Pull Request #5023 · python/peps · GitHub

2 Likes

Memory usage for JITs is notoriously tough to pin down, so I personally am not bothered by the lack of goal regarding memory usage beyond “don’t be obscene”.

Because it is currently a tier 1 platform for CPython and the PEP explicitly said it’s making sure there’s support for those platforms.

That seems out of scope for the PEP. I think the only reason there are so much technical details around perf is to tell the SC that there’s a reason to wait until 3.17 for hitting the perf goal (moving to a method-based JIT), and give guidance around how the authors roughly plan to make that goal. If it wasn’t for that I would have suggested the authors drop the SSA mention and focus on the fact the method switch will take some time, but has maintenance benefits and left it at that.

6 Likes

Not to mention the deferred annotation evaluation introduced in Python 3.14.

In case you are curious, I wrote a little bit about why the type hints don’t help a lot unless you do more work: Compiling typed Python | Max Bernstein

4 Likes

Unless the PEP authors say otherwise, as an admin I’m declaring talking about type hints as a way to improve the JIT as off-topic.

16 Likes

Thank you for the PEP — the candid “Learnings” section especially. I don’t have real experience with JIT internals, so these may be naive questions.

  1. The PEP notes the method frontend “may initially use more memory or perform slightly worse” during the transition. Since the tracing and method frontends presumably differ in when compilation kicks in, not just what gets compiled: will warmup behavior (time to reach steady-state performance) be visible in the nightly benchmark infrastructure during the transition? I’m wondering whether a warmup regression would be caught, or whether pyperformance’s methodology would amortize it away.

  2. Out of curiosity about a property of the new design: since method compilation units are whole code objects rather than recorded execution paths, do you believe the compiled code could in principle be persisted and reused across processes (loosely analogous to .pyc caching), or do fundamental blockers remain, such as runtime addresses burned into the generated code? I fully understand anything like that is well out of scope for this PEP — I’m asking about the design’s properties, not suggesting scope changes. Part of my curiosity is that persistence would move code generation to build or install time, which seems relevant for environments that restrict generating code at runtime.

Thank you!

2 Likes

@eternitygod

Since the tracing and method frontends presumably differ in when compilation kicks in, not just what gets compiled: will warmup behavior (time to reach steady-state performance) be visible in the nightly benchmark infrastructure during the transition? I’m wondering whether a warmup regression would be caught, or whether pyperformance’s methodology would amortize it away.

This is a really good point. We don’t expect warmup to change mainly because the Copy and Patch backend is so fast, so it pays off for having a slightly slower frontend and there’s no need to change warmup. The other thing is that pyperformance likely wouldn’t really catch it anyways unless we cranked up the warmup to unrealistic numbers, as it usually measures performance after warmup not during.

do you believe the compiled code could in principle be persisted and reused across processes

This is another great question! Just to answer your curiosity: the short answer is yes it would work. Caching code and reusing them has been the subject of quite a bit of research for method JITs and there are quite a few papers on them.

5 Likes

(Posting this as myself, not as a Steering Council member.)

Thanks for this PEP, I enjoyed reading it, and I have lots of thoughts on the JIT design[1].

However, this really feels like (at least) two PEPs, and I think it should be split up. I think I understand you want to show that the criteria for supported status are attainable even if we aren’t particularly close right now, but honestly, I don’t think that should matter for the simple goal of setting acceptable criteria. I also don’t think it’s a good idea to mix the two fairly disparate discussions, about the criteria and about how to get there. Fundamentally, the discussion about the minimum expectations for a supported JIT is distinct from how to get there, especially as we have different proposals for how to get there.

We can decide on the criteria for declaring the JIT supported without deciding on how to get there, and we can decide on a different approach for the JIT (like tracing versus method JIT) without it affecting the criteria at all.

The criteria for supported status as laid out in the PEP look pretty good to me. I think the strong requirement on free-threading support[2], the JIT distribution story, the maintenance and tooling and platform support, all of that is important, and I don’t disagree with anything mentioned in the PEP about those.

The timeline given in the PEP is, I think, a little out of place. It combines the performance goals with the implementation paths for the new JIT ideas, and I can’t help but point out that past predictions for performance progress have rarely panned out precisely as predicted. I don’t think we should expect a firm timeline for JIT development. It’s just not how development works in an open-source project, especially not one that cares about backward compatibility[3].

I think the PEP should encapsulate the expectations the community has for the JIT’s benefits and costs, in the form of +X% on benchmarks Y and Z, no more than -U% penalty on startup/memory according to benchmarks V and W. Those goals should be realistic, but they don’t have to be certain, and they don’t have to come with an implementation plan. The purpose is to set some minimum expectations, some goals for the JIT work to aim for. It may very well be that those goals are unattainable at this time, if it turns out that people actually care way more about memory use or startup time or maintainability than they do about performance. Maybe the community wants to see +50% performance before it’ll consider it acceptable to include the JIT in CPython. If that’s the end result of the discussions, then so be it.

I also think the goals should be aspriational, not absolute, and in the end the Steering Council at the time should decide whether the criteria have been sufficiently met.

Personally, I think +20% geometric mean over pyperformance is an excellent goal, although I will point out that pyperformance is far from a rigorous or meticulous collection of benchmarks. (For instance, at the moment, asyncio is quite overrepresented in the result set, a lot of benchmarks run so quickly it’s hard to get accurate results, and there’s a handful of benchmarks that are genuinely bad.) We can’t just hit that goal by changing pyperformance to suit the JIT of course, but I do think we should be open to work on making pyperformance more reflective of real-world programs.

I also think the PEP should be explicit about startup time, warmup time, and memory overhead. Some of that is reflected in pyperformance as it is, but I think they deserve separate, explicit measurements. I don’t think these have to be particularly strict numbers – the JIT is still optional even if it’s supported – but if we don’t track them we don’t know what cost we’re actually paying here.

The other big requirement is, of course, maintainability, or support among the Core team. This is very difficult to quantify and I can see how the talk about the JIT implementation is supposed to address this. Using a stack based JIT makes sense if you look at it from the view point of a CPython developer familiar with the stack based interpreter who needs to learn about the JIT. I do not think that this is important. We should not design our architecture to be most understandable to current Core team members. Instead, we should focus on making whatever design we end up with understandable, and understood by the Core team. We need to make sure enough people work on it and spend time with it. We actually discussed this notion briefly among people working on CinderX, and @mpage and @kddnewton pointed out that attempting to design a JIT to be more easily understood by Core team members makes it less easily understood by others, and makes it harder to apply literature and lessons learned from other JITs to CPython’s.

I will have more thoughts on the JIT implementation discussion, but I need more time to articulate them… In the interest of word count in this post, and given the suggestion to split up the discussion, I’ll hold on to them for now :slight_smile:


  1. probably not surprising, as I sometimes work on CinderX at Meta ↩︎

  2. I know, I know, I’m biased ↩︎

  3. at least the way Python does it, with the expectation of a single installation/build catering to all users ↩︎

7 Likes

Yeah, this is something I’ve definitely spent a lot of time thinking about. At the risk of going off-topic…

Briefly, the existing copy-and-patch backend is fast enough that caching and reusing code probably just isn’t worth the additional complexity. The frontend and middle-end for both designs are pretty fast too, and a lot of the optimizations they perform (inlining, guard removals, various refcount removals, etc.) rely enough on live process info that effective caching at that level would be quite difficult (and again, probably wouldn’t pay off much).

What a method JIT does create as a nice side-effect is a more cache-friendly compilation structure: large, infrequently-compiled regions that are likely to be pretty stable across processes. If we wanted to move to a heavier backend later (waaay outside the scope of this PEP), then caching compiled code across processes (or even machines) becomes an option that can be implemented quite effectively to mitigate the costs of that heavier backend.

So while it doesn’t need anything more than copy-and-patch to be successful, it makes interesting options like caching or heavier backends a lot more practical to experiment with.

…though this use-case in particular is something that would still be much more difficult to do.

1 Like