Repository navigation
Description
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
Activity
Fidget-Spinner commentedon Nov 11, 2026
I think the
Py_NO_INLINEwith a comment explaining we need to preserve the function in the call stack makes sense._PyEval_EvalFrameDefaultis 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.ikrommyd commentedon Nov 11, 2026
Opened the PR. See you there :)
gh-161827: Mark `_PyEval_EvalFrameDefault` as `Py_NO_INLINE` (GH-161829)
pythongh-161827: Mark `_PyEval_EvalFrameDefault` as `Py_NO_INLINE` (p…