From 2d3e018af8b80dd3f59822e5c7323a1c1291cfde Mon Sep 17 00:00:00 2001 From: Dino Viehland Date: Mon, 6 May 2024 14:54:24 -0700 Subject: [PATCH 1/2] use thread state set of dict versions --- Include/cpython/pystate.h | 1 + Include/internal/pycore_dict.h | 23 +++++++++++++-- Lib/test/test_free_threading/test_dict.py | 36 +++++++++++++++++++++++ Modules/_testcapi/dict.c | 14 ++++++++- Python/pystate.c | 1 + 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h index 0611e299403031..2df9ecd6d52084 100644 --- a/Include/cpython/pystate.h +++ b/Include/cpython/pystate.h @@ -188,6 +188,7 @@ struct _ts { PyObject *previous_executor; + uint64_t dict_global_version; }; #ifdef Py_DEBUG diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index cb7d4c3219a9af..827c90a5df31e3 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -221,8 +221,27 @@ 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 + THREAD_LOCAL_DICT_VERSION_BATCH; + return next; + } + return tstate->dict_global_version += DICT_VERSION_INCREMENT; +} + +#define DICT_NEXT_VERSION(INTERP) dict_next_version(INTERP) #else #define DICT_NEXT_VERSION(INTERP) \ diff --git a/Lib/test/test_free_threading/test_dict.py b/Lib/test/test_free_threading/test_dict.py index 6a909dd3ee025f..f877582e6b565c 100644 --- a/Lib/test/test_free_threading/test_dict.py +++ b/Lib/test/test_free_threading/test_dict.py @@ -8,6 +8,8 @@ from threading import Thread from unittest import TestCase +from _testcapi import dict_version + from test.support import threading_helper @@ -137,5 +139,39 @@ def writer_func(l): 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() diff --git a/Modules/_testcapi/dict.c b/Modules/_testcapi/dict.c index 4319906dc4fee0..e80d898118daa5 100644 --- a/Modules/_testcapi/dict.c +++ b/Modules/_testcapi/dict.c @@ -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}, }; diff --git a/Python/pystate.c b/Python/pystate.c index f442d87ba3150e..b49318a92a7a72 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1487,6 +1487,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; From adb5e13d633076dc6db9d52b68e3307e20dc61d6 Mon Sep 17 00:00:00 2001 From: Dino Viehland Date: Mon, 6 May 2024 16:28:12 -0700 Subject: [PATCH 2/2] Fix increment --- Include/internal/pycore_dict.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index 827c90a5df31e3..8d8d3748edaea8 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -231,12 +231,10 @@ 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 + THREAD_LOCAL_DICT_VERSION_BATCH; - return next; + tstate->dict_global_version = next; } return tstate->dict_global_version += DICT_VERSION_INCREMENT; }