GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Improving JIT code quality · Issue #115802 · python/cpython

Improving JIT code quality #115802

New issue
Copy link
New issue
Copy link
Closed
Closed
Improving JIT code quality#115802
Copy link
Assignees
brandtbucher
Labels
3.13only security fixesonly security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagePerformance or resource usagetopic-JIT

Description

@brandtbucher
brandtbucher
opened on Feb 22, 2024
Issue body actions

We have a JIT compiler, but the quality of the templates needs improvement. Here are some areas that I plan on working on (note that these strictly pertain to machine code quality, not improvements to tier two in general):

  • Stop using the "large" code model.
    • GH-115802: JIT "small" code for macOS and Linux #115826
    • GH-115802: JIT "small" code for Windows #115964
    • Try emitting trampolines for out-of-range branches on AArch64.
    • Try emitting trampolines for out-of-range branches on x86_64.
  • Remove zero-length jumps when tail-calling between templates.
    • GH-115802: Don't JIT zero-length jumps #116177
    • aarch64-apple-darwin
    • aarch64-pc-windows-msvc
    • Investigate slowdown on i686-pc-windows-msvc.
  • Use LLVM's ghccc/preserve_none calling convention when tail-calling between templates.
  • Split hot and cold parts of the templates. #116422
  • Cache top-of-stack values in registers when tail-calling between templates.

The nice thing about these improvements is that they compound. For example, getting rid of 64-bit register-indirect jumps (the "large" code model) in the first step will make all of the following improvements even more potent .

Stop using the "large" code model.

Since our jitted code is mapped into a 64-bit address space, we're not able to compile our templates using the default "small" code model that assumes a 31-bit address space (and can thus use 32-bit relative addressing). This forces the templates to use convoluted code patterns to make sure that this 64-bit assumption is met for all relocations, typically using two large instructions (load the wide value into a register, then actually use it) where only one should be necessary:

Example of current code.

Unfortunately, many of the relocations we perform are values that can comfortably fit in 32 signed bits, meaning that this code bloat is pure unnecessary overhead in critical parts of the code.

What we can do instead is generate position-independent code, which is already the default for most platforms. On the surface, this seems less efficient, since it means that every value we patch will be loaded indirectly from memory (even though the code itself is smaller):

Example of position-independent code.

However, notice that the relocations that are emitted are relaxable. This means that if the oparg value and continuation address are small enough, we can (quite easily) relax mov eax, dword ptr [rip + AAA] into lea eax, [rip + XXX] and jmp qword ptr [rip + BBB] into jmp YYY; nop. This turns both memory loads into signed 32-bit PC-relative immediate values, and gives us some good quality code:

8D 05 XX XX XX XX lea eax, [rip + XXXXXXXX]
01 07             add dword ptr [rdi], eax
E9 YY YY YY YY    jmp YYYYYYYY
90                nop

Since macOS JIT builds already use position-independent code, this isn't very difficult to implement. The two main "new" bits are:

  • Implementing the actual relaxations.
  • Achieving the same effect on Windows JIT builds (via __declspec(dllimport)).

