Skip to content

_PyEval_EvalFrameDefault not in the call stack when interpreter is built with --with-tail-call-interp #161827

Open

Description

@ikrommyd

Found in numpy in numpy/numpy#33290

Numpy uses a "hack" to do temporary elision for arithmetic expressions to avoid allocating memory for temporaries. One thing it does to enable this elision is to detect "Am I being called directly by the python interpreter?" and part of that check is the question, "Is _PyEval_EvalFrameDefault in the call stack?".
Now on my macos machine for example with clang, If I'm on an interpreter that was built with --with-tail-call-interp such as the conda-forge build of cpython, _PyEval_EvalFrameDefault is no longer in the call stack and temporary elision is disabled so users lose this nice performance boost. On that issue I was informed that _PyEval_EvalFrameDefault should still be in the call stack and in turns out that this is probably PGO inlining as marking _PyEval_EvalFrameDefault with Py_NO_INLINE brings it back in the call stack. Is there any chance cpython can do something about that? Such as making the function Py_NO_INLINE? I'd be happy to send a patch if that's a fine approach.

Here are some call stacks with lldb to showcase my point that I got with spin lldb -- $(pyenv which python) -P -c 'import numpy as np; n = 1<<16; np.ones(n) + np.ones(n) + np.ones(n)'

Without tail-call interpeter:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
   * frame #0: 0x0000000106372710 _multiarray_umath.cpython-314-darwin.so`check_callers(cannot=0x000000016fdfd020) at temp_elide.c:165:9
     frame #1: 0x0000000106372460 _multiarray_umath.cpython-314-darwin.so`can_elide_temp(olhs=0x0000000105273390, orhs=0x00000001052f40f0, cannot=0x000000016fdfd020) at temp_elide.c:348:20
     frame #2: 0x0000000106372018 _multiarray_umath.cpython-314-darwin.so`try_binary_elide(m1=0x0000000105273390, m2=0x00000001052f40f0, inplace_op=(_multiarray_umath.cpython-314-darwin.so`array_inplace_add at number.c:517), res=0x000000016fdfd060, commutative=1) at temp_elide.c:366:9
     frame #3: 0x0000000106358e08 _multiarray_umath.cpython-314-darwin.so`array_add(m1=0x0000000105273390, m2=0x00000001052f40f0) at number.c:243:9
     frame #4: 0x0000000100063fbc python`PyNumber_Add + 92
     frame #5: 0x00000001001f1158 python`_PyEval_EvalFrameDefault + 528
     frame #6: 0x00000001001efca4 python`_PyEval_Vector + 576
     frame #7: 0x00000001001ef9c0 python`PyEval_EvalCode + 160
     frame #8: 0x00000001003b3a48 python`run_mod + 232
     frame #9: 0x00000001003b387c python`_PyRun_String + 192
     frame #10: 0x00000001002a7c58 python`_PyRun_SimpleString + 156
     frame #11: 0x00000001002d4f28 python`Py_RunMain + 416
     frame #12: 0x00000001003b7ab0 python`pymain_main + 464
     frame #13: 0x0000000100000954 python`main + 44
     frame #14: 0x00000001838b0d7c dyld`start + 6652

And with tail-call interpeter:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
   * frame #0: 0x000000010639e710 _multiarray_umath.cpython-314-darwin.so`check_callers(cannot=0x000000016fdfd6d0) at temp_elide.c:165:9
     frame #1: 0x000000010639e460 _multiarray_umath.cpython-314-darwin.so`can_elide_temp(olhs=0x000000010529f390, orhs=0x00000001053200f0, cannot=0x000000016fdfd6d0) at temp_elide.c:348:20
     frame #2: 0x000000010639e018 _multiarray_umath.cpython-314-darwin.so`try_binary_elide(m1=0x000000010529f390, m2=0x00000001053200f0, inplace_op=(_multiarray_umath.cpython-314-darwin.so`array_inplace_add at number.c:517), res=0x000000016fdfd710, commutative=1) at temp_elide.c:366:9
     frame #3: 0x0000000106384e08 _multiarray_umath.cpython-314-darwin.so`array_add(m1=0x000000010529f390, m2=0x00000001053200f0) at number.c:243:9
     frame #4: 0x00000001000658cc python`PyNumber_Add + 92
     frame #5: 0x000000010034df80 python`_TAIL_CALL_BINARY_OP + 68
     frame #6: 0x00000001001ec4c4 python`_PyEval_Vector + 820
     frame #7: 0x00000001001ec0ec python`PyEval_EvalCode + 160
     frame #8: 0x0000000100382978 python`run_mod + 232
     frame #9: 0x00000001003827ac python`_PyRun_String + 192
     frame #10: 0x0000000100253544 python`_PyRun_SimpleString + 156
     frame #11: 0x000000010028071c python`Py_RunMain + 416
     frame #12: 0x000000010038693c python`pymain_main + 464
     frame #13: 0x0000000100000954 python`main + 44
     frame #14: 0x00000001838b0d7c dyld`start + 6652

cc @ngoldbaum @Fidget-Spinner

Linked PRs

Activity

  1. Fidget-Spinner commented on Nov 11, 2026

    @Fidget-Spinner
    Member

    I think the Py_NO_INLINE with a comment explaining we need to preserve the function in the call stack makes sense. _PyEval_EvalFrameDefault is special not just for stack elision as well, but for anything that needs to walk the call stack, including stack unwinders and profilers and such, as it's commonly treated as a special sentinel by these tools. We need to backport to 3.14 and 3.15 at the very least.

  2. added
    buildThe build process and cross-build
    3.14bugs and security fixes
    3.15bugs and security fixes
    3.16new features, bugs and security fixes
    on Nov 11, 2026
  3. ikrommyd commented on Nov 11, 2026

    @ikrommyd
    ContributorAuthor

    Opened the PR. See you there :)

  4. added a commit that references this issue on Nov 11, 2026
  5. added a commit that references this issue on Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

3.14bugs and security fixes3.15bugs and security fixes3.16new features, bugs and security fixesbuildThe build process and cross-build

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions