Skip to content

Commit ba3d67c

Browse files
authored
bpo-39465: Fix _PyUnicode_FromId() for subinterpreters (GH-20058)
Make _PyUnicode_FromId() function compatible with subinterpreters. Each interpreter now has an array of identifier objects (interned strings decoded from UTF-8). * Add PyInterpreterState.unicode.identifiers: array of identifiers objects. * Add _PyRuntimeState.unicode_ids used to allocate unique indexes to _Py_Identifier. * Rewrite the _Py_Identifier structure. Microbenchmark on _PyUnicode_FromId(&PyId_a) with _Py_IDENTIFIER(a): [ref] 2.42 ns +- 0.00 ns -> [atomic] 3.39 ns +- 0.00 ns: 1.40x slower This change adds 1 ns per _PyUnicode_FromId() call in average.
1 parent f0853bc commit ba3d67c

File tree

6 files changed

+102
-37
lines changed

6 files changed

+102
-37
lines changed

Include/cpython/object.h

+4-3
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ PyAPI_FUNC(Py_ssize_t) _Py_GetRefTotal(void);
3535
_PyObject_{Get,Set,Has}AttrId are __getattr__ versions using _Py_Identifier*.
3636
*/
3737
typedef struct _Py_Identifier {
38-
struct _Py_Identifier *next;
3938
const char* string;
40-
PyObject *object;
39+
// Index in PyInterpreterState.unicode.ids.array. It is process-wide
40+
// unique and must be initialized to -1.
41+
Py_ssize_t index;
4142
} _Py_Identifier;
4243

43-
#define _Py_static_string_init(value) { .next = NULL, .string = value, .object = NULL }
44+
#define _Py_static_string_init(value) { .string = value, .index = -1 }
4445
#define _Py_static_string(varname, value) static _Py_Identifier varname = _Py_static_string_init(value)
4546
#define _Py_IDENTIFIER(varname) _Py_static_string(PyId_##varname, #varname)
4647

Include/internal/pycore_interp.h

+7
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,20 @@ struct _Py_bytes_state {
6464
PyBytesObject *characters[256];
6565
};
6666

67+
struct _Py_unicode_ids {
68+
Py_ssize_t size;
69+
PyObject **array;
70+
};
71+
6772
struct _Py_unicode_state {
6873
// The empty Unicode object is a singleton to improve performance.
6974
PyObject *empty_string;
7075
/* Single character Unicode strings in the Latin-1 range are being
7176
shared as well. */
7277
PyObject *latin1[256];
7378
struct _Py_unicode_fs_codec fs_codec;
79+
// Unicode identifiers (_Py_Identifier): see _PyUnicode_FromId()
80+
struct _Py_unicode_ids ids;
7481
};
7582

7683
struct _Py_float_state {

Include/internal/pycore_runtime.h

+7
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ typedef struct _Py_AuditHookEntry {
4949
void *userData;
5050
} _Py_AuditHookEntry;
5151

52+
struct _Py_unicode_runtime_ids {
53+
PyThread_type_lock lock;
54+
Py_ssize_t next_index;
55+
};
56+
5257
/* Full Python runtime state */
5358

5459
typedef struct pyruntimestate {
@@ -106,6 +111,8 @@ typedef struct pyruntimestate {
106111
void *open_code_userdata;
107112
_Py_AuditHookEntry *audit_hook_head;
108113

114+
struct _Py_unicode_runtime_ids unicode_ids;
115+
109116
// XXX Consolidate globals found via the check-c-globals script.
110117
} _PyRuntimeState;
111118

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Make :c:func:`_PyUnicode_FromId` function compatible with subinterpreters.
2+
Each interpreter now has an array of identifier objects (interned strings
3+
decoded from UTF-8). Patch by Victor Stinner.

Objects/unicodeobject.c

+62-23
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
4141
#define PY_SSIZE_T_CLEAN
4242
#include "Python.h"
4343
#include "pycore_abstract.h" // _PyIndex_Check()
44+
#include "pycore_atomic_funcs.h" // _Py_atomic_size_get()
4445
#include "pycore_bytes_methods.h" // _Py_bytes_lower()
4546
#include "pycore_format.h" // F_LJUST
4647
#include "pycore_initconfig.h" // _PyStatus_OK()
@@ -302,9 +303,6 @@ unicode_decode_utf8(const char *s, Py_ssize_t size,
302303
_Py_error_handler error_handler, const char *errors,
303304
Py_ssize_t *consumed);
304305

305-
/* List of static strings. */
306-
static _Py_Identifier *static_strings = NULL;
307-
308306
/* Fast detection of the most frequent whitespace characters */
309307
const unsigned char _Py_ascii_whitespace[] = {
310308
0, 0, 0, 0, 0, 0, 0, 0,
@@ -2312,42 +2310,85 @@ PyUnicode_FromString(const char *u)
23122310
return PyUnicode_DecodeUTF8Stateful(u, (Py_ssize_t)size, NULL, NULL);
23132311
}
23142312

2313+
23152314
PyObject *
23162315
_PyUnicode_FromId(_Py_Identifier *id)
23172316
{
2318-
if (id->object) {
2319-
return id->object;
2317+
PyInterpreterState *interp = _PyInterpreterState_GET();
2318+
struct _Py_unicode_ids *ids = &interp->unicode.ids;
2319+
2320+
int index = _Py_atomic_size_get(&id->index);
2321+
if (index < 0) {
2322+
struct _Py_unicode_runtime_ids *rt_ids = &interp->runtime->unicode_ids;
2323+
2324+
PyThread_acquire_lock(rt_ids->lock, WAIT_LOCK);
2325+
// Check again to detect concurrent access. Another thread can have
2326+
// initialized the index while this thread waited for the lock.
2327+
index = _Py_atomic_size_get(&id->index);
2328+
if (index < 0) {
2329+
assert(rt_ids->next_index < PY_SSIZE_T_MAX);
2330+
index = rt_ids->next_index;
2331+
rt_ids->next_index++;
2332+
_Py_atomic_size_set(&id->index, index);
2333+
}
2334+
PyThread_release_lock(rt_ids->lock);
23202335
}
2336+
assert(index >= 0);
23212337

23222338
PyObject *obj;
2323-
obj = PyUnicode_DecodeUTF8Stateful(id->string,
2324-
strlen(id->string),
2339+
if (index < ids->size) {
2340+
obj = ids->array[index];
2341+
if (obj) {
2342+
// Return a borrowed reference
2343+
return obj;
2344+
}
2345+
}
2346+
2347+
obj = PyUnicode_DecodeUTF8Stateful(id->string, strlen(id->string),
23252348
NULL, NULL);
23262349
if (!obj) {
23272350
return NULL;
23282351
}
23292352
PyUnicode_InternInPlace(&obj);
23302353

2331-
assert(!id->next);
2332-
id->object = obj;
2333-
id->next = static_strings;
2334-
static_strings = id;
2335-
return id->object;
2354+
if (index >= ids->size) {
2355+
// Overallocate to reduce the number of realloc
2356+
Py_ssize_t new_size = Py_MAX(index * 2, 16);
2357+
Py_ssize_t item_size = sizeof(ids->array[0]);
2358+
PyObject **new_array = PyMem_Realloc(ids->array, new_size * item_size);
2359+
if (new_array == NULL) {
2360+
PyErr_NoMemory();
2361+
return NULL;
2362+
}
2363+
memset(&new_array[ids->size], 0, (new_size - ids->size) * item_size);
2364+
ids->array = new_array;
2365+
ids->size = new_size;
2366+
}
2367+
2368+
// The array stores a strong reference
2369+
ids->array[index] = obj;
2370+
2371+
// Return a borrowed reference
2372+
return obj;
23362373
}
23372374

2375+
23382376
static void
2339-
unicode_clear_static_strings(void)
2377+
unicode_clear_identifiers(PyThreadState *tstate)
23402378
{
2341-
_Py_Identifier *tmp, *s = static_strings;
2342-
while (s) {
2343-
Py_CLEAR(s->object);
2344-
tmp = s->next;
2345-
s->next = NULL;
2346-
s = tmp;
2379+
PyInterpreterState *interp = _PyInterpreterState_GET();
2380+
struct _Py_unicode_ids *ids = &interp->unicode.ids;
2381+
for (Py_ssize_t i=0; i < ids->size; i++) {
2382+
Py_XDECREF(ids->array[i]);
23472383
}
2348-
static_strings = NULL;
2384+
ids->size = 0;
2385+
PyMem_Free(ids->array);
2386+
ids->array = NULL;
2387+
// Don't reset _PyRuntime next_index: _Py_Identifier.id remains valid
2388+
// after Py_Finalize().
23492389
}
23502390

2391+
23512392
/* Internal function, doesn't check maximum character */
23522393

23532394
PyObject*
@@ -16238,9 +16279,7 @@ _PyUnicode_Fini(PyThreadState *tstate)
1623816279
Py_CLEAR(state->latin1[i]);
1623916280
}
1624016281

16241-
if (_Py_IsMainInterpreter(tstate)) {
16242-
unicode_clear_static_strings();
16243-
}
16282+
unicode_clear_identifiers(tstate);
1624416283

1624516284
_PyUnicode_FiniEncodings(&tstate->interp->unicode.fs_codec);
1624616285
}

Python/pystate.c

+19-11
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,24 @@ _PyRuntimeState_Init_impl(_PyRuntimeState *runtime)
7373

7474
runtime->interpreters.mutex = PyThread_allocate_lock();
7575
if (runtime->interpreters.mutex == NULL) {
76-
return _PyStatus_ERR("Can't initialize threads for interpreter");
76+
return _PyStatus_NO_MEMORY();
7777
}
7878
runtime->interpreters.next_id = -1;
7979

8080
runtime->xidregistry.mutex = PyThread_allocate_lock();
8181
if (runtime->xidregistry.mutex == NULL) {
82-
return _PyStatus_ERR("Can't initialize threads for cross-interpreter data registry");
82+
return _PyStatus_NO_MEMORY();
8383
}
8484

8585
// Set it to the ID of the main thread of the main interpreter.
8686
runtime->main_thread = PyThread_get_thread_ident();
8787

88+
runtime->unicode_ids.lock = PyThread_allocate_lock();
89+
if (runtime->unicode_ids.lock == NULL) {
90+
return _PyStatus_NO_MEMORY();
91+
}
92+
runtime->unicode_ids.next_index = 0;
93+
8894
return _PyStatus_OK();
8995
}
9096

@@ -108,17 +114,17 @@ _PyRuntimeState_Fini(_PyRuntimeState *runtime)
108114
/* Force the allocator used by _PyRuntimeState_Init(). */
109115
PyMemAllocatorEx old_alloc;
110116
_PyMem_SetDefaultAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
111-
112-
if (runtime->interpreters.mutex != NULL) {
113-
PyThread_free_lock(runtime->interpreters.mutex);
114-
runtime->interpreters.mutex = NULL;
117+
#define FREE_LOCK(LOCK) \
118+
if (LOCK != NULL) { \
119+
PyThread_free_lock(LOCK); \
120+
LOCK = NULL; \
115121
}
116122

117-
if (runtime->xidregistry.mutex != NULL) {
118-
PyThread_free_lock(runtime->xidregistry.mutex);
119-
runtime->xidregistry.mutex = NULL;
120-
}
123+
FREE_LOCK(runtime->interpreters.mutex);
124+
FREE_LOCK(runtime->xidregistry.mutex);
125+
FREE_LOCK(runtime->unicode_ids.lock);
121126

127+
#undef FREE_LOCK
122128
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
123129
}
124130

@@ -139,12 +145,14 @@ _PyRuntimeState_ReInitThreads(_PyRuntimeState *runtime)
139145
int reinit_interp = _PyThread_at_fork_reinit(&runtime->interpreters.mutex);
140146
int reinit_main_id = _PyThread_at_fork_reinit(&runtime->interpreters.main->id_mutex);
141147
int reinit_xidregistry = _PyThread_at_fork_reinit(&runtime->xidregistry.mutex);
148+
int reinit_unicode_ids = _PyThread_at_fork_reinit(&runtime->unicode_ids.lock);
142149

143150
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc);
144151

145152
if (reinit_interp < 0
146153
|| reinit_main_id < 0
147-
|| reinit_xidregistry < 0)
154+
|| reinit_xidregistry < 0
155+
|| reinit_unicode_ids < 0)
148156
{
149157
return _PyStatus_ERR("Failed to reinitialize runtime locks");
150158

0 commit comments

Comments
 (0)