(As a bonus, we can probably use something similar to PLT stubs to relax all jumps and calls without too much fuss, since we already emit these sorts of trampolines for aarch64-unknown-linux-gnu builds. I haven't looked into this too closely to see what it will win us performance-wise.)

Benchmarking runs suggest that this change will give us a ~1-3% performance improvement, depending on platform.

Remove zero-length jumps when tail-calling between templates.

This is easier once we've performed the above step, since we no longer need to worry about two-instruction register-indirect jumps (which are tricky to elide correctly). In the above code, we can trivially remove the jmp and nop instructions, giving us:

8D 05 XX XX XX XX lea eax, [rip + XXXXXXXX]
01 07             add dword ptr [rdi], eax

Benchmarking suggests that this change will give us an additional ~1-3% performance improvement.

Use LLVM's ghccc/preserve_none calling convention when tail-calling between templates.

LLVM has cool calling conventions that have no callee-saved registers, and pass as many arguments in registers as possible. This is super efficient for our tail calls, and is a big part of the original copy-and-patch paper.

Clang 19 will ship with support for this __attribute__((preserve_none)) calling convention, but not until much later this year. In the meantime, hacks exist (by dropping briefly into LLVM IR at build time and changing the convention manually).

Benchmarking suggests that this change will give us an additional ~0-6% performance improvement (no wins on aarch64, moderate wins on x86_64, and big wins on i686).

Split hot and cold parts of the templates.

Our stencils contain a lot of error-handling and deoptimization code. We can make the hot code more compact and linear by "jumping away" to shared cold stubs for these paths (and these jumps will be more efficient with the above improvements).

This isn't too difficult, since we already have two "segments" of jitted code (executable code and read-only data). This just adds a third.

Early experiments suggest a ~1-2% performance improvement with this approach.

Cache top-of-stack values in registers when tail-calling between templates.

See this paper for inspiration.

This is a bigger project, but will likely be one of the bigger wins here. It is probably made much easier by our fancy interpreter generator, which should be a decent framework for generating variants of instructions with certain inputs and outputs living outside of the value stack, and generating lookup tables for automatically replacing them during trace formation.

The above step of changing the calling convention will be helpful here, since we'll get a lot more registers to pin stack items in across tail calls. We can probably do this in a way that works for the tier two interpreter as well.

No idea what this will win us, but it should be good given the amount of memory traffic that's being reduced to and from interpreter frames.

Linked PRs

  • GH-115802: JIT "small" code for macOS and Linux #115826
  • GH-115802: JIT "small" code for Windows #115964
  • GH-115802: Don't JIT zero-length jumps #116177
  • GH-115802: Reduce the size of _INIT_CALL_PY_EXACT_ARGS. #116856
  • GH-115802: Use the GHC calling convention in JIT code #118287
  • GH-115802: JIT using the "medium" code model on x86_64-unknown-linux-gnu #130097
  • GH-115802: Optimize JIT stencils for size #136393
  • GH-115802: Remove no-plt for Linux AArch64 #148238
Reacted by Michael Sasser, Máté Gyöngyösi, Derek Visch, Revathy Venugopal, benedikt-bartscher, Eric Zhang and Joshua von Damm
Reacted by amazigh, noam-better, Derek Visch, Riccardo Belli, Abhijeet Pal, Eric Zhang and Joshua von Damm
Reacted by Rene Leonhardt, Derek Visch, Eric Zhang, Maksim Ivanov and Joshua von Damm

Activity

Next
  1. brandtbucher
    added
    performancePerformance or resource usagePerformance or resource usage
    interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
    3.13only security fixesonly security fixes
    on Feb 22, 2024
  2. brandtbucher
    self-assigned this
    on Feb 22, 2024
  3. brandtbucher commented on Feb 23, 2024

    @brandtbucher
    brandtbucher
    on Feb 23, 2024
    MemberAuthor
    More actions

    Upon discussion with @markshannon, it seems like both hot-cold splitting and top-of-stack caching can be done in the tier two IR, with minimal JIT surgery. That seems preferable, since it keeps the JIT simpler, the optimizer may be able to do something with it, and the same schemes can be used in the tier two interpreter. We'll probably need similar integration for frame reconstruction anyways.

  4. brandtbucher
    added 3 commits that reference this issue on Feb 26, 2024

    GH-115802: JIT "small" code for macOS and Linux (GH-115826)

    7259480

    GH-115802: JIT "small" code for Windows (GH-115964)

    f0df35e

    GH-115802: Don't JIT zero-length jumps (GH-116177)

    981f27d
  5. woodruffw
    added 3 commits that reference this issue on Mar 4, 2024

    pythonGH-115802: JIT "small" code for macOS and Linux (pythonGH-115826)

    9b82f99

    pythonGH-115802: JIT "small" code for Windows (pythonGH-115964)

    29f4b45

    pythonGH-115802: Don't JIT zero-length jumps (pythonGH-116177)

    0895028
  6. markshannon
    added a commit that references this issue on Mar 15, 2024

    GH-115802: Reduce the size of _INIT_CALL_PY_EXACT_ARGS. (GH-116856)

    950667e
  7. vstinner
    added a commit that references this issue on Mar 20, 2024

    pythonGH-115802: Reduce the size of _INIT_CALL_PY_EXACT_ARGS. (python…

    dc3ba9d
  8. adorilson
    added 2 commits that reference this issue on Mar 25, 2024

    pythonGH-115802: JIT "small" code for macOS and Linux (pythonGH-115826)

    a12e0a7

    pythonGH-115802: JIT "small" code for Windows (pythonGH-115964)

    8a9dd45
  9. 8 remaining items

  10. AlexWaygood
    added
    topic-JIT
    on May 30, 2024
  11. Fidget-Spinner commented on Dec 29, 2024

    @Fidget-Spinner
    Fidget-Spinner
    on Dec 29, 2024
    Member
    More actions

    @brandtbucher It seems that -fno-plt is producing worse code.

    For example, _GUARD_BOTH_INT with -fno-plt:

        // 
        // _GUARD_BOTH_INT.o:     file format elf64-x86-64
        // 
        // Disassembly of section .text:
        // 
        // 0000000000000000 <_JIT_ENTRY>:
        // 0: 49 8b 45 f0                   movq    -0x10(%r13), %rax
        // 4: 48 8b 40 08                   movq    0x8(%rax), %rax
        // 8: 48 3b 05 00 00 00 00          cmpq    (%rip), %rax            # 0xf <_JIT_ENTRY+0xf>
        // 000000000000000b:  R_X86_64_REX_GOTPCRELX       PyLong_Type-0x4
        // f: 75 17                         jne     0x28 <_JIT_ENTRY+0x28>
        // 11: 49 8b 45 f8                   movq    -0x8(%r13), %rax
        // 15: 48 8b 40 08                   movq    0x8(%rax), %rax
        // 19: 48 3b 05 00 00 00 00          cmpq    (%rip), %rax            # 0x20 <_JIT_ENTRY+0x20>
        // 000000000000001c:  R_X86_64_REX_GOTPCRELX       PyLong_Type-0x4
        // 20: 75 06                         jne     0x28 <_JIT_ENTRY+0x28>
        // 22: ff 25 00 00 00 00             jmpq    *(%rip)                 # 0x28 <_JIT_ENTRY+0x28>
        // 0000000000000024:  R_X86_64_GOTPCRELX   _JIT_CONTINUE-0x4
        // 28: ff 25 00 00 00 00             jmpq    *(%rip)                 # 0x2e <_JIT_ENTRY+0x2e>
        // 000000000000002a:  R_X86_64_GOTPCRELX   _JIT_JUMP_TARGET-0x4
    

    Note: it's 2 jumps to deopt! And one jump to the next instruction.

    Versus without -fno-plt:

        // 
        // _GUARD_BOTH_INT.o:     file format elf64-x86-64
        // 
        // Disassembly of section .text:
        // 
        // 0000000000000000 <_JIT_ENTRY>:
        // 0: 49 8b 45 f0                   movq    -0x10(%r13), %rax
        // 4: 48 8b 40 08                   movq    0x8(%rax), %rax
        // 8: 48 3b 05 00 00 00 00          cmpq    (%rip), %rax            # 0xf <_JIT_ENTRY+0xf>
        // 000000000000000b:  R_X86_64_REX_GOTPCRELX       PyLong_Type-0x4
        // f: 0f 85 00 00 00 00             jne     0x15 <_JIT_ENTRY+0x15>
        // 0000000000000011:  R_X86_64_PLT32       _JIT_JUMP_TARGET-0x4
        // 15: 49 8b 45 f8                   movq    -0x8(%r13), %rax
        // 19: 48 8b 40 08                   movq    0x8(%rax), %rax
        // 1d: 48 3b 05 00 00 00 00          cmpq    (%rip), %rax            # 0x24 <_JIT_ENTRY+0x24>
        // 0000000000000020:  R_X86_64_REX_GOTPCRELX       PyLong_Type-0x4
        // 24: 0f 85 00 00 00 00             jne     0x2a <_JIT_ENTRY+0x2a>
        // 0000000000000026:  R_X86_64_PLT32       _JIT_JUMP_TARGET-0x4
        // 2a: e9 00 00 00 00                jmp     0x2f <_JIT_ENTRY+0x2f>
        // 000000000000002b:  R_X86_64_PLT32       _JIT_CONTINUE-0x4
    

    Now it's only 1 jump to deopt, and zero jumps to the next instruction.

    I sorta understand you want to emit a "trampoline" for out-of-range branches but I don't know how to do that. Is there any way I can help? Or would you like to do this?

  12. LukasWoodtli
    added a commit that references this issue on Jan 22, 2025

    pythonGH-115802: JIT "small" code for macOS and Linux (pythonGH-115826)

    100b014
  13. brandtbucher
    added a commit that references this issue on Mar 5, 2025

    GH-115802: JIT using the "medium" code model on x86_64-unknown-linux-…

    5d8db36
  14. brandtbucher
    added a commit that references this issue on Jul 9, 2025

    GH-115802: Optimize JIT stencils for size (GH-136393)

    c49dc3b
  15. AndPuQing
    added a commit that references this issue on Jul 11, 2025

    pythonGH-115802: Optimize JIT stencils for size (pythonGH-136393)

    21b1d77
  16. Pranjal095
    added a commit that references this issue on Jul 12, 2025

    pythonGH-115802: Optimize JIT stencils for size (pythonGH-136393)

    2e4718c
  17. picnixz
    added a commit that references this issue on Jul 13, 2025

    pythonGH-115802: Optimize JIT stencils for size (pythonGH-136393)

    02855b9
  18. taegyunkim
    added a commit that references this issue on Aug 4, 2025

    pythonGH-115802: Optimize JIT stencils for size (pythonGH-136393)

    46aadc8
  19. Agent-Hellboy
    added a commit that references this issue on Aug 19, 2025

    pythonGH-115802: Optimize JIT stencils for size (pythonGH-136393)

    febc81b
  20. diegorusso
    added 2 commits that reference this issue on Apr 8, 2026

    pythonGH-115802: Remove no-plt for Linux targets

    ba32120

    GH-115802: Remove no-plt for Linux AArch64 (#148238)

    e11315d
  21. ljfp
    added a commit that references this issue on Apr 25, 2026

    pythonGH-115802: Remove no-plt for Linux AArch64 (python#148238)

    05b7318
  22. markshannon commented on Jun 1, 2026

    @markshannon
    markshannon
    on Jun 1, 2026
    Member
    More actions

    I think the only thing left here is to ensure we always emit correct trampolines, but that's covered by #149316

    Reacted by Rene Leonhardt
  23. markshannon
    closed this as completedon Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

  • @brandtbucher
    brandtbucher

Labels

3.13only security fixesonly security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagePerformance or resource usagetopic-JIT

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.