Conversation
`def`, `lambda`, calls, `return`, decorators, recursion and `global`. Every parameter shape composes: positional, positional-only, keyword-only, defaults on either kind, `*args` and `**kwargs`. A parameter is a register the call fills in rather than a dictionary entry, so binding an ordinary call is filling a vector from zero. Defaults live on the function object rather than on its code, which is why `def f(x=[])` shares one list between calls. Names are one table per module rather than one per body, because a global has to be the same slot wherever it is read from. All nine of Python's call-binding `TypeError` messages are here word for word, in CPython's order, checked against a running 3.14. The recursion limit is 1000 with the module body counting against it, and the interpreter runs on a thread with 256 MB of stack so the limit is reachable on Windows. A call allocates once rather than four times: constant pools are built up front per body instead of per call, the callee's name is borrowed rather than copied, and arguments go straight from the caller's frame into the callee's registers. 0.28s to 0.11s on the call benchmark.
tamnd
added a commit
to tamnd/kohebi-bench
that referenced
this pull request
Aug 29, 2026
A call is a frame, an argument list bound to parameters, a body and a return value handed back. Methods, comprehensions, generators and every dunder in the object model are that sequence with something wrapped around it, so whatever a call costs is a floor under all of them. Three shapes, because they cost differently and a regression in one should not hide behind the other two: two positional arguments straight into two registers, a keyword argument that has to be matched against the parameter list, and arguments collected into a tuple for a `*args`. It found something immediately. kohebi was at 0.28s against CPython's 0.08s for 700,000 calls while beating it on the same loop with the call inlined, which turned out to be four heap allocations per call. Three of them are gone in tamnd/kohebi#97 and it is at 0.11s now.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
def,lambda, calling one,return, decorators, recursion andglobal. Every parameter shape Python has works and they compose: positional, positional-only before a/, keyword-only after a*, defaults on either kind,*argsand**kwargs.A parameter is a register the call fills in rather than an entry in a dictionary, so binding an ordinary call is filling a vector from zero and only a keyword argument costs a search. Defaults belong to the function object rather than to its code, which is why
def f(x=[])shares one list between calls and why adefinside a loop builds a new function every turn out of the same code.Names are one table for the whole module rather than one per body. A global has to be the same slot wherever it is read from, and a function reading
totaland the module body writing it are the same name in the same namespace.All nine of Python's call-binding
TypeErrormessages are here word for word, in CPython's order (keyword failures before counting failures), along with theUnboundLocalErrorthat names the variable and theRecursionErrorat the end of a runaway. Every one was checked against a running 3.14 before it was written. The recursion limit is 1000 with the module body counting against it, so 998 nested calls work and 999 raises, and the interpreter runs on a thread with 256 MB of stack so the limit is reachable on a platform whose default is one megabyte.A call allocates once rather than four times. The constant pool was being rebuilt on every call because it was built at the top of the interpreter loop, and is built up front per body now. The callee's name was copied onto the heap for an error message that almost never happens, and is borrowed. The arguments were collected into a vector and then moved out of it, and go straight from the caller's frame into the callee's registers now. On the new call benchmark that is 0.28s down to 0.11s for 700,000 calls, against CPython's 0.08s.
Part of #7.