Skip to content

Commit 43f850d

Browse files
committed
pythongh-146041: Avoid lock in sys.intern() for already interned strings
1 parent 87b9280 commit 43f850d

4 files changed

Lines changed: 29 additions & 18 deletions

File tree

InternalDocs/string_interning.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,9 @@ The key and value of each entry in this dict reference the same object.
5252

5353
## Immortality and reference counting
5454

55-
Invariant: Every immortal string is interned.
55+
In the GIL-enabled build interned strings may be mortal or immortal. In the
56+
free-threaded build, interned strings are always immortal.
5657

57-
In practice, this means that you must not use `_Py_SetImmortal` on
58-
a string. (If you know it's already immortal, don't immortalize it;
59-
if you know it's not interned you might be immortalizing a redundant copy;
60-
if it's interned and mortal it needs extra processing in
61-
`_PyUnicode_InternImmortal`.)
62-
63-
The converse is not true: interned strings can be mortal.
6458
For mortal interned strings:
6559

6660
- the 2 references from the interned dict (key & value) are excluded from
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix free-threading scaling bottleneck in :func:`sys.intern` and
2+
:c:func:`PyObject_SetAttr` by avoiding the interpreter-wide lock when the string
3+
is already interned and immortalized.

Objects/object.c

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2678,13 +2678,6 @@ _Py_NewReferenceNoTotal(PyObject *op)
26782678
void
26792679
_Py_SetImmortalUntracked(PyObject *op)
26802680
{
2681-
#ifdef Py_DEBUG
2682-
// For strings, use _PyUnicode_InternImmortal instead.
2683-
if (PyUnicode_CheckExact(op)) {
2684-
assert(PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL
2685-
|| PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL_STATIC);
2686-
}
2687-
#endif
26882681
// Check if already immortal to avoid degrading from static immortal to plain immortal
26892682
if (_Py_IsImmortal(op)) {
26902683
return;

Objects/unicodeobject.c

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15946,8 +15946,11 @@ immortalize_interned(PyObject *s)
1594615946
_Py_DecRefTotal(_PyThreadState_GET());
1594715947
}
1594815948
#endif
15949-
FT_ATOMIC_STORE_UINT8_RELAXED(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_IMMORTAL);
1595015949
_Py_SetImmortal(s);
15950+
// The switch to SSTATE_INTERNED_IMMORTAL must be the last thing done here
15951+
// to synchronize with the check in intern_common() that avoids locking if
15952+
// the string is already immortal.
15953+
FT_ATOMIC_STORE_UINT8(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_IMMORTAL);
1595115954
}
1595215955

1595315956
static /* non-null */ PyObject*
@@ -16027,7 +16030,25 @@ intern_common(PyInterpreterState *interp, PyObject *s /* stolen */,
1602716030
/* Do a setdefault on the per-interpreter cache. */
1602816031
PyObject *interned = get_interned_dict(interp);
1602916032
assert(interned != NULL);
16030-
16033+
#ifdef Py_GIL_DISABLED
16034+
// Lock-free fast path: check if there's already an interned copy that
16035+
// is in its final immortal state.
16036+
PyObject *r;
16037+
int res = PyDict_GetItemRef(interned, s, &r);
16038+
if (res < 0) {
16039+
PyErr_Clear();
16040+
return s;
16041+
}
16042+
if (res > 0) {
16043+
unsigned int state = _Py_atomic_load_uint8(&_PyUnicode_STATE(r).interned);
16044+
if (state == SSTATE_INTERNED_IMMORTAL) {
16045+
Py_DECREF(s);
16046+
return r;
16047+
}
16048+
// Not yet fully interned; fall through to the locking path.
16049+
Py_DECREF(r);
16050+
}
16051+
#endif
1603116052
LOCK_INTERNED(interp);
1603216053
PyObject *t;
1603316054
{
@@ -16064,7 +16085,7 @@ intern_common(PyInterpreterState *interp, PyObject *s /* stolen */,
1606416085
Py_DECREF(s);
1606516086
Py_DECREF(s);
1606616087
}
16067-
FT_ATOMIC_STORE_UINT8_RELAXED(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_MORTAL);
16088+
FT_ATOMIC_STORE_UINT8(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_MORTAL);
1606816089

1606916090
/* INTERNED_MORTAL -> INTERNED_IMMORTAL (if needed) */
1607016091

0 commit comments

Comments
 (0)