Skip to content

Define and call functions - #97

Merged
tamnd merged 1 commit into
mainfrom
functions
Aug 29, 2026
Merged

Define and call functions#97
tamnd merged 1 commit into
mainfrom
functions

Conversation

@tamnd

@tamnd tamnd commented Aug 29, 2026

Copy link
Copy Markdown
Owner

def, lambda, calling one, return, decorators, recursion and global. Every parameter shape Python has works and they compose: positional, positional-only before a /, keyword-only after a *, defaults on either kind, *args and **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 a def inside 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 total and the module body writing it are the same name in the same namespace.

All nine of Python's call-binding TypeError messages are here word for word, in CPython's order (keyword failures before counting failures), along with the UnboundLocalError that names the variable and the RecursionError at 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.

`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
tamnd merged commit eb633d3 into main Aug 29, 2026
9 checks passed
@tamnd
tamnd deleted the functions branch August 29, 2026 11:13
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant