Bug report
On CPython 3.14.7 free-threaded, d.copy() becomes about 52 times slower than dict(d) after enough keys are deleted. This reproduces with one thread and no third-party modules.
Tested on macOS 26.7, arm64, using uv's Python build with the GIL disabled.
import statistics
import sys
import timeit
compact = dict.fromkeys(range(4), 13)
sparse = dict.fromkeys(range(10), 13)
for key in range(4, 10):
del sparse[key]
assert compact == sparse
print(sys.version)
print("GIL enabled:", sys._is_gil_enabled())
for label, d, expression in [
("compact.copy()", compact, "d.copy()"),
("sparse.copy()", sparse, "d.copy()"),
("dict(sparse)", sparse, "dict(d)"),
]:
samples = timeit.repeat(
expression, globals={"d": d}, number=20000, repeat=5
)
print(label, statistics.median(samples) / 20000 * 1e6, "us/copy")
Measured median times:
| Operation |
Microseconds per copy |
compact.copy() |
0.029 |
sparse.copy() |
3.67 |
dict(sparse) |
0.070 |
I expect copying a dict to avoid waiting on a lock the same thread already holds.
PyDict_Copy locks the source dict. When enough entries have been deleted, it calls dict_merge to make a compact copy. That function tries to lock both dicts, including the already-locked source. The thread then calls sched_yield() 40 times before parking releases the outer lock and allows progress. Lock code.
Related to #150902. That fix handles reacquiring the same pair of locks. This case holds one lock and then requests two.
CPython versions tested on:
3.14
Operating systems tested on:
macOS
Linked PRs
Bug report
On CPython 3.14.7 free-threaded,
d.copy()becomes about 52 times slower thandict(d)after enough keys are deleted. This reproduces with one thread and no third-party modules.Tested on macOS 26.7, arm64, using uv's Python build with the GIL disabled.
Measured median times:
compact.copy()sparse.copy()dict(sparse)I expect copying a dict to avoid waiting on a lock the same thread already holds.
PyDict_Copylocks the source dict. When enough entries have been deleted, it callsdict_mergeto make a compact copy. That function tries to lock both dicts, including the already-locked source. The thread then callssched_yield()40 times before parking releases the outer lock and allows progress. Lock code.Related to #150902. That fix handles reacquiring the same pair of locks. This case holds one lock and then requests two.
CPython versions tested on:
3.14
Operating systems tested on:
macOS
Linked PRs