GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — gh-127716: make memoryview thread-safe in free-threaded build by nascheme · Pull Request #156248 · python/cpython Conversation
|
| 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 |
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).
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Documentation build overview
427 files changed ·
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Co-authored-by: ayaangazali <[email protected]> Co-authored-by: Lu Xiaowei <[email protected]>
4269e2d to
8fa2daf
Compare
* 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().
…yview-thread-safe
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
* Add comment to CAS while loop, yielding is unnecessary. * Use "relaxed" memory ordering, seq-cst is overkill.
…yview-thread-safe
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.
There was an error while loading. Please reload this page.
|
@nascheme What do you think about making the iterator hold exports for its whole lifetime, instead of pinning in |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Increasing performance would be nice but that would change the behaviour of |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
I mean
cpython/Objects/memoryobject.c Lines 1253 to 1263 in e3c9012
What I'm thinking (quick Sol 5.6-assisted proof of concept patch to demonstrate the idea): diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 2d8537ee57..b66f3b9153 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3946,6 +3946,11 @@ memoryiter_dealloc(PyObject *self) { memoryiterobject *it = (memoryiterobject *)self; _PyObject_GC_UNTRACK(it); +#ifdef Py_GIL_DISABLED + if (it->it_seq != NULL) { + mbuf_drop_export(it->it_seq->mbuf); + } +#endif Py_XDECREF(it->it_seq); PyObject_GC_Del(it); } @@ -3969,33 +3974,28 @@ memoryiter_next(PyObject *self) if (it->it_index >= it->it_length) { /* Preserve StopIteration for exhausted iterators over relea */ +#ifndef Py_GIL_DISABLED it->it_seq = NULL; Py_DECREF(seq); +#endif return NULL; } -#ifdef Py_GIL_DISABLED - /* Keep seq alive if another thread exhausts the iterator. */ - Py_INCREF(seq); -#endif + CHECK_RELEASED(seq); - PyObject *result = NULL; - if (memoryview_pin(seq) == 0) { - Py_buffer *view = &seq->view; - char *ptr = (char *)view->buf; + Py_buffer *view = &seq->view; + char *ptr = (char *)view->buf; - ptr += view->strides[0] * it->it_index++; - ptr = ADJUST_PTR(ptr, view->suboffsets, 0); - if (ptr != NULL) { - result = unpack_single(seq, ptr, it->it_fmt); - } - memoryview_unpin(seq); + ptr += view->strides[0] * it->it_index++; + ptr = ADJUST_PTR(ptr, view->suboffsets, 0); + if (ptr == NULL) { + return NULL; } - -#ifdef Py_GIL_DISABLED - Py_DECREF(seq); -#endif - return result; + return unpack_single(seq, ptr, it->it_fmt); } static PyObject * @@ -4032,6 +4032,11 @@ memory_iter_pinned(PyObject *seq) it->it_length = memory_length((PyObject *)obj); it->it_index = 0; it->it_seq = (PyMemoryViewObject*)Py_NewRef(obj); +#ifdef Py_GIL_DISABLED + FT_ATOMIC_ADD_SSIZE(obj->mbuf->exports, 1); +#endif _PyObject_GC_TRACK(it); return (PyObject *)it; } The downside is that one won't be able to resize a ba = bytearray(128); m = memoryview(ba); it = iter(m) m.release() ba.append(1) # would raise |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
I see. That still changes the release semantics as your example shows. My guess is that iterating over the memoryview is likely not more common than using getitem or slicing it. So, I feel like this optimization is not worth the (small) code complexity it adds and the change in semantics. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Hi! Granted I'm not familiar with python's policies for cherrypicking fixes for release, I was wondering whether you expected this fix in a specific future python 3.{14,15,16}.x release? Many thanks! |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
…yview-thread-safe
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I'm looking at alternate approaches that have better scaling for reads on shared memoryviews but Claude is still chewing on it. For now I found some minor issues with the current version of the PR, see below.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
I think there is a version of this PR that uses hazard pointers and scales better under shared reads. However, it would be more complicated and I think it's possible to move to that approach in the future, in a manner than composes nicely with this. So I think we shouldn't delay this on a version that scales better: it can always be done in the future if reports come in about multithreaded performance. Or perhaps later in the 3.16 release cycle. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Incorporate review suggestions. Add a regression test using a tp_clear helper, recommend per-thread views for read-heavy workloads, and clarify the scope of the thread-safety NEWS entry. Co-authored-by: Nathan Goldbaum <[email protected]>
…yview-thread-safe
…yview-thread-safe
There was an error while loading. Please reload this page.
|
🤖 New build scheduled with the buildbot fleet by @kumaraditya303 for commit 8833436 🤖 Results will be shown at: https://buildbot.python.org/all/#/grid?branch=refs%2Fpull%2F156248%2Fmerge If you want to schedule another build, you need to add the 🔨 test-with-buildbots label again. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.