]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-112075: use per-thread dict version pool (#118676)
authorDino Viehland <dinoviehland@meta.com>
Tue, 7 May 2024 00:22:26 +0000 (17:22 -0700)
committerGitHub <noreply@github.com>
Tue, 7 May 2024 00:22:26 +0000 (00:22 +0000)
use thread state set of dict versions

Include/cpython/pystate.h
Include/internal/pycore_dict.h
Lib/test/test_free_threading/test_dict.py
Modules/_testcapi/dict.c
Python/pystate.c

index 0611e299403031ea18ce16fc8995b4b20e58b8a2..2df9ecd6d520840130dcfd32a9fdacbe93b10e7c 100644 (file)
@@ -188,6 +188,7 @@ struct _ts {
 
     PyObject *previous_executor;
 
+    uint64_t dict_global_version;
 };
 
 #ifdef Py_DEBUG
index cb7d4c3219a9afa519f8a83eeaa639be9e9c9cb4..8d8d3748edaea834f2f224cb58376801c35c7923 100644 (file)
@@ -221,8 +221,25 @@ static inline PyDictUnicodeEntry* DK_UNICODE_ENTRIES(PyDictKeysObject *dk) {
 #define DICT_WATCHER_AND_MODIFICATION_MASK ((1 << (DICT_MAX_WATCHERS + DICT_WATCHED_MUTATION_BITS)) - 1)
 
 #ifdef Py_GIL_DISABLED
-#define DICT_NEXT_VERSION(INTERP) \
-    (_Py_atomic_add_uint64(&(INTERP)->dict_state.global_version, DICT_VERSION_INCREMENT) + DICT_VERSION_INCREMENT)
+
+#define THREAD_LOCAL_DICT_VERSION_COUNT 256
+#define THREAD_LOCAL_DICT_VERSION_BATCH THREAD_LOCAL_DICT_VERSION_COUNT * DICT_VERSION_INCREMENT
+
+static inline uint64_t
+dict_next_version(PyInterpreterState *interp)
+{
+    PyThreadState *tstate = PyThreadState_GET();
+    uint64_t cur_progress = (tstate->dict_global_version &
+                            (THREAD_LOCAL_DICT_VERSION_BATCH - 1));
+    if (cur_progress == 0) {
+        uint64_t next = _Py_atomic_add_uint64(&interp->dict_state.global_version,
+                                              THREAD_LOCAL_DICT_VERSION_BATCH);
+        tstate->dict_global_version = next;
+    }
+    return tstate->dict_global_version += DICT_VERSION_INCREMENT;
+}
+
+#define DICT_NEXT_VERSION(INTERP) dict_next_version(INTERP)
 
 #else
 #define DICT_NEXT_VERSION(INTERP) \
index 6a909dd3ee025f801fd69ef9ded5e001263509b1..f877582e6b565c987f2ba5cab3be9d4ae9ee60fc 100644 (file)
@@ -8,6 +8,8 @@ from functools import partial
 from threading import Thread
 from unittest import TestCase
 
+from _testcapi import dict_version
+
 from test.support import threading_helper
 
 
@@ -137,5 +139,39 @@ class TestDict(TestCase):
             for ref in thread_list:
                 self.assertIsNone(ref())
 
+    def test_dict_version(self):
+        THREAD_COUNT = 10
+        DICT_COUNT = 10000
+        lists = []
+        writers = []
+
+        def writer_func(thread_list):
+            for i in range(DICT_COUNT):
+                thread_list.append(dict_version({}))
+
+        for x in range(THREAD_COUNT):
+            thread_list = []
+            lists.append(thread_list)
+            writer = Thread(target=partial(writer_func, thread_list))
+            writers.append(writer)
+
+        for writer in writers:
+            writer.start()
+
+        for writer in writers:
+            writer.join()
+
+        total_len = 0
+        values = set()
+        for thread_list in lists:
+            for v in thread_list:
+                if v in values:
+                    print('dup', v, (v/4096)%256)
+                values.add(v)
+            total_len += len(thread_list)
+        versions = set(dict_version for thread_list in lists for dict_version in thread_list)
+        self.assertEqual(len(versions), THREAD_COUNT*DICT_COUNT)
+
+
 if __name__ == "__main__":
     unittest.main()
index 4319906dc4fee0a0e01dbce48d30e04e57d16179..e80d898118daa512395b847434e9ff483e319ceb 100644 (file)
@@ -1,7 +1,6 @@
 #include "parts.h"
 #include "util.h"
 
-
 static PyObject *
 dict_containsstring(PyObject *self, PyObject *args)
 {
@@ -182,6 +181,18 @@ dict_popstring_null(PyObject *self, PyObject *args)
     RETURN_INT(PyDict_PopString(dict, key,  NULL));
 }
 
+static PyObject *
+dict_version(PyObject *self, PyObject *dict)
+{
+    if (!PyDict_Check(dict)) {
+        PyErr_SetString(PyExc_TypeError, "expected dict");
+        return NULL;
+    }
+_Py_COMP_DIAG_PUSH
+_Py_COMP_DIAG_IGNORE_DEPR_DECLS
+    return PyLong_FromUnsignedLongLong(((PyDictObject *)dict)->ma_version_tag);
+_Py_COMP_DIAG_POP
+}
 
 static PyMethodDef test_methods[] = {
     {"dict_containsstring", dict_containsstring, METH_VARARGS},
@@ -193,6 +204,7 @@ static PyMethodDef test_methods[] = {
     {"dict_pop_null", dict_pop_null, METH_VARARGS},
     {"dict_popstring", dict_popstring, METH_VARARGS},
     {"dict_popstring_null", dict_popstring_null, METH_VARARGS},
+    {"dict_version", dict_version, METH_O},
     {NULL},
 };
 
index 7c75263b7e526a76726401f922717f32ff227809..2f1521edd8bf0ab219bf4537c0be70a73a6dfcd9 100644 (file)
@@ -1488,6 +1488,7 @@ init_threadstate(_PyThreadStateImpl *_tstate,
     tstate->datastack_limit = NULL;
     tstate->what_event = -1;
     tstate->previous_executor = NULL;
+    tstate->dict_global_version = 0;
 
     tstate->delete_later = NULL;