Repository navigation
Poor thread scaling when constructing instances or accessing attributes #139103
Description
Bug report
Remaining scaling bugs
- dataclass
- namedtuple
- enum
Bug description:
When constructing dataclass or NamedTuple instances on multiple threads (on a free threading build), or accessing enum class attributes, performance doesn't scale when using multiple threads.
Regular class example (scales well):
# b_regular_class.py
from threading import Thread
from time import time
import sys
class Foo:
def __init__(self, x):
self.x = x
niter = 5 * 1000 * 1000
def benchmark(n):
for i in range(n):
Foo(x=1)
for nth in (1, 4):
t0 = time()
threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"{nth=} {(time() - t0) / nth}")Dataclass example (doesn't scale well):
# b_dataclass.py
from threading import Thread
from dataclasses import dataclass
from time import time
import sys
@dataclass
class Foo:
x: int
niter = 5 * 1000 * 1000
def benchmark(n):
for i in range(n):
Foo(x=1)
for nth in (1, 4):
t0 = time()
threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"{nth=} {(time() - t0) / nth}")Named tuple example (doesn't scale well):
# b_namedtuple.py
from threading import Thread
from typing import NamedTuple
from time import time
import sys
class Foo(NamedTuple):
x: int
niter = 5 * 1000 * 1000
def benchmark(n):
for i in range(n):
Foo(x=1)
for nth in (1, 4):
t0 = time()
threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"{nth=} {(time() - t0) / nth}")Enum example (doesn't scale well):
# b_enum.py
from threading import Thread
from time import time
from enum import Enum
import sys
class Foo(Enum):
X = 1
Y = 2
niter = 5 * 1000 * 1000
def benchmark(n):
for i in range(n):
Foo.X
Foo.Y.value
for nth in (1, 4):
t0 = time()
threads = [Thread(target=benchmark, args=(niter,)) for _ in range(nth)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"{nth=} {(time() - t0) / nth}")Results on recent main branch (running on an EC2 instance):
(cpython-dev) jukka@jukka-coder-dbx free-threading-benchmarks $ py b_regular_class.py
nth=1 1.1085155010223389
nth=4 0.2796591520309448
(cpython-dev) jukka@jukka-coder-dbx free-threading-benchmarks $ py b_dataclass.py
nth=1 1.1910037994384766
nth=4 1.0931583642959595
(cpython-dev) jukka@jukka-coder-dbx free-threading-benchmarks $ py b_namedtuple.py
nth=1 1.5688557624816895
nth=4 2.0257126092910767
(cpython-dev) jukka@jukka-coder-dbx free-threading-benchmarks $ py b_enum.py
nth=1 0.9439797401428223
nth=4 2.272495985031128
The expected behavior is that when using 4 threads (nth=4), the elapsed time per benchmark iteration (the second printed value) goes down significantly compared to when using a single thread (nth=1), which happens with the first benchmark (b_regular_class.py) but not the others.
cc @colesbury (we discussed this at CPython Core Dev Sprint in person)
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs
- gh-139103: fix free-threading
dataclass.__init__perf issue #141596 - Draft: gh-139103: Use _PyObject_GetMethodStackRef for slot_tp_new #141603
- [3.14] gh-139103: fix free-threading
dataclass.__init__perf issue (gh-141596) #141750 - gh-139103: Improve namedtuple scaling in free-threaded build #144332
- gh-139103: Use METH_FASTCALL for tp_new_wrapper #144406
- gh-139103: Use borrowed references for positional args in _PyStack_UnpackDict #144407
- gh-139103: Improve enum free-threaded scaling #144977
- gh-139103: Defer reference count mutable class attributes #155876
Activity
[-]Poor thread scaling when constructing instances or accessings attributes[/-][+]Poor thread scaling when constructing instances or accessing attributes[/+]colesbury commentedon Sep 22, 2025
I've started looking into this. The following is mostly for my own future reference, but may be useful if anyone else wants to take a look at this as well.
Here are the above examples added to
Tools/ftscalingbench/ftscalingbench.py: https://gist.github.com/colesbury/65234f8e8d9afad361981255d8259d7fDataclasses: there's reference count contention on the
__init__function. I think the function is created by callingexec()on a string, so the__init__function doesn't have deferred reference counting enabled. I think we can fix this by enabling deferred reference counting when the function is set on the type object intype_setattro.typing.NamedTuple: Calling
slot_tp_newdoesn't scale well.PyObject_GetAttr()increments the refcount and thePy_DECREFdecrements it. We want something like_PyObject_GetMethodStackRef()here that uses stackrefs so that we avoid reference count contention.cpython/Objects/typeobject.c
Lines 10842 to 10856 in f0d8583
Enum: I think there's reference count contention on instances of
enum.property:cpython/Lib/enum.py
Line 173 in f0d8583
LindaSummer commentedon Oct 29, 2025
Hi @colesbury and @JukkaL ,
I'm interested in this topic. Could I have a try? 😊
Best Regards,
Edward
LindaSummer commentedon Nov 6, 2025
Hi @colesbury ,
Sorry to bother you for this issue. 😊
I have read the dataclass code and find that it generated below code and runs with
exec.Here is the reference code snippts.
cpython/Lib/dataclasses.py
Lines 492 to 524 in 2e5e6fd
So I create a case without dataclass below.
But got below result.
It seems that this case follow the dataclass logic to exec string and generate an
__init__.But the performance didn't change.
I use below configure to build cpython on main branch 1697cb5 .
./configure --with-pydebug --disable-gil "CC=clang"Could you help correct me on the understanding of dataclass's case? 😊
Wish you a good day!
Best Regards,
Edward
colesbury commentedon Nov 6, 2025
Don't use
--with-pydebugwhen benchmarking:uv run -p 3.14t python example.py:LindaSummer commentedon Nov 6, 2025
Thanks very much for your kind help and patience!
I will try to solve this issue. 😊
eendebakpt commentedon Nov 15, 2025
@colesbury In #141603 I used the
_PyObject_GetMethodStackRefapproach, but the scaling performance is unchanged. Not sure yet why that is.eendebakpt commentedon Nov 16, 2025
A small update: for
the call
B(1)is specialized to_CALL_NON_PY_GENERAL. Maybe that does not scale.Even with the modified
slot_tp_newthe scaling of getting an attribute is quite poor:gh-139103: fix free-threading `dataclass.__init__` perf issue (gh-141596
[3.14] pythongh-139103: fix free-threading `dataclass.__init__` perf …
[3.14] gh-139103: fix free-threading `dataclass.__init__` perf issue (g…
7 remaining items
pythongh-139103: Improve namedtuple scaling in free-threaded build
pythongh-139103: Use METH_FASTCALL for tp_new_wrapper
pythongh-139103: Use borrowed references for positional args in _PySt…
gh-139103: use `METH_FASTCALL` for tp_new_wrapper (#144406)
pythongh-139103: use `METH_FASTCALL` for tp_new_wrapper (python#144406)
gh-139103: Use borrowed references for positional args in _PyStack_Un…
Merge branch 'main' into pythongh-139103-named-tuple
Merge branch 'main' into pythongh-139103-named-tuple
gh-139103: Improve namedtuple scaling in free-threaded build (gh-144332)
pythongh-139103: use `METH_FASTCALL` for tp_new_wrapper (python#144406)
pythongh-139103: Use borrowed references for positional args in _PySt…
pythongh-139103: Improve namedtuple scaling in free-threaded build (p…
pythongh-139103: Improve enum free-threaded scaling
pythongh-139103: Improve namedtuple scaling in free-threaded build (p…
johng commentedon Aug 16, 2026
Also see this on various class/instance attributes in addition to the above PR, shall I be putting these in a new issue?
./python_main.exe Tools/ftscalingbench/ftscalingbench.py shared_instance_attribute
Running benchmarks with 18 threads
shared_instance_attribute 25.0x slower