Skip to content

Commit 6142b58

Browse files
committed
gh-146041: Avoid lock in sys.intern() for already interned strings
1 parent dc24b8a commit 6142b58

File tree

4 files changed

+27
-17
lines changed

4 files changed

+27
-17
lines changed

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
@@ -2728,13 +2728,6 @@ _Py_NewReferenceNoTotal(PyObject *op)
27282728
void
27292729
_Py_SetImmortalUntracked(PyObject *op)
27302730
{
2731-
#ifdef Py_DEBUG
2732-
// For strings, use _PyUnicode_InternImmortal instead.
2733-
if (PyUnicode_CheckExact(op)) {
2734-
assert(PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL
2735-
|| PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL_STATIC);
2736-
}
2737-
#endif
27382731
// Check if already immortal to avoid degrading from static immortal to plain immortal
27392732
if (_Py_IsImmortal(op)) {
27402733
return;

Objects/unicodeobject.c

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14188,8 +14188,11 @@ immortalize_interned(PyObject *s)
1418814188
_Py_DecRefTotal(_PyThreadState_GET());
1418914189
}
1419014190
#endif
14191-
FT_ATOMIC_STORE_UINT8_RELAXED(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_IMMORTAL);
1419214191
_Py_SetImmortal(s);
14192+
// The switch to SSTATE_INTERNED_IMMORTAL must be the last thing done here
14193+
// to synchronize with the check in intern_common() that avoids locking if
14194+
// the string is already immortal.
14195+
FT_ATOMIC_STORE_UINT8(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_IMMORTAL);
1419314196
}
1419414197

1419514198
static /* non-null */ PyObject*
@@ -14271,6 +14274,23 @@ intern_common(PyInterpreterState *interp, PyObject *s /* stolen */,
1427114274
assert(interned != NULL);
1427214275
#ifdef Py_GIL_DISABLED
1427314276
# define INTERN_MUTEX &_Py_INTERP_CACHED_OBJECT(interp, interned_mutex)
14277+
// Lock-free fast path: check if there's already an interned copy that
14278+
// is in its final immortal state.
14279+
PyObject *r;
14280+
int res = PyDict_GetItemRef(interned, s, &r);
14281+
if (res < 0) {
14282+
PyErr_Clear();
14283+
return s;
14284+
}
14285+
if (res > 0) {
14286+
unsigned int state = _Py_atomic_load_uint8(&_PyUnicode_STATE(r).interned);
14287+
if (state == SSTATE_INTERNED_IMMORTAL) {
14288+
Py_DECREF(s);
14289+
return r;
14290+
}
14291+
// Not yet fully interned; fall through to the locking path.
14292+
Py_DECREF(r);
14293+
}
1427414294
#endif
1427514295
FT_MUTEX_LOCK(INTERN_MUTEX);
1427614296
PyObject *t;
@@ -14308,7 +14328,7 @@ intern_common(PyInterpreterState *interp, PyObject *s /* stolen */,
1430814328
Py_DECREF(s);
1430914329
Py_DECREF(s);
1431014330
}
14311-
FT_ATOMIC_STORE_UINT8_RELAXED(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_MORTAL);
14331+
FT_ATOMIC_STORE_UINT8(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_MORTAL);
1431214332

1431314333
/* INTERNED_MORTAL -> INTERNED_IMMORTAL (if needed) */
1431414334

0 commit comments

Comments
 (0)