It might be specific to certain hardware. I'm also not seeing it on my Mac, but I can reliably reproduce the biggest gap on the Xeon Gold 6230 (Cascade Lake / 2019).
I don't think it's the typical contention on reference count fields. It might be contention on some other data structure.
Using Claude to measure and bisect. Here's a summary so far
Perf on various release tags:
┌───────────┬───────────┬────────────────────────────┐
│ Tag │ Time (ms) │ Notes │
├───────────┼───────────┼────────────────────────────┤
│ v3.14.0 │ ~840 │ Baseline │
├───────────┼───────────┼────────────────────────────┤
│ v3.14.2 │ ~850 │ Same as baseline │
├───────────┼───────────┼────────────────────────────┤
│ v3.15.0a1 │ ~1210 │ Already regressed │
├───────────┼───────────┼────────────────────────────┤
│ v3.15.0a3 │ ~970 │ Better than a1 (some fix?) │
├───────────┼───────────┼────────────────────────────┤
│ v3.15.0a4 │ ~1530 │ Big regression │
├───────────┼───────────┼────────────────────────────┤
│ v3.15.0a5 │ ~1890 │ Worse still │
└───────────┴───────────┴────────────────────────────┘
Bisecting between v3.15.0a3 and v3.15.0a4 gives 9e51301. Particularly, it seems like it's related to the added static strings. If I apply just the static strings to the previous commit, it introduces the regression.
RFO is "request for ownership". Basically, when a CPU needs to modify some memory (execute a store instruction), it must have exclusive ownership of the cache line. I'm not sure these are necessarily due to actually executed store instruction. I think it might be either issued speculatively due to mispredicting stores or a prefetcher.
Ok, it's false sharing between tlbc_index in _PyThreadStateImpl and the _qsbr_thread_state from a different thread. If you're unlucky, the tail end of _PyThreadStateImpl ends up in the same cache line as the beginning of the _qsbr_thread_state array.
We have padding at the end of each struct _qsbr_pad to avoid false sharing with other entries within the array, but that doesn't prevent false sharing between the first entry in the array and other data.
I think we want to do two things:
Align the beginning of the array to 64 bytes. Since we use the PyMem_Raw allocator, I think we just over allocate by 64 bytes and then align.
Pad the end of _PyThreadStateImpl so that we avoid potential false sharing with other data structures.
Activity
kumaraditya303 commented on Feb 6, 2026
Timings on my machine:
I am not seeing any regression on mac, is this linux specific?
colesbury commented on Feb 6, 2026
It might be specific to certain hardware. I'm also not seeing it on my Mac, but I can reliably reproduce the biggest gap on the Xeon Gold 6230 (Cascade Lake / 2019).
I don't think it's the typical contention on reference count fields. It might be contention on some other data structure.
colesbury commented on Feb 6, 2026
Using Claude to measure and bisect. Here's a summary so far
Perf on various release tags:
Bisecting between v3.15.0a3 and v3.15.0a4 gives 9e51301. Particularly, it seems like it's related to the added static strings. If I apply just the static strings to the previous commit, it introduces the regression.
Per stat shows:
And some other events:
RFO is "request for ownership". Basically, when a CPU needs to modify some memory (execute a store instruction), it must have exclusive ownership of the cache line. I'm not sure these are necessarily due to actually executed store instruction. I think it might be either issued speculatively due to mispredicting stores or a prefetcher.
colesbury commented on Feb 6, 2026
Ok, it's false sharing between tlbc_index in _PyThreadStateImpl and the _qsbr_thread_state from a different thread. If you're unlucky, the tail end of _PyThreadStateImpl ends up in the same cache line as the beginning of the _qsbr_thread_state array.
cpython/Include/internal/pycore_tstate.h
Lines 86 to 87 in 895e837
cpython/Include/internal/pycore_qsbr.h
Lines 72 to 76 in 895e837
We have padding at the end of each struct _qsbr_pad to avoid false sharing with other entries within the array, but that doesn't prevent false sharing between the first entry in the array and other data.
I think we want to do two things:
Align the beginning of the array to 64 bytes. Since we use the PyMem_Raw allocator, I think we just over allocate by 64 bytes and then align.
Pad the end of _PyThreadStateImpl so that we avoid potential false sharing with other data structures.
Helpful commands:
pythongh-144438: Fix false sharing between QSBR and tlbc_index
pythongh-144438: Fix false sharing between QSBR and tlbc_index
pythongh-144438: Fix false sharing between QSBR and tlbc_index
gh-144438: Fix false sharing between QSBR and tlbc_index (gh-144554)
[3.14] pythongh-144438: Fix false sharing between QSBR and tlbc_index (…
pythongh-144438: Fix false sharing between QSBR and tlbc_index (pytho…
[3.14] gh-144438: Fix false sharing between QSBR and tlbc_index (gh-1…
pythongh-144438: Fix false sharing between QSBR and tlbc_index (pytho…
pythongh-159860: Pad QSBR thread states to 128 bytes on AArch64