Skip to content

gh-127716: make memoryview thread-safe in free-threaded build - #156248

Open
nascheme wants to merge 7 commits into
python:mainfrom
nascheme:gh-127716-memoryview-thread-safe
Open

gh-127716: make memoryview thread-safe in free-threaded build#156248
nascheme wants to merge 7 commits into
python:mainfrom
nascheme:gh-127716-memoryview-thread-safe

Conversation

@nascheme

@nascheme nascheme commented Aug 23, 2026

Copy link
Copy Markdown
Member

Note: this PR was developed with assistance from LLMs, Claude Opus/Fable and GPT 5.6 Sol.

This incorporates changes from gh-155882 and gh-154770. It goes further and ensures the underlying buffer remains "pinned" if a thread releases the memory view while other threads have in-flight operations on it.

Performance testing results are below. The goal is negligible slowdown for the GIL-enabled build (achieved). Also, we want negligible slowdown in the un-contended free-threaded case (also achieved). Unfortunately, multi-threaded scaling with a shared memoryview object is terrible.

I explored various ways of improving the scaling (QSBR, RCU, hazard pointers) but I think other options are too complex in terms of implementation or have other critical problems. If you want multiple threads operating on the same underlying "mbuf", a good way to do it is to create a thread-local memoryview copy. You need to do this from the orginal "exporter" object, not the existing memoryview object. I have an additional change that adds a .reexport() method that does this, which is probably handly.

Single-thread performance

Times are nanoseconds per operation (lower is better), minus loop overhead; iteration is
nanoseconds per element. "base" is before this PR and "patched" is after.

Results from Intel i7-14700K

Benchmark base FT patched FT ratio base GIL patched GIL ratio
getitem 14.65 20.54 1.40x 16.00 15.45 0.97x
setitem 19.33 22.33 1.16x 19.17 19.52 1.02x
format 17.42 20.19 1.16x 17.88 17.76 0.99x
slice 33.36 53.38 1.60x 34.10 33.59 0.99x
cast 33.31 53.39 1.60x 35.02 34.01 0.97x
tobytes_16 30.87 28.99 0.94x 29.91 27.04 0.90x
memoryview_of_view 42.64 59.11 1.39x 41.88 41.42 0.99x
struct.unpack_from 48.83 55.51 1.14x 51.15 50.20 0.98x
BytesIO.readinto 59.57 76.09 1.28x 40.55 39.95 0.99x
BytesIO.write 63.15 76.51 1.21x 22.64 21.85 0.97x

Results from Macbook M3 Pro.

Benchmark base FT patched FT ratio
getitem 8.36 9.35 1.12x
setitem 10.71 11.00 1.03x
format 10.11 11.62 1.15x
slice 23.11 25.37 1.10x
cast 23.88 24.21 1.01x
tobytes_16 18.71 21.10 1.13x
memoryview_of_view 30.60 30.84 1.01x
unpack_from 27.16 30.20 1.11x
bytesio_write 22.12 24.90 1.13x
bytesio_readinto 22.19 24.37 1.10x

Free-threaded scaling

Throughput is millions of operations per second (higher is better). For iteration, one yielded
element is one operation. Base has no reexport(), so that workload has a
patched row only; compare it against the separate views of one exporter rows.

Results from Intel i7-14700K

Workload Build 1 thread 2 threads 4 threads 8 threads 8-thread patched/base
shared view, mv[0] base 40.4 79.5 157.5 308.1
patched 32.9 11.5 10.1 10.7 0.03x
slices of one shared view, mv[0] base 40.1 79.8 157.4 313.3
patched 32.9 16.7 13.7 10.1 0.03x
separate views of one exporter, mv[0] base 40.3 79.9 157.0 313.4
patched 33.0 65.9 130.1 259.7 0.83x
per-thread reexport() views, mv[0] patched 33.0 66.1 130.6 253.4 n/a
private views and exporters, mv[0] base 40.5 80.2 158.1 313.5
patched 32.9 66.0 130.2 250.0 0.80x
shared view, iterate 16 bytes base 82.4 55.7 61.7 60.1
patched 30.0 5.2 5.4 4.9 0.08x
shared view, tobytes() 4 KiB base 10.6 21.7 42.7 79.6
patched 10.4 9.2 12.5 9.4 0.12x
shared view, struct.unpack_from base 14.9 3.9 4.6 5.0
patched 12.5 3.2 3.2 2.1 0.42x

Results from Macbook M3 Pro. Note that this CPU has 6 P-cores and 6 E-cores.

Workload Build 1 thread 2 threads 4 threads 8 threads 4-thread patched/base
shared view, mv[0] base 73.1 144.8 282.3 321.9
patched 68.7 28.7 22.6 10.0 0.080x
slices of one shared view, mv[0] base 73.2 144.0 279.9 320.6
patched 68.4 29.0 23.0 10.0 0.082x
separate views of one exporter, mv[0] base 73.7 143.8 278.4 298.3
patched 68.2 133.8 260.8 295.1 0.94x
per-thread reexport() views, mv[0] patched 68.4 133.8 261.5 276.4 n/a
private views and exporters, mv[0] base 70.1 139.2 275.5 314.8
patched 68.4 134.2 261.6 287.4 0.95x
shared view, iterate 16 bytes base 146.3 177.0 99.1 34.1
patched 106.7 16.4 13.5 4.3 0.14x
shared view, tobytes() 4 KiB base 11.4 22.4 42.0 51.5
patched 11.0 12.8 15.8 9.7 0.38x
shared view, struct.unpack_from base 28.4 9.7 9.2 3.8
patched 27.2 8.0 7.3 5.6 0.79x

Anything that shares one managed buffer contends (shared mbuf->exports cache line).

@read-the-docs-community

read-the-docs-community Bot commented Aug 23, 2026

Copy link
Copy Markdown

@weixlu

weixlu commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Hi, thanks for the thorough fix! I read the code and it looks like all operations of the counters are well handled, so I'll close my PR.

Also, I checkout your branch and run the reproducers from #127716 and #155606. Both issues appear to be resolved.

Co-authored-by: ayaangazali <ayaangazali.work@gmail.com>
Co-authored-by: Lu Xiaowei <weixlu420302@gmail.com>
@nascheme
nascheme force-pushed the gh-127716-memoryview-thread-safe branch from 4269e2d to 8fa2daf Compare August 25, 2026 16:24
* The CHECK_* macros can return.  Restructure code so that unpin is
  always done for toreadonly().

* Use atomic load for asserts on `exports`.
Speed it up (less iterations, rounds and threads).  Rename confusing
assert_exporter_free() method to ensure_exporter_free().
@nascheme
nascheme marked this pull request as ready for review August 27, 2026 20:22
Comment thread Objects/memoryobject.c Outdated
Comment thread Objects/memoryobject.c Outdated
* Add comment to CAS while loop, yielding is unnecessary.
* Use "relaxed" memory ordering, seq-cst is overkill.
We only need the "while" loop if we are setting multiple flag bits
concurrently.  That's not needed, we only set RELEASED.  So, restructure
code, no while loop needed and someone can't accidently call the
function in the wrong way.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants