diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-07-00-00-00.gh-issue-148223.Rk4xPe.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-07-00-00-00.gh-issue-148223.Rk4xPe.rst new file mode 100644 index 00000000000000..c9308cf9219d13 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-07-00-00-00.gh-issue-148223.Rk4xPe.rst @@ -0,0 +1,4 @@ +Avoid unnecessary ``NotShareableError`` creation in +``_PyObject_GetXIData()`` when falling back to pickle for types not in the +XIData registry. This speeds up cross-interpreter transfers of mutable +types such as ``list`` and ``dict``. diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 4cd4b32ef906bb..b2aabaa5df7508 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -536,25 +536,54 @@ _PyObject_GetXIData(PyThreadState *tstate, case _PyXIDATA_XIDATA_ONLY: return _get_xidata(tstate, obj, fallback, xidata); case _PyXIDATA_FULL_FALLBACK: - if (_get_xidata(tstate, obj, fallback, xidata) == 0) { - return 0; + { + // Check the type registry first to avoid unnecessary exception + // creation when falling back to pickle for unregistered types. + dlcontext_t ctx; + if (get_lookup_context(tstate, &ctx) < 0) { + return -1; } - PyObject *exc = _PyErr_GetRaisedException(tstate); + _PyXIData_getdata_t getdata = lookup_getdata(&ctx, obj); + if (getdata.basic != NULL || getdata.fallback != NULL) { + // Type is in the registry. Use the normal path which may + // still fail (e.g. a tuple with non-shareable elements). + if (_get_xidata(tstate, obj, fallback, xidata) == 0) { + return 0; + } + // Save the exception to restore if all fallbacks fail. + PyObject *exc = _PyErr_GetRaisedException(tstate); + if (PyFunction_Check(obj)) { + if (_PyFunction_GetXIData(tstate, obj, xidata) == 0) { + Py_DECREF(exc); + return 0; + } + _PyErr_Clear(tstate); + } + if (_PyPickle_GetXIData(tstate, obj, xidata) == 0) { + Py_DECREF(exc); + return 0; + } + _PyErr_SetRaisedException(tstate, exc); + return -1; + } + // Type is NOT in the registry. Skip _get_xidata() entirely + // to avoid creating and discarding a NotShareableError. if (PyFunction_Check(obj)) { if (_PyFunction_GetXIData(tstate, obj, xidata) == 0) { - Py_DECREF(exc); return 0; } _PyErr_Clear(tstate); } // We could try _PyMarshal_GetXIData() but we won't for now. if (_PyPickle_GetXIData(tstate, obj, xidata) == 0) { - Py_DECREF(exc); return 0; } - // Raise the original exception. - _PyErr_SetRaisedException(tstate, exc); + // All fallbacks failed. Raise the same NotShareableError + // as the non-optimized path would. + _PyErr_Clear(tstate); + _set_xid_lookup_failure(tstate, obj, NULL, NULL); return -1; + } default: #ifdef Py_DEBUG Py_FatalError("unsupported xidata fallback option");