Calling a function: what it costs, what does not happen, and where cells live
Three common claims about the cost of a call — a method costs more than a function, binding a method in advance is cheaper, staticmethod is faster — the measurement confirms none of them and contradicts two. The reason is visible in the bytecode: on the fast path no bound-method object is created at all. That same fact opens the second half of the article — closures: the variable a nested function sees is stored through a separate object — a cell the frame only points at — and everything else follows from that.
Full technical treatment
TL;DR
An empty call is about fifteen nanoseconds, roughly twice the cheapest attribute read — a comparison of scale, not a breakdown of what a call costs. Everything else in the table is a surcharge for the form of the call.
Three common claims are not supported by measurement. o.m() is only 1.16
times f0(); a method bound in advance is no cheaper — and that it is more
expensive is not proven, the difference lies inside the spread; staticmethod,
with the way of reading held fixed, is one and a half times slower than an
ordinary method, and classmethod nearly three times slower.
One reason covers all three: no bound-method object is created on the fast
path. The interpreter sees a call right after the attribute read and puts the
function and the instance on the stack separately. There is nothing to save, and
the "optimisation" bound = o.m brings the object back.
Of the star forms, one is expensive. f(**kwargs) costs five times direct
passing on an empty function; f(*args) costs sixteen per cent. On a function
with a body, six per cent is all that is left of the fivefold gap.
A closure holds a cell, not a value. Hence the shared variable between two
functions, one cell for a whole loop, and [2, 2, 2]. Reading from a cell is
indistinguishable in time from reading a local variable.
What a call costs
Almost every number was taken on a call to an empty function: the body does
nothing, so only what happens around it is measured. Two rows are there for scale:
p.x — reading a slot, with no call in it at all — and len_(data), a C function.
| Form | ns |
|---|---|
p.x — reading a slot, no call | 8.16 |
f0() — a function with no arguments | 15.35 |
o.m() — a method through the dot | 17.74 |
bound() — a method bound in advance | 18.34 |
C.s() — a staticmethod | 27.88 |
C.c() — a classmethod | 46.89 |
len_(data) — a C function | 9.48 |
Numbers from bench/calls/cost.py, one run of 3.13.7; they may be compared only
with one another.
Why a method barely costs more than a function
By the data model o.m creates a bound-method object, and the call ought to
cost more. On the fast path that object does not exist at all: the interpreter
sees that a call follows the attribute read immediately and puts the function
and the instance on the stack separately. No bound method is built at all.
That the object really is created when asked for without a call takes one line to see:
o.m is o.m # False
o.m == o.m # TrueHence the advice best not followed: bound = o.m brings the object back and the
call starts going through it — 18.34 against 17.74 nanoseconds: no cheaper, and
that it is more expensive the measurement does not prove, the difference being
smaller than the round-to-round spread.
Star-args
One form out of five is expensive: f3(**kwargs) costs five times direct
passing. f3(*args) costs only sixteen per cent more.
The reason is that positional arguments and the values of keyword arguments travel in an array while the names of the keyword arguments travel in a separate tuple. Positional arguments already lie the way they are needed and are handed over as they are; a dict lies neither way and has to be taken apart into an array of values and a tuple of names. The cost is not in how many values are passed but in having to turn one structure into two others.
Where a closure's variable lives
A closure holds not a value but a cell — a separate object, visible from Python
as an element of __closure__. Everything else follows from that.
Two functions created side by side refer to the same cell, and
increment.__closure__[0] is read.__closure__[0] gives True. Neither passed
the other anything: they simply look into one box — one cell object, shared.
Three lambdas from a loop share one cell too — because a cell is made per
VARIABLE, and the loop has one variable. By the time they are called it holds
the last value, hence [2, 2, 2]. This is not a special rule about loops, nor a
bug.
Writing lambda i=i: i does not "fix the binding" but removes the need for a
shared box: those functions have an empty __closure__, and the value was
computed when each was created.
There is no reason to fear closures for their speed: reading from a cell costs 3.91 nanoseconds against 3.96 for a local variable and 3.78 for a global — all three indistinguishable, and all three a fraction of an empty call.
What to do with this
Do not rewrite o.m() as bound = o.m for speed. Do not pick staticmethod
and classmethod by speed — only by meaning. Of the stars, beware **kwargs on
a hot path. And keep the cell in mind: it is ONE for everyone who sees the
variable, and that is what deserves attention in closures.
Three claims about the cost of a call travel together, and one measurement
checks all of them: "calling a method costs more than calling a function",
"binding a method in advance is cheaper than going through the dot every time",
"staticmethod is faster than an ordinary method — it has no self".
The measurement barely supports the first. It contradicts the second and the third, and the second in the opposite direction.
All three have the same explanation, and it is not about nanoseconds: on the fast path of a method call, the bound-method object IS NOT CREATED. There is nothing to save on its creation, and whoever "optimises" the call by binding the method in advance brings the object back.
The second half of the article is about closures, and it is not here by
coincidence. A cell is where the variable a nested function sees actually lives,
and how it works explains the rest: the variable two functions share, the three
identical answers from a loop, and why nonlocal exists.
Part I. The cost of a call
What is being measured
Every number was taken on a call to an EMPTY function: the body does nothing, so
only what happens around the body is measured. Beside it stands a line with no
call at all — reading an attribute on an object with __slots__, the cheapest
read there is — to give the nanoseconds a scale. Measured on 3.13.7 with
bench/calls/cost.py:
| Form | ns |
|---|---|
p.x — reading a slot, no call | 8.16 |
f0() — a function with no arguments | 15.35 |
f1(1) — one positional | 16.65 |
f3(1, 2, 3) — three positional | 20.22 |
f1(a=1) — the same, by name | 25.39 |
fdef(1) — two defaulted arguments | 23.61 |
fdef(1, 2, 3) — the same ones passed | 20.24 |
fkwonly(a=1) — keyword-only | 25.64 |
lam() — a lambda | 15.36 |
clo() — a closure with one cell | 20.88 |
o.m() — a method through the dot | 17.74 |
bound() — a method bound in advance | 18.34 |
C.s() — a staticmethod | 27.88 |
C.c() — a classmethod | 46.89 |
o() — an object with __call__ | 36.36 |
len_(data) — a C function | 9.48 |
Read it this way: an empty call on this machine is about fifteen nanoseconds — roughly twice the cheapest attribute read available here. That is a comparison of scale between two different operations, not a decomposition of the cost of a call: what those fifteen nanoseconds are made of is not something this measurement says at all. Everything else in the table is a surcharge for the form.
Every number comes from one run of 3.13.7 and is compared only with the others in it. The absolute values are a property of the measuring machine (Intel Xeon 2.80 GHz, 2 vCPU); what carries to other hardware is the ratio, not the nanosecond. Time is never compared across versions in this project.
The three claims and what became of them
From the same run:
| Claim | The pair | Ratio |
|---|---|---|
| a method call costs more than a function call | f0() 15.35 ns, o.m() 17.74 ns | ×1.16 |
| binding in advance is cheaper than the dot | o.m() 17.74 ns, bound() 18.34 ns | ×1.03 |
staticmethod is faster: it has no self | o.m() 17.74 ns, C.s() 27.88 ns | ×1.57 |
The first barely holds: sixteen per cent is not a reason to rewrite anything,
and it is less than the gap between f1(1) and f1(a=1) in the same table:
16.65 against 25.39.
The second does not hold either: a method bound in advance is at any rate no cheaper. That it is MORE EXPENSIVE is not something these two numbers prove, and why is taken up below, in a section of its own about spread.
The third is contradicted in the other direction: staticmethod is slower than
an ordinary method and classmethod noticeably slower than both. But that
comparison changes two things at once, so below it is separated by axis.
First — what is being compared with what
The third row puts o.m() next to C.s(), and TWO things differ between those
two spellings: the kind of method and what it is read through — the instance or
the class. While both axes are mixed, the difference cannot be attributed to the
kind of method: it could have come from reading through the class.
So both axes are separated by a run of their own, in which all six forms were
taken together — bench/calls/forms.py, block 1:
| Form | ns | Against f0() |
|---|---|---|
f0() — a plain function | 15.45 | ×1.00 |
o.m() — a method through the instance | 17.33 | ×1.12 |
o.s() — staticmethod through the instance | 26.60 | ×1.72 |
C.s() — staticmethod through the class | 28.45 | ×1.84 |
o.c() — classmethod through the instance | 47.15 | ×3.05 |
C.c() — classmethod through the class | 51.26 | ×3.32 |
Now each comparison changes exactly one thing. With the reading held fixed —
through the instance — staticmethod costs ×1.54 of an ordinary method and
classmethod ×2.72. The way of reading, with the kind of method held fixed, adds
×1.07 and ×1.09 — a few per cent.
Separating the axes did not overturn the conclusion: staticmethod really is
more expensive than an ordinary method, and it is the kind of method that does
it, not the reading through the class. But that can only be said now.
The numbers in this block come from a different run than the table above: they
were taken by bench/calls/forms.py, not bench/calls/cost.py. They must not be compared
with each other, so 17.33 here and 17.74 there are not a discrepancy but two
separate measurements of the same thing on the same machine. The ratios inside
each run held.
Why: what exactly does not happen
The answer to all three at once is in the bytecode. bench/calls/shapes.py
disassembles these forms after two hundred warm-up calls each:
| What is called | Instructions after specialisation |
|---|---|
plain(t) | LOAD_GLOBAL_MODULE + CALL_PY_EXACT_ARGS |
o.m(t) | LOAD_ATTR_METHOD_WITH_VALUES + CALL_PY_EXACT_ARGS |
bound(t) | CALL_BOUND_METHOD_EXACT_ARGS |
C.s(t) | LOAD_GLOBAL_MODULE + LOAD_ATTR + CALL_PY_EXACT_ARGS |
C.c(t) | LOAD_GLOBAL_MODULE + LOAD_ATTR + CALL_BOUND_METHOD_EXACT_ARGS |
len(data) | LOAD_GLOBAL_BUILTIN + CALL_LEN |
o.m with no call | LOAD_ATTR |
Look at the o.m(t) row and the last one. The attribute-reading instruction is
DIFFERENT: LOAD_ATTR_METHOD_WITH_VALUES when a call follows immediately, and
a plain LOAD_ATTR when it does not.
The difference is in what goes on the stack. On the path with a call, the function and the instance go there separately: there is nothing to bind them into, and no bound-method object is created. On the path without a call it is created, because it was asked for.
That it really is created when asked for needs no bytecode to check:
o.m is o.m # False
o.m == o.m # TrueTwo consecutive reads give two DIFFERENT objects. That work is exactly what the fast path does not do.
The third claim is visible in the same table. C.s(t) takes three instructions
rather than two: the class name is read first, then the attribute on it, and
there is no specialisation on the instance at all. That is the "way of reading"
axis, to which the matrix above assigns ×1.07; the rest of the ×1.57 is the kind of
method, ×1.54 with the reading held fixed through the instance. This run does not
disassemble o.s(t), so the split comes from time rather than from
instructions. C.c(t) has the same extra attribute read and calls
through a bound method on top of it: CALL_BOUND_METHOD_EXACT_ARGS. What each
of the two additions costs on its own, this measurement does not separate — only
the total is visible, 46.89 against 27.88.
Hence the second claim as well. Binding a method in advance means creating a
PyMethodObject once and calling through it:
CALL_BOUND_METHOD_EXACT_ARGS. There is nothing to save — the fast path never
created that object — while a level of indirection remains.
The numbers, though, say less than one would like. 18.34 against 17.74 is three
per cent, and before leaning on a difference that size one has to know the spread.
It is printed by bench/calls/forms.py, block 2. The numbers there are its own —
a different run, not comparable with the ones above; what has to be compared is
the gap inside that run against its own spread:
| Form | Best | Worst | Spread |
|---|---|---|---|
f0() | 15.36 | 15.72 | 0.37 |
o.m() | 17.27 | 19.51 | 2.24 |
bound() | 18.55 | 19.38 | 0.82 |
o.s() | 27.12 | 27.54 | 0.42 |
C.c() | 49.33 | 51.32 | 1.99 |
The gap between bound() and o.m() is 1.28 ns against a spread of up to
2.24 ns within the rounds — that is LESS than the spread. So "a method bound in
advance is slower" does NOT follow from these measurements: the two numbers lie
inside their own noise. What does follow is that it is no cheaper, and that is
exactly the conclusion practice needs: binding a method in advance for speed is
not worth it, because there is no gain in any run. Binding in advance is for when
the method has to be PASSED somewhere.
The data model, meanwhile, describes o.m precisely as creating an object:
When a non-data attribute of an instance is referenced, the instance's class is searched. If the name denotes a valid class attribute that is a function object, references to both the instance object and the function object are packed into a method object
. That remains true: the
observable behaviour has not changed, and o.m without a call does create the
object. What changed is how the interpreter reaches the same result when it
sees a call.
Star-args: one form out of five is expensive
"Stars are expensive" is one more durable habit, and it is right in exactly one
case out of five (bench/calls/cost.py):
| Form | ns | Against direct passing |
|---|---|---|
f3(1, 2, 3) — direct | 20.17 | ×1.00 |
f3(*args) — unpacking a tuple | 23.49 | ×1.16 |
f3(**kwargs) — unpacking a dict | 104.35 | ×5.17 |
fstar(1, 2, 3) — received into *args | 42.27 | ×2.10 |
fstar(a=1) — received into **kwargs | 46.12 | ×2.29 |
fstar(*args, **one) — both | 125.77 | ×6.24 |
Unpacking a tuple is nearly free — sixteen per cent. Receiving into stars costs double, which is a fair price for an arbitrary signature. But unpacking a DICT when passing costs five times as much, and that is the one place worth thinking about.
The last row, at ×6.24, is not an exception but the same cause: it contains a dict unpack too, and that alone accounts for nearly the whole gap. The tuple beside it adds little.
The reason is in how the fast calling protocol is built. Its description says in what shape the callee receives the arguments:
args is a C array consisting of the positional arguments followed by the
values of the keyword arguments
And the names of those arguments travel separately:
kwnames is a tuple containing the names of the keyword arguments; in other
words, the keys of the kwargs dict
Positional arguments already lie in an array: they can be handed over as they are. A dict lies neither in an array of values nor in a tuple of names, and it has to be taken apart into both. Hence the fivefold gap — the cost is not in how many values are passed but in having to turn one structure into two others. The run does not disassemble this form: the explanation comes from the description of the protocol, and how those hundred nanoseconds break down step by step the measurement does not show.
And straight away about the bounds of that ratio. All the numbers were taken on an
EMPTY function, which makes it a ratio of overhead to overhead. The same thing on a
function with a body was taken by a separate run — bench/calls/forms.py,
block 3:
| Form | ns | Against direct passing |
|---|---|---|
fwork(1, 2, 3) — direct passing | 1663.47 | ×1.00 |
fwork(*args) — tuple unpack | 1659.83 | ×1.00 |
fwork(**kwargs) — dict unpack | 1763.83 | ×1.06 |
fworkstar(*args, **one) — both | 1755.32 | ×1.06 |
Same mechanism, different ratio: ×1.06 against ×5.17 on the empty function. Those two numbers come from different runs, and what is compared here is not them but the ratios inside each. What carries over from this measurement is not the coefficient but WHICH form of passing is the expensive one — and the fact that avoiding it makes sense where the body of the function is comparable to the overhead of the call.
Part II. Cells
A closure holds a box, not a value
"A function remembers its environment" is an explanation from which none of the observable behaviour follows. All of it follows from one fact: the variable a nested function sees is stored THROUGH A SEPARATE OBJECT — a cell. From here on it is called a box, because that is the part of it that matters: the value sits in the box, not in the function itself.
Saying "it does not live in the frame" would be inaccurate, and the inaccuracy
matters. The reference to the cell is in the frame: a frame's local variables hold
references to cells, and the LOAD_DEREF and STORE_DEREF instructions work
through them. The separate object is not instead of the frame but between the
frame and the value — which is exactly why several closures can hold the same
cell, and why it outlives the frame it appeared in.
A cell is not a CPython implementation detail but part of the data model, and it is described directly:
Cell objects are used to implement variables referenced by multiple scopes.
For each such variable, a cell object is created to store the value; the
local variables of each stack frame that references the value contain a
reference to the cells from outer scopes which also use that variable
"For each such VARIABLE" is the whole thing. A cell is made per variable, not per function and not per call.
Two functions, one box
From bench/calls/cells.py — identical on 3.12.3, 3.13.7 and 3.14.7:
| Check | Result |
|---|---|
increment.__closure__[0] is read.__closure__[0] | True |
read() before calling increment | 0 |
read() after two increment() calls | 2 |
| the value inside the cell | 2 |
Neither function passed the other anything. They simply refer to the same
object, and is confirms it. That is what a "shared variable" actually is — not
scoping magic but a shared object.
Why a loop yields three identical functions
The classic surprise comes from the same place:
[lambda: i for i in range(3)] # [2, 2, 2]
[lambda i=i: i for i in range(3)] # [0, 1, 2]The run counts the cells, and that answers the question better than any explanation: the first three functions share ONE cell, the second three have none at all.
The first line is not a Python bug, nor a separate rule called "late binding". A cell is made per variable, the loop has one variable, and by the time the lambdas are called it holds the last value. The second line does not "fix the binding" — it removes the need for a shared box: the value goes into a default argument, and that is computed when each function is CREATED.
Who decides that a variable becomes a cell
The compiler decides, once, and the decision is visible in the code
(bench/calls/cells.py):
| What is inspected | Value |
|---|---|
outer.__code__.co_varnames | ('param', 'local', 'inner') |
outer.__code__.co_cellvars | ('box',) |
inner.co_freevars | ('box',) |
box is assigned in outer, yet it is not in co_varnames: the compiler saw
that a nested function takes it and moved it into a cell. The rest of the chain
is visible in the instructions the same run prints:
| Where | Instruction |
|---|---|
in outer | MAKE_CELL box |
in outer | STORE_DEREF box |
in inner | COPY_FREE_VARS |
in inner | LOAD_DEREF box |
MAKE_CELL creates the box on entry to outer, STORE_DEREF puts the value
in, COPY_FREE_VARS hands the same box to inner, LOAD_DEREF takes the value
out. At no step is the value copied.
The output of bench/calls/cells.py on 3.12.3, 3.13.7 and 3.14.7 agrees in
everything but the addresses in the cell's repr. That is what should be
expected: the observable BEHAVIOUR of closures is part of the data model. One
cell for everyone who sees the variable; a value that outlives the frame;
nonlocal, which turns an assignment into a write to the cell — that is the
contract.
HOW that is arranged, on the other hand, is CPython's implementation, and code
must not depend on it. PyCellObject, the MAKE_CELL, STORE_DEREF,
COPY_FREE_VARS and LOAD_DEREF instructions, the split of names across
co_varnames, co_cellvars and co_freevars, the names of call
specialisations — all of that is how one implementation is built. It is
observable from Python and therefore convenient for explaining; but it has
changed between releases and will change again, and what is promised out of it
is only what the previous note lists.
Why a box is needed at all, and where nonlocal comes in
A function's frame lives until it returns; the box lives longer. That is what
bench/calls/cells.py, block 5, shows:
| What the run reports | Value |
|---|---|
short_lived has returned, its frame is gone | — |
keeper() returns | a value from a function that has already returned |
keeper.__closure__[0].cell_contents is alive | True |
That is where nonlocal comes from. An ordinary assignment inside a function is
a write into the frame, and the compiler decides where to write by one rule: a
name assigned anywhere in the body is local. For a nested function that is not
enough — it has to write not into its own frame but into the box shared with the
enclosing function. nonlocal does not "permit changing an outer variable"; it
tells the compiler the name is not local, so the assignment must become
STORE_DEREF.
What reading from a cell costs
One question remains: are closures slow? Each name below is read INSIDE
a function, so the call is in the number; the second column has the empty call
subtracted (bench/calls/cost.py):
| What is read | Call + read, ns | The read, ns |
|---|---|---|
a local name — LOAD_FAST | 19.31 | 3.96 |
a name from a cell — LOAD_DEREF | 19.25 | 3.91 |
a global name — LOAD_GLOBAL | 19.13 | 3.78 |
for comparison: f0() — an empty call | 15.35 | 0.00 |
The three reading instructions came out at 3.96, 3.91 and 3.78 nanoseconds — less than five per cent apart, and each about a quarter of an empty call. What is paid for is not reading from a cell but making a call at all.
Part III. Practice
Practice · predict the output
def make():
out = []
for i in range(3):
out.append(lambda: i)
return out
def make_bound():
out = []
for i in range(3):
out.append(lambda i=i: i)
return out
class C:
def m(self):
return 1
o = C()
print([f() for f in make()])
print([f() for f in make_bound()])
print(o.m is o.m, o.m == o.m)Practice · estimate
Part IV. What follows
Four conclusions the measurements support
Do not rewrite o.m() as bound = o.m. No bound-method object is created
on the fast path, so there is nothing to save, and such an edit brings it back —
and it is no cheaper either: the 18.34 against 17.74 nanoseconds lies inside the
round-to-round spread. The one case where binding in advance is right is when the method has to
be PASSED somewhere, and then it is not about saving anything.
Do not pick staticmethod for speed. With the way of reading held fixed it
is one and a half times slower than an ordinary method, and classmethod nearly
three times slower. Pick them by meaning: does the method need the instance or
the class.
Only one star form is worth avoiding. f(**kwargs) on a hot path with an
empty body costs five times what direct passing costs; f(*args) costs sixteen
per cent and needs no thought. The ratio is tied to how much the function itself
does: on a body of fifty rounds of arithmetic, six per cent is all that is left
of the fivefold gap. What to look at is not the coefficient but whether the body
of the function is comparable to the cost of the call.
Do not fear closures over their cost. Reading from a cell is indistinguishable in time from reading a local or a global name, and all three are a fraction of a call. What is worth fearing in closures is something else: that the cell is ONE for everyone who sees the variable.
Version history
| Version | Change | What this means for your code |
|---|---|---|
| 3.12 | The call specialisations are the same as later, but some carry different names: the len specialisation is called CALL_NO_KW_LEN. Cells, __closure__ and co_cellvars behave exactly as in 3.13 and 3.14 — the output of bench/calls/cells.py agrees in everything but the addresses in the cell's repr. | |
| 3.13 | The len specialisation is renamed to CALL_LEN. That is the rule illustrating itself: none of these names is in dis.opmap or in the dis module's documentation, and code must not rely on them. | |
| 3.14 | The instruction set on the disassembled call paths is the same as in 3.13. Cell behaviour has not changed once across the three versions. |
How this was measured
The numbers in this article come from these scripts. Each opens from here, together with the record of its run.
Time — two records, both on one build:
bench/calls/cost.py— fifteen call forms and the three claimsbench/calls/forms.py— the matrix of method forms, the round-to-round spread, and star-args on a non-empty function
Bytecode and objects — comparable across versions, recorded on all three:
The basis for the exercises in "Practice":
Python 3.12.3 (GCC 13.3.0), 3.13.7 (Clang 20.1.4), 3.14.7 (Clang 22.1.3); Intel Xeon 2.80 GHz, 2 vCPU.
The three kinds of claim in this article have different bounds, and they are worth separating:
- Behaviour — the same on all three versions:
o.mwithout a call creates a method object,o.m is o.mgivesFalse, one cell goes per variable. Checked on 3.12.3, 3.13.7 and 3.14.7. - Specialisation names — a property of the version: on 3.12.3 the
lenspecialisation is calledCALL_NO_KW_LEN, on 3.13.7 and 3.14.7CALL_LEN. Those are properties of code and may be compared across versions. - Time — 3.13.7 with the GIL enabled only, and only within each of the two runs separately. It is never compared across versions: the builds have different compilers and different flags. Free-threaded builds were not measured here.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
In fact
- The object is not created. The interpreter sees that a call follows the attribute read immediately, and puts the function and the instance on the stack separately — the instruction
LOAD_ATTR_METHOD_WITH_VALUESinstead of a plainLOAD_ATTR. Hence the number:o.m()costs 17.74 ns against 15.35 for a function call, a factor of 1.16. The object really is created when asked for without a call, and one line checks it:o.m is o.mgivesFalse. - It is no cheaper — and that it is more expensive is not something the measurement proves. In one run it is 18.34 against 17.74 ns; in another, which also prints the spread, the gap is 1.28 ns against a spread of up to 2.24 ns for the same
o.m(). There is no gain in either. There is nothing to save — the fast path never created the bound-method object — while such an edit brings it back, and the call now goes through it:CALL_BOUND_METHOD_EXACT_ARGSinstead of a read-and-call pair. Binding in advance is right when the method has to be PASSED somewhere; for speed, no. - It is slower — and that only shows once the way of reading is held fixed. In a separate run where both axes were taken,
o.s()costs ×1.54 ofo.m()read through the instance, while the way of reading adds only ×1.07. The reason is in the bytecode: the staticmethod path carries an extraLOAD_ATTR, whileo.m()gets by with a specialisation on the instance.classmethodis more expensive still, ×2.72 of an ordinary method in that same run. Choose between them by meaning, not by speed. - One form out of five is. Unpacking a tuple,
f3(*args), costs ×1.16 against direct passing; receiving into*argsand**kwargscosts ×2.10 and ×2.29. Butf3(**kwargs)costs ×5.17 on an empty function — on a body of fifty rounds of arithmetic the same form costs only ×1.06, so the fivefold gap belongs to the empty function — and the reason is the shape in which the callee receives its arguments: args is a C array consisting of the positional arguments followed by the values of the keyword arguments, with the names travelling in a separate tuple. Positional arguments already lie in an array; a dict does not — it has to be taken apart into an array of values and a tuple of names. - It holds a CELL — a separate object with the value inside. Everything else follows: two functions created side by side refer to the same cell (
increment.__closure__[0] is read.__closure__[0]givesTrue), and a change through one is visible through the other. A cell is part of the data model rather than a CPython detail: Cell objects are used to implement variables referenced by multiple scopes. - There is no separate rule. A cell is made per VARIABLE, the loop has one variable, and all three functions refer to it: the run counts distinct cells and finds one. By the time they are called it holds the last value — hence
[2, 2, 2]. Writinglambda i=i: idoes not "fix the binding" but removes the need for a shared cell: those functions have an empty__closure__, and the value was computed when each was created. - The three reading instructions —
LOAD_FAST,LOAD_DEREFandLOAD_GLOBAL— cost 3.96, 3.91 and 3.78 nanoseconds, indistinguishable within this machine's noise, and all three are a fraction of an empty call (15.35 ns). What is paid for is not reading from a cell but making a call at all. - None of them is in
dis.opmap— the run checks that by enumeration; nor are they described in thedismodule's documentation. They are visible only throughdis.dis(..., adaptive=True)and change between releases: thelenspecialisation isCALL_NO_KW_LENon 3.12.3 andCALL_LENon 3.13.7 and 3.14.7. Code must not rely on these names; looking at them to understand what happens is both allowed and worthwhile.
By version
- 3.12
- The call specialisations are the same as later, but some carry different names: the
lenspecialisation is calledCALL_NO_KW_LEN. Cells,__closure__andco_cellvarsbehave exactly as in 3.13 and 3.14 — the output ofbench/calls/cells.pyagrees in everything but the addresses in the cell'srepr.< - 3.13
- The
lenspecialisation is renamed toCALL_LEN. That is the rule illustrating itself: none of these names is indis.opmapor in thedismodule's documentation, and code must not rely on them.< - 3.14
- The instruction set on the disassembled call paths is the same as in 3.13. Cell behaviour has not changed once across the three versions.<
What is covered
- Part I. The cost of a call
- What is being measured
- The three claims and what became of them
- First — what is being compared with what
- Why: what exactly does not happen
- Star-args: one form out of five is expensive
- Part II. Cells
- A closure holds a box, not a value
- Two functions, one box
- Why a loop yields three identical functions
- Who decides that a variable becomes a cell
- Why a box is needed at all, and where `nonlocal` comes in
- What reading from a cell costs
- Part III. Practice
- Part IV. What follows
- Four conclusions the measurements support
- Version history
- How this was measured
Common misconceptions
A method call is noticeably more expensive than a function call: a bound-method object is created first
The object is not created. The interpreter sees that a call follows the attribute read immediately, and puts the function and the instance on the stack separately — the instruction LOAD_ATTR_METHOD_WITH_VALUES instead of a plain LOAD_ATTR. Hence the number: o.m() costs 17.74 ns against 15.35 for a function call, a factor of 1.16. The object really is created when asked for without a call, and one line checks it: o.m is o.m gives False.
Binding a method in advance (bound = o.m) is cheaper than going through the dot
It is no cheaper — and that it is more expensive is not something the measurement proves. In one run it is 18.34 against 17.74 ns; in another, which also prints the spread, the gap is 1.28 ns against a spread of up to 2.24 ns for the same o.m(). There is no gain in either. There is nothing to save — the fast path never created the bound-method object — while such an edit brings it back, and the call now goes through it: CALL_BOUND_METHOD_EXACT_ARGS instead of a read-and-call pair. Binding in advance is right when the method has to be PASSED somewhere; for speed, no.
staticmethod is faster than an ordinary method: it has no self
It is slower — and that only shows once the way of reading is held fixed. In a separate run where both axes were taken, o.s() costs ×1.54 of o.m() read through the instance, while the way of reading adds only ×1.07. The reason is in the bytecode: the staticmethod path carries an extra LOAD_ATTR, while o.m() gets by with a specialisation on the instance. classmethod is more expensive still, ×2.72 of an ordinary method in that same run. Choose between them by meaning, not by speed.
Stars in a call are expensive
One form out of five is. Unpacking a tuple, f3(*args), costs ×1.16 against direct passing; receiving into *args and **kwargs costs ×2.10 and ×2.29. But f3(**kwargs) costs ×5.17 on an empty function — on a body of fifty rounds of arithmetic the same form costs only ×1.06, so the fivefold gap belongs to the empty function — and the reason is the shape in which the callee receives its arguments: args is a C array consisting of the positional arguments followed by the values of the keyword arguments
, with the names travelling in a separate tuple. Positional arguments already lie in an array; a dict does not — it has to be taken apart into an array of values and a tuple of names.
A closure remembers the value of a variable
It holds a CELL — a separate object with the value inside. Everything else follows: two functions created side by side refer to the same cell (increment.__closure__[0] is read.__closure__[0] gives True), and a change through one is visible through the other. A cell is part of the data model rather than a CPython detail: Cell objects are used to implement variables referenced by multiple scopes
.
A loop makes three functions with the same answer because of “late binding” — a separate rule of Python
There is no separate rule. A cell is made per VARIABLE, the loop has one variable, and all three functions refer to it: the run counts distinct cells and finds one. By the time they are called it holds the last value — hence [2, 2, 2]. Writing lambda i=i: i does not "fix the binding" but removes the need for a shared cell: those functions have an empty __closure__, and the value was computed when each was created.
Closures are slower than ordinary functions: reading from a cell costs more
The three reading instructions — LOAD_FAST, LOAD_DEREF and LOAD_GLOBAL — cost 3.96, 3.91 and 3.78 nanoseconds, indistinguishable within this machine's noise, and all three are a fraction of an empty call (15.35 ns). What is paid for is not reading from a cell but making a call at all.
Specialisation names like CALL_PY_EXACT_ARGS are part of the language and can be found in the documentation
None of them is in dis.opmap — the run checks that by enumeration; nor are they described in the dis module's documentation. They are visible only through dis.dis(..., adaptive=True) and change between releases: the len specialisation is CALL_NO_KW_LEN on 3.12.3 and CALL_LEN on 3.13.7 and 3.14.7. Code must not rely on these names; looking at them to understand what happens is both allowed and worthwhile.
Check yourself
Why does o.m() barely cost more than calling an ordinary function?
Sources & further reading
6 SOURCES
- PEP 590 — Vectorcall: a fast calling protocol for CPythonPEP. The document that introduced the protocol underlying every number in this article. The goal is in the abstract: “This PEP introduces a new C API to optimize calls of objects. It introduces a new "vectorcall" protocol and calling convention”. Where the old cost came from is in the Motivation: “The poor performance is largely a result of having to create intermediate tuples, and possibly intermediate dicts, during the call”. Final, implemented in 3.8. How the arguments actually travel under this protocol is described not here but in the C API reference — see the next source.https://peps.python.org/pep-0590/
- Call protocol: the shape in which the callee receives its argumentsOfficial documentation. Where the explanation for the dict unpack being more expensive than the tuple unpack comes from: “args is a C array consisting of the positional arguments followed by the values of the keyword arguments” and “kwnames is a tuple containing the names of the keyword arguments; in other words, the keys of the kwargs dict”. Positional arguments already lie in the required shape; a dict does not — it has to be taken apart into an array of values and a tuple of names.https://docs.python.org/3/c-api/call.html
- Objects/classobject.c — what a bound method isCPython source code. The `PyMethodObject` struct of three fields — `im_func`, `im_self` and weak references — and `method_vectorcall`, the function through which an already-created bound method is called. This is precisely the object that is NOT created on the `o.m()` path, and precisely the one created when a method is bound in advance. Read at tag v3.13.7.https://github.com/python/cpython/blob/v3.13.7/Objects/classobject.c
- dis — the instruction listOfficial documentation. Cited as a source of ABSENCE: neither `CALL_PY_EXACT_ARGS` nor `LOAD_ATTR_METHOD_WITH_VALUES` nor `CALL_BOUND_METHOD_EXACT_ARGS` is described in this document, and none of them is in `dis.opmap` — which the run prints by enumeration. They are visible only through `dis.dis(..., adaptive=True)` and change between releases: on 3.12.3 the `len` specialisation is called `CALL_NO_KW_LEN`, on 3.13.7 and 3.14.7 `CALL_LEN`.https://docs.python.org/3/library/dis.html
- Data model: function objects and method bindingOfficial documentation. The normative description of what happens on `o.m`: “When a non-data attribute of an instance is referenced, the instance's class is searched. If the name denotes a valid class attribute that is a function object, references to both the instance object and the function object are packed into a method object”. That is exactly what is NOT carried out literally on the fast call path — while the observable behaviour is unchanged, and `o.m` without a call still creates the object.https://docs.python.org/3/tutorial/classes.html#method-objects
- Data model: cell objectsOfficial documentation. A cell is described as an ordinary object of the language: “Cell objects are used to implement variables referenced by multiple scopes. For each such variable, a cell object is created to store the value; the local variables of each stack frame that references the value contain a reference to the cells from outer scopes which also use that variable”. Hence what the figure shows: one cell for all the functions that see the variable, outliving the frame.https://docs.python.org/3/c-api/cell.html