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 — Supporting stack unwinding in the JIT compiler · Issue #126910 · python/cpython

Supporting stack unwinding in the JIT compiler #126910

New issue
Copy link
New issue
Copy link
Open
Open
Supporting stack unwinding in the JIT compiler#126910
Copy link
Assignees
pablogsalbrandtbucher
Labels
3.14bugs and security fixesbugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-JIT

Description

@pablogsal
pablogsal
opened on Nov 16, 2024
Issue body actions

TLDR

This is a lot of text because the issue is complex but if you want the gist:

To not break a lot of tools that rely on unwinding, I propose to fix this by compiling the JIT stencils with frame pointers which has a trivial maintenance cost (2 lines) and only involves a 2% hit on speed when using the JIT while making almost all existing debuggers and profilers just work™ in the presence of the JIT.

Although this doesn't fix everything sadly I think is the best compromise that I can find (and we are quite lucky to have it as normally fixing this is a nightmare - as in thousands of complex lines of code and 50% slowdown nightmare).


The issue + proposal

CPython's JIT compiler must provide robust stack unwinding support to maintain compatibility with the Python ecosystem's debugging and profiling tools. This requirement is particularly critical given Python's reliance on native extensions written in C, C++, and Rust. As more performance-critical code moves to these native implementations, the ability to properly unwind through mixed Python and native frames becomes fundamental for effective debugging and profiling. This capability is even more crucial with the introduction of both JIT compilation and free-threaded modes, which significantly increase the complexity of runtime behavior. Without proper unwinding support, debugging te issues that can appear in the presence of these new modes becomes extremely challenging – developers would be unable to generate meaningful stack traces, hampering their ability to understand where and why their programs failed. Consider debugging a deadlock in free threadded code and a Rust extension, or investigating a crash in native code called from JIT-compiled Python – without proper unwinding support, error reports would be incomplete or misleading, making production issues significantly harder to diagnose and fix. The ability to get complete, accurate stack traces in these scenarios is not just a convenience; it's essential for maintaining production applications where Python increasingly interacts with native code through multiple execution modes.

Profiling is equally affected – modern performance analysis tools like py-spy, Austin, or eBPF-based solutions need to understand the complete call stack, including JIT-compiled code, native extensions, and regular Python frames, to provide accurate performance insights.

Some of the popular tools that use native unwinding in some way or form:

  • Austin (statistical profiler). Uses libunwind
  • py-spy (statistical profiler).Uses libunwind
  • memray: uses C++ unwinder for macOS and libunwind for Linux
  • pystack: uses elfutils (as is the only one supporting core files).
  • EBPf unwinders : normally rely on frame pointers or rely on the C++ unwinder (https://israelo.io/blog/ehframes/, https://www.polarsignals.com/blog/posts/2022/11/29/dwarf-based-stack-walking-using-ebpf).
  • Additionally, most JIT compilers implement not only frame pointers to make possible to profile and debug, but also implement either C++ exception unwinder via eh_frames or even more hardcore support for gdb and other debuggers.
  • Gdb and hdb-helpers: a combination of libdw, libunwind or custom
  • Perf: frame pointers, lubunwind or libdw
  • Lldb: libunwind or custom
  • valgrind: uses a custom unwinder based on libdw

This doesn't include the considerable amount of custom tools that are not open source either in the debugging or profiling space.

Unwinding Libraries and Their Capabilities

Several libraries provide stack unwinding capabilities, each with its own strengths and limitations.

Library Platforms Local Unwinding Remote Unwinding Core Files Registration API Remote Registration Support Main Users
libunwind Linux only Yes (fast) Yes No _U_dyn_register No Profilers, debuggers
Native Unwinder (LLVM/GDB) Linux, MacOS Yes (optimized) No No __register_frame No C++ exceptions, backtrace()
libdw (elfutils) Linux only Yes Yes Yes None (ELF parsing) No GDB, debuggers like pystack
GDB/LLDB built-in Cross-platform Yes Yes Yes __jit_debug_register_code No Debug tools

Implementation Plan for Stack Unwinding Support

After extensive experimentation and analysis (believe me this was a lot of very difficult research work, @brandtbucher and I found that since we have preserve_none and LLVM 19 we can just compile the JIT stencils with frame pointers and that makes most tools just work. Based on this I propose implementing frame pointer support as the primary strategy for stack unwinding in CPython's JIT. This approach has proven to be the most pragmatic and effective solution, providing broad compatibility with existing tools while maintaining reasonable performance characteristics. I do think we got very lucky this fixes most of the tools since adding unwinding support for JITs otherwise is very challenging (see following sections).

Frame pointer support can be enabled through a minimal change to the JIT compiler flags:

diff --git a/Tools/jit/_targets.py b/Tools/jit/_targets.py
index d8dce0a905c..7c3b2e5aab7 100644
--- a/Tools/jit/_targets.py
+++ b/Tools/jit/_targets.py
@@ -135,6 +135,8 @@ async def _compile(
             # Don't call stack-smashing canaries that we can't find or patch:
             "-fno-stack-protector",
             "-std=c11",
+            "-fno-omit-frame-pointer",
+            "-mno-omit-leaf-frame-pointer",
             "-o",
             f"{o}",
             f"{c}",

This simple change enables compatibility with a wide range of tools:

  • libunwind (both local and remote unwinding)
  • libdw (supporting core file analysis, remote and local unwinding)
  • GDB and LLDB
  • eBPF-based unwinders

Most of the popular tools will just work with this change, instead of having to implement tons of different unwinding information support, which is a nightmare)

Performance Impact

https://github.com/faster-cpython/benchmarking-public/blob/main/results/bm-20241114-3.14.0a1%2B-925b70b-JIT/bm-20241114-linux-x86_64-brandtbucher-justin_frame_pointer-3.14.0a1%2B-925b70b-vs-base.md

The performance impact of this change has been carefully measured:

  • AMD64: Approximately 2% overhead, as shown in recent benchmarks
  • ARM64 and macOS: Even lower overhead due to the presence of dedicated link registers

This modest performance cost is significantly outweighed by the expected JIT performance improvements. Moreover, the maintenance burden is minimal compared to alternative approaches like implementing custom unwinding support for each tool.

Additional Optimization

We can further enhance compatibility by generating eh-frames in the JIT stencils and calling __register_frame_table (a bulk version of __register_frame). Benchmarks show this additional feature has neutral performance impact while enabling native unwinder support. This makes the solution even more robust without additional overhead.

Given these considerations - the minimal implementation complexity, broad tool compatibility, reasonable performance characteristics, and the ability to extend support to native unwinders - frame pointer support represents the optimal path forward for CPython's JIT implementation.

Why this must be activated by default

The decision to enable frame pointers by default is driven by two critical production requirements.

  • When applications crash or hang, tools like pystack need to analyze program state without the luxury of reproduction – you cannot simply "run it again with debug options enabled." You need to unwind the stack either remotely or from a core file.
  • Performance profiling in production requires safely inspecting program state from another process or the kernel. Tools like Austin, py-spy, and eBPF-based profilers need to periodically sample stack traces without interrupting the target process. Both scenarios require frame pointers to work reliably.

This must be enabled by default because most Python users don't compile Python themselves – they install it through package managers or tools like uv. Unless frame pointers are enabled by default, these critical debugging and profiling capabilities won't be available in most Python installations, severely impacting the quality of bug reports to both CPython and C extensions. Given the minimal performance impact of 2% frame pointers should be enabled by default, following the principle of being "safe by default." While we should provide the option to disable them, this should be a conscious decision made by end users who understand they're trading away the ability to properly profile and debug their applications in production – not a choice made by intermediate distributors or package managers.

Additional information

Using frame pointers aligns with a broader industry trend where major tech companies and Linux distributions are reverting previous optimization decisions and re-enabling frame pointers. Companies like Meta and Google now compile their entire software stack, including their Python installations, with frame pointers enabled. This shift is driven by the recognition that observability and debugging capabilities in production environments far outweigh the minor performance impact of frame pointers. Ubuntu has also adopted this approach, now compiling virtually all packages with frame pointers enabled (with Python being a notable exception we aim to address). This industry-wide move reflects a fundamental reality: in modern production environments, the ability to profile, monitor, and debug applications effectively is crucial, and frame pointers provide the most reliable and universal way to achieve this. The performance cost, typically around 1-5%, is normally considered to be a worthwhile trade-off for the improved observability they enable. This is particularly relevant for server workloads where understanding performance characteristics and debugging production issues is far more valuable than the small overhead frame pointers introduce. The fact that major tech companies maintain this policy even for performance-critical services demonstrates that the benefits of comprehensive profiling and debugging support outweigh the minimal performance impact. Some links:

https://ubuntu.com/blog/ubuntu-performance-engineering-with-frame-pointers-by-default
https://www.brendangregg.com/blog/2024-03-17/the-return-of-the-frame-pointers.html
https://www.polarsignals.com/blog/posts/2023/12/13/embracing-frame-pointers-in-ubuntu-24-04-lts


Background on Stack Unwinding Support Requirements

Adding JIT compilation to CPython requires careful consideration of stack unwinding support. Without proper unwinding capabilities, we risk breaking compatibility with essential development tools that Python developers rely on daily. To understand the scope of this requirement, we need to examine the landscape of tools that depend on stack unwinding.

Tools Requiring Stack Unwinding

Stack unwinding is a critical capability used by three major categories of tools in the Python ecosystem:

  • Debuggers (GDB, LLDB, pystack) rely on unwinding to show developers where their program is currently executing. These tools deal with remote processes or core files.

  • Profilers use unwinding to understand program performance.

  • C++ Exception Handling depends on unwinding information to:

    • Propagate exceptions through the stack
    • Clean up resources during stack unwinding

Unwinding Mechanisms

Stack unwinding occurs in two fundamentally different contexts:

  • In-process unwinding:

    • Code examines its own stack
    • Used by tracing profilers and exception handlers
    • Has direct access to program memory and CPU registers
    • Primarily used for live analysis
  • Out-of-process unwinding:

    • External program examines another program's stack
    • Used by debuggers and statistical profilers
    • Includes core dump analysis

Types of Analysis Tools

The tools that perform unwinding can be categorized by their data collection method:

  • Tracing Profilers:

    • Run inside the target process
    • Instrument function entries and exits
    • Collect detailed execution information
    • Higher overhead but more precise
    • Rely on local unwinding capabilities
    • Examples: cProfile, memray
  • Statistical Profilers:

    • Run outside the target process
    • Sample program state periodically
    • Much lower overhead
    • Suitable for production use
    • Growing eBPF ecosystem:
      • Enables efficient system-wide analysis
      • Becoming standard for production monitoring
      • Requires reliable stack unwinding support
      • Examples: perf, py-spy, bcc tools

Unwinding Libraries and Their Capabilities

Several libraries provide stack unwinding capabilities, each with its own strengths and limitations. The strategies to support unwinding for JIT compiled code are:

  • Allow frame pointers: makes libdw, gdb, lldb and lubinwind work. libunwind and libdw work in remote mode, including core files.

  • Per unwinder support for dynamically generated code using DWARF (only local mode and only if it supports it - libdw has no support):

    • libunwind: Construct ELF .debug_frame data structures, and table_entry data structures, and a unw_dyn_table_info data structure, and a unw_dyn_info_t structure, then call _U_dyn_register.
    • C++ exception unwinder: Construct ELF .eh_frame data structures, then call __register_frame
    • GDB: Construct a full in-memory ELF object, manually maintain a doubly-linked list of all such objects in a global variable called __jit_debug_descriptor, and call a global function called __jit_debug_register_code when this list is changed

Any other library has no support.

Other JITs

Most JIT compilers implement not only frame pointers to make it possible to profile and debug, but also implement either C++ exception unwinder via eh_frames or even more hardcore support for gdb and other debuggers. Some links:

LuaJIT: https://github.com/LuaJIT/LuaJIT/blob/fe71d0fb54ceadfb5b5f3b6baf29e486d97f6059/src/lj_err.c#L620
DotNet: https://github.com/dotnet/coreclr/pull/468/files
DotNet remote unwinding: https://github.com/dotnet/runtime/blob/d9855f27f7d5d5138452009e9f0dd7a81c5c8b74/src/coreclr/pal/src/exception/remote-unwind.cpp
Julia (frame pointers and __register_frame): https://github.com/JuliaLang/julia/blob/caa2f7d52b430f50c8038a7f6766edba28a3fb65/src/debuginfo.cpp#L554
LLVM's ORC jit (frame pointers, __register_frame and much more): https://github.com/llvm/llvm-project/blob/935d753c6dca0cd9bc5ea14fde5b00386ebcc5be/compiler-rt/lib/orc/elfnix_platform.cpp#L191
V8: Frame pointers, __register_frame and much more: https://www.kvakil.me/posts/2022-10-17-ustackjs-unwind-javascript-stacks-in-ebpf.html, https://github.com/v8/v8/blob/b70457462cb22753a011096c9c9be20275dc4437/src/diagnostics/gdb-jit.cc#L1674, https://github.com/v8/v8/blob/b70457462cb22753a011096c9c9be20275dc4437/src/diagnostics/unwinder.cc#L100, https://github.com/v8/v8/blob/b70457462cb22753a011096c9c9be20275dc4437/src/diagnostics/eh-frame.cc

Linked PRs

  • GH-126910: Allow native profilers and debuggers to unwind through JIT frames #143548
  • GH-126910: add test for manual frame unwinding #144137
  • GH-126910: Add gdb support for unwinding JIT frames #146071
  • GH-126910: reserve FP on AArch64 when generating JIT stencils #146520
  • GH-126910: avoid reading the FP for getting the SP #146521
  • gh-126910: Verify that JIT stencils preserve frame pointer #146524
  • GH-126910: Make _Py_get_machine_stack_pointer return the stack pointer #147945
  • GH-126910: Revert "Make _Py_get_machine_stack_pointer return the stack pointer (#147945)" #147994
  • GH-126910: Increase default stack size for linux platforms without working HAVE_PTHREAD_GETATTR_NP #148002
  • GH-126910: Build/link the JIT shim in the Python interpreter #148872
  • GH-126910: Make _Py_get_machine_stack_pointer return the actual stack pointer #149103
  • GH-126910: Add GNU backtrace support for unwinding JIT frames #149104
Reacted by Brandt Bucher, Savannah Ostrowski, Brett Cannon, Arthur Pastel, Yury Selivanov, Mikhail Efimov, Erlend E. Aasland, Bojun Ren, Bartosz Sławecki, Chris Eibl and 2 more
Reacted by Vasily Ryabov, Rodolfo De Nadai, Filipe Laíns, Kirill Podoprigora, Sergei Iakhnitskii, Daniel Hollas, Erlend E. Aasland, Chris Eibl, Markus Stange and Gabriel Corona

Activity

Next
  1. Eclips4
    added
    topic-JIT
    interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
    type-featureA feature request or enhancementA feature request or enhancement
    on Nov 16, 2024
  2. pablogsal
    removed
    type-featureA feature request or enhancementA feature request or enhancement
    on Nov 16, 2024
  3. pablogsal
    assigned
    pablogsal
    and
    brandtbucher
    on Nov 16, 2024
  4. brandtbucher commented on Nov 16, 2024

    @brandtbucher
    brandtbucher
    on Nov 16, 2024
    Member
    More actions

    Just to clarify for anyone reading, it is sufficient to only enable frame pointers in JIT code, not necessarily the entire interpreter.

    After spending a week with @pablogsal digging deep into this issue and prototyping and evaluating the different options available to us (including doing nothing), my personal opinion is that this issue is fixing a behavioral change, not adding a feature. It's also my personal opinion that the two-line diff above and the 2% hit are a good compromise that unblocks adoption of the JIT, and I think it's the best path forward. We have explored strategies to emit DWARF as well; copy-and-patch makes harvesting and emitting correct DWARF not-too-difficult, but I also see it as an optional, lower priority nice-to-have.

    Frame pointers are a convenient escape hatch; for many tools that still don't work, we can at least point at frame pointers and argue that what the tools want should be possible using them. Currently, what they want is impossible without disabling the JIT... and we want people to turn it on!

    Turning on frame pointers in the naive way above also does not preclude the possibility of improving the way we emit frame pointer code in the future should we decide it's worth clawing back that 2% in exchange for additional implementation complexity; such a patch can stand on its own merits. It also shouldn't preclude a runtime option to opt in or out of them using an environment variable (again, though, I'm not sure this is worth additional complexity right now to compile two versions of every stencil to make this work, but it's certainly possible).

    I'd just like to thank @pablogsal for patiently educating me on this deep, dark, expansive area (and the people who rely on it) and working with me to explore solutions and compromise on something workable.

    Reacted by Pablo Galindo Salgado, Savannah Ostrowski, Rodolfo De Nadai, Brett Cannon, Yury Selivanov, Mikhail Efimov, Diego Russo, mpage, Alyssa Coghlan, Erlend E. Aasland and 3 more
  5. brandtbucher
    added
    3.14bugs and security fixesbugs and security fixes
    on Nov 16, 2024
  6. brandtbucher commented on Nov 16, 2024

    @brandtbucher
    brandtbucher
    on Nov 16, 2024
    Member
    More actions

    Also, it's kind of neat that we've pretty much followed through with what PEP 744 had to say about this issue:

    Since the code templates emitted by the JIT are compiled by Clang, it may be possible to allow JIT frames to be traced through by simply modifying the compiler flags to use frame pointers more carefully. It may also be possible to harvest and emit the debugging information produced by Clang. Neither of these ideas have been explored very deeply.

    While this is an issue that should be fixed, fixing it is not a particularly high priority at this time. This is probably a problem best explored by somebody with more domain expertise in collaboration with those maintaining the JIT, who have little experience with the inner workings of these tools.

    Reacted by Pablo Galindo Salgado
  7. pablogsal commented on Nov 16, 2024

    @pablogsal
    pablogsal
    on Nov 16, 2024
    MemberAuthor
    More actions

    Also, it's kind of neat that we've pretty much followed through with what PEP 744 had to say about this issue:

    Maybe except this part 😆 :

    Neither of these ideas have been explored very deeply.

    Now all of these ideas have been explored deeply. We have been through some stuff 😨

    Reacted by Mikhail Efimov
  8. python
    deleted a comment from maleycl on Nov 16, 2024
  9. brandtbucher commented on Nov 16, 2024

    @brandtbucher
    brandtbucher
    on Nov 16, 2024
    Member
    More actions

    Benchmarks on JIT frame pointers are in:

    • aarch64-apple-darwin: 2.1% slower
    • aarch64-unknown-linux-gnu: 6.1% slower
    • x86_64-unknown-linux-gnu: 2.1-3.1% slower
    • x86_64-pc-windows-msvc: 2.6% slower
    • i686-pc-windows-msvc: 2.2% slower

    Looks like a 2-3% performance hit, with the clear outlier of aarch64-unknown-linux-gnu at around a 6% hit.

    Reacted by Diego Russo
  10. python
    deleted a comment from maleycl on Nov 16, 2024
  11. markshannon commented on Nov 18, 2024

    @markshannon
    markshannon
    on Nov 18, 2024
    Member
    More actions

    According to our profiling jitted code is only ~14% of the execution time, so a 3% slowdown is a ~20% slowdown in the jitted code.

    I think that's too slow.

    Reacted by Bruno Cabral
  12. pablogsal commented on Nov 18, 2024

    @pablogsal
    pablogsal
    on Nov 18, 2024
    MemberAuthor
    More actions

    Then what do you propose? I don't think we have an alternative here. This is just the cost of not breaking existing tools and honestly I think we are extremely lucky to have a way to fix it that's just 2 lines with 2% hit.

  13. 56 remaining items

  14. ljfp
    added 3 commits that reference this issue on Apr 25, 2026

    pythonGH-126910: Make `_Py_get_machine_stack_pointer` return the stac…

    02f04b2

    pythonGH-126910: Revert "Make `_Py_get_machine_stack_pointer` return …

    eb941df

    pythonGH-126910: Build/link the JIT shim in the Python interpreter (p…

    414a501
  15. diegorusso
    added 3 commits that reference this issue on Apr 28, 2026

    pythonGH-126910: Add gdb support for unwinding JIT frames

    b97b769

    pythonGH-126910: Add GNU backtrace support for unwinding JIT frames

    fd1c59a

    pythonGH-126910: Derive JIT unwind info from shim CFI

    13db9a9
  16. pablogsal
    added 3 commits that reference this issue on May 2, 2026

    pythongh-126910: Reset JIT GDB mutex earlier after fork

    27da8e5

    pythongh-126910: Serialize perf jitdump initialization

    9df4d7d

    GH-126910: Add gdb support for unwinding JIT frames (#146071)

    c7b7ca2
  17. diegorusso
    added 2 commits that reference this issue on May 2, 2026

    pythonGH-126910: Add GNU backtrace support for unwinding JIT frames

    db967dc

    GH-126910: Add GNU backtrace support for unwinding JIT frames (#149104)

    1e5d942
  18. hugovk
    added a commit that references this issue on May 7, 2026

    gh-126910: Update 3.15 what's new to include GDB/backtrace integration (

    299cf3c
  19. markshannon
    added a commit that references this issue on May 26, 2026

    GH-126910: Make `_Py_get_machine_stack_pointer` return the actual sta…

    a679366
  20. dylan-conway
    added a commit that references this issue on Jul 23, 2026

    crash_handler(windows): let foreign first-chance AVs reach SEH via JS…

    79ee451
  21. liooil
    added a commit that references this issue on Aug 7, 2026

    crash_handler(windows): let foreign first-chance AVs reach SEH via JS…

    c25451d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

  • @brandtbucher
    brandtbucher
  • @pablogsal
    pablogsal

Labels

3.14bugs and security fixesbugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-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.