Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions InternalDocs/string_interning.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,9 @@ The key and value of each entry in this dict reference the same object.

## Immortality and reference counting

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

In practice, this means that you must not use `_Py_SetImmortal` on
a string. (If you know it's already immortal, don't immortalize it;
if you know it's not interned you might be immortalizing a redundant copy;
if it's interned and mortal it needs extra processing in
`_PyUnicode_InternImmortal`.)

The converse is not true: interned strings can be mortal.
For mortal interned strings:

- the 2 references from the interned dict (key & value) are excluded from
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix free-threading scaling bottleneck in :func:`sys.intern` and
:c:func:`PyObject_SetAttr` by avoiding the interpreter-wide lock when the string
is already interned and immortalized.
11 changes: 2 additions & 9 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -1999,7 +1999,7 @@ _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name,
}

Py_INCREF(name);
Py_INCREF(tp);
_Py_INCREF_TYPE(tp);

PyThreadState *tstate = _PyThreadState_GET();
_PyCStackRef cref;
Expand Down Expand Up @@ -2074,7 +2074,7 @@ _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name,
}
done:
_PyThreadState_PopCStackRef(tstate, &cref);
Py_DECREF(tp);
_Py_DECREF_TYPE(tp);
Py_DECREF(name);
return res;
}
Expand Down Expand Up @@ -2728,13 +2728,6 @@ _Py_NewReferenceNoTotal(PyObject *op)
void
_Py_SetImmortalUntracked(PyObject *op)
{
#ifdef Py_DEBUG
// For strings, use _PyUnicode_InternImmortal instead.
if (PyUnicode_CheckExact(op)) {
assert(PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL
|| PyUnicode_CHECK_INTERNED(op) == SSTATE_INTERNED_IMMORTAL_STATIC);
}
#endif
// Check if already immortal to avoid degrading from static immortal to plain immortal
if (_Py_IsImmortal(op)) {
return;
Expand Down
24 changes: 22 additions & 2 deletions Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -14188,8 +14188,11 @@ immortalize_interned(PyObject *s)
_Py_DecRefTotal(_PyThreadState_GET());
}
#endif
FT_ATOMIC_STORE_UINT8_RELAXED(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_IMMORTAL);
_Py_SetImmortal(s);
// The switch to SSTATE_INTERNED_IMMORTAL must be the last thing done here
// to synchronize with the check in intern_common() that avoids locking if
// the string is already immortal.
FT_ATOMIC_STORE_UINT8(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_IMMORTAL);
}

static /* non-null */ PyObject*
Expand Down Expand Up @@ -14271,6 +14274,23 @@ intern_common(PyInterpreterState *interp, PyObject *s /* stolen */,
assert(interned != NULL);
#ifdef Py_GIL_DISABLED
# define INTERN_MUTEX &_Py_INTERP_CACHED_OBJECT(interp, interned_mutex)
// Lock-free fast path: check if there's already an interned copy that
// is in its final immortal state.
PyObject *r;
int res = PyDict_GetItemRef(interned, s, &r);
if (res < 0) {
PyErr_Clear();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearing the error here feels a little weird to me, but it's consistent with the rest of the function.

return s;
}
if (res > 0) {
unsigned int state = _Py_atomic_load_uint8(&_PyUnicode_STATE(r).interned);
if (state == SSTATE_INTERNED_IMMORTAL) {
Py_DECREF(s);
return r;
}
// Not yet fully interned; fall through to the locking path.
Py_DECREF(r);
}
#endif
FT_MUTEX_LOCK(INTERN_MUTEX);
PyObject *t;
Expand Down Expand Up @@ -14308,7 +14328,7 @@ intern_common(PyInterpreterState *interp, PyObject *s /* stolen */,
Py_DECREF(s);
Py_DECREF(s);
}
FT_ATOMIC_STORE_UINT8_RELAXED(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_MORTAL);
FT_ATOMIC_STORE_UINT8(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_MORTAL);

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

Expand Down
9 changes: 9 additions & 0 deletions Tools/ftscalingbench/ftscalingbench.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,15 @@ def deepcopy():
for i in range(40 * WORK_SCALE):
copy.deepcopy(x)

@register_benchmark
def setattr_non_interned():
prefix = "prefix"
obj = MyObject()
for _ in range(1000 * WORK_SCALE):
setattr(obj, f"{prefix}_a", None)
setattr(obj, f"{prefix}_b", None)
setattr(obj, f"{prefix}_c", None)


def bench_one_thread(func):
t0 = time.perf_counter_ns()
Expand Down
Loading