Repository navigation
Calls with 16+ keyword arguments are ~4.5x slower than calls with 15 #155141
Description
Bug report
Bug description:
A function call with 16 or more keyword arguments compiles to a
different, slower call opcode than the same call with 15. The switch
happens at an exact, fixed cutoff, not a gradual slowdown: adding one
more keyword argument (15 -> 16) makes the call ~4.5x slower.
codegen_call_helper_impl() in Python/codegen.c picks the call opcode
based on this check:
https://github.com/python/cpython/blob/main/Python/codegen.c#L4430
if (nelts + nkwelts*2 > _PY_STACK_USE_GUIDELINE) {
goto ex_call;
}nelts is the number of positional args, nkwelts the number of
keyword args, _PY_STACK_USE_GUIDELINE is 30. A call with only keyword
arguments trips this at 16. Above the threshold the compiler builds a
tuple+dict and calls CALL_FUNCTION_EX; below it, it just uses
CALL_KW.
Reproducer
import dis
import timeit
N_CALLS = 200_000
N_REPEATS = 21
for n in range(12, 21):
params = ", ".join(f"k{i}" for i in range(n))
kwargs = ", ".join(f"k{i}=1" for i in range(n))
namespace = {}
exec(f"def callee({params}): pass", namespace)
exec(f"def caller(): return callee({kwargs})", namespace)
caller = namespace["caller"]
call_instr = next(
instr for instr in dis.get_instructions(caller)
if instr.opname.startswith("CALL")
)
best = min(timeit.repeat(caller, number=N_CALLS, repeat=N_REPEATS))
time_ns = best / N_CALLS * 1e9
print(f"n ={n:>3} {call_instr.opname:<18} {time_ns:7.1f} ns "
f"stacksize={caller.__code__.co_stacksize}")Calling an empty function with n named params, all passed as keywords:
| n | opcode | time |
|----|------------------|----------|
| 12 | CALL_KW | 80.2 ns |
| 13 | CALL_KW | 85.5 ns |
| 14 | CALL_KW | 90.7 ns |
| 15 | CALL_KW | 98.3 ns |
| 16 | CALL_FUNCTION_EX | 440.1 ns | <- one more keyword, 4.5x slower
| 17 | CALL_FUNCTION_EX | 458.3 ns |
| 18 | CALL_FUNCTION_EX | 509.7 ns |
| 19 | CALL_FUNCTION_EX | 530.8 ns |
| 20 | CALL_FUNCTION_EX | 549.9 ns |
Possible directions
-
Raise the threshold for this check. sets with >30 elements not constant-folded in
in/not inandforloops #148817 has a similar discussion
for a different code path, with 64 proposed as a round number -
Drop the threshold entirely for the call case. In the same thread,
one maintainer argued cutoffs aren't worth it here, runtime
performance matters more than bytecode size -
Improve the accuracy of the formula, so it tracks the
CALL_KW
path's real stack use - close tonelts + nkwelts + (nkwelts != 0),
notnelts + nkwelts*2.Benchmarked this version,
_PY_STACK_USE_GUIDELINEleft at 30:// Python/codegen.c:4430, before: if (nelts + nkwelts*2 > _PY_STACK_USE_GUIDELINE) { // after: if (nelts + nkwelts + (nkwelts != 0) > _PY_STACK_USE_GUIDELINE) {
n=16 CALL_KW 101.6 ns (was 440.1 ns) ... n=29 CALL_KW 219.2 ns n=30 CALL_FUNCTION_EX 895.6 ns (cliff still there, but moved to 30)
Related
#148817 (open) - a different change (gh-126835) stopped constant-folding
set/list literals over 30 items in in/not in/for, ~20x slower
3.13 -> 3.14. Different code path, same constant
Versions affected
Present in every release from 3.10.0 through 3.14.0, and current main
CPython versions tested on:
3.10, 3.12, CPython main branch
Operating systems tested on:
macOS
Activity
picnixz commentedon Aug 3, 2026
I would say: how likely do you have more than 15 keyword arguments? we could consider removing the threshold but we could also increase it to support more common cases.
cc @serhiy-storchaka @markshannon @Fidget-Spinner
G000D1ESS commentedon Aug 3, 2026
I guess in the world of dataclass/attrs/pydantic/ORM, 15 keywords might not be enough - models mapping to API payload or db row commonly have that many fields
That's actually how we ran into this in the first place - the workaround ended up being dropping keyword arguments in favor of positional ones, but once you're past 15 keys that doesn't really feel like a good solution either 😅
picnixz commentedon Aug 4, 2026
The dataclass argument is reasonable. Let's wait for more core devs opinion here.
ericvsmith commentedon Aug 5, 2026
I agree a higher limit is warranted. I routinely have 20 or so keyword args for dataclasses. I’m not sure what a good limit would be, but maybe 15 => 31, for starters?
hugovk commentedon Aug 5, 2026
cc @markshannon
Are two slots no longer needed for kwargs? Perhaps switch to a calculation like in here?
cpython/Python/codegen.c
Line 4137 in ea85e8f
cuishuang commentedon Aug 14, 2026
I tested the calculation @hugovk suggested:
nelts + nkwelts + (nkwelts != 0)It makes keyword-only calls with 16 through 29 arguments use CALL_KW, while larger calls still fall back to CALL_FUNCTION_EX.
I have a small patch ready with regression tests and a NEWS entry. I don't see an assignee or linked PR, so I plan to open it as a draft
to make this option concrete. If someone is already working on this, please let me know and I'm happy to coordinate or close.
pythongh-155141: Address review comments
Merge branch 'main' into pythongh-155141-call-kw-stack-use
Merge branch 'main' into pythongh-155141-call-kw-stack-use
gh-155141: Improve stack-use estimation for keyword calls (#155785)
pythongh-155141: Improve stack-use estimation for keyword calls (pyth…