Skip to content
Merged
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
6 changes: 6 additions & 0 deletions Include/internal/pycore_crossinterp.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@ typedef struct {
// heap types
PyObject *PyExc_NotShareableError;
} exceptions;

// Cached references to pickle.dumps/loads (per-interpreter).
struct {
PyObject *dumps;
PyObject *loads;
} pickle;
} _PyXI_state_t;

#define _PyXI_GET_GLOBAL_STATE(interp) (&(interp)->runtime->xi)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Cache ``pickle.dumps`` and ``pickle.loads`` per interpreter in the XIData
framework, avoiding repeated module lookups on every cross-interpreter data
transfer. This speeds up :class:`~concurrent.futures.InterpreterPoolExecutor`
for mutable types (``list``, ``dict``) by 1.7x--3.3x.
62 changes: 56 additions & 6 deletions Python/crossinterp.c
Original file line number Diff line number Diff line change
Expand Up @@ -568,20 +568,61 @@ _PyObject_GetXIData(PyThreadState *tstate,

/* pickle C-API */

/* Per-interpreter cache for pickle.dumps and pickle.loads.
*
* Each interpreter has its own cache in _PyXI_state_t.pickle, preserving
* interpreter isolation. The cache is populated lazily on first use and
* cleared during interpreter finalization in _Py_xi_state_fini().
*
* Note: the cached references are captured at first use and not invalidated
* on module reload. This matches the caching pattern used elsewhere in
* CPython (e.g. arraymodule.c, _decimal.c). */

static PyObject *
_get_pickle_dumps(PyThreadState *tstate)
{
_PyXI_state_t *state = _PyXI_GET_STATE(tstate->interp);
PyObject *dumps = state->pickle.dumps;
if (dumps != NULL) {
return dumps;
}
dumps = PyImport_ImportModuleAttrString("pickle", "dumps");
if (dumps == NULL) {
return NULL;
}
state->pickle.dumps = dumps; // owns the reference
return dumps;
}

static PyObject *
_get_pickle_loads(PyThreadState *tstate)
{
_PyXI_state_t *state = _PyXI_GET_STATE(tstate->interp);
PyObject *loads = state->pickle.loads;
if (loads != NULL) {
return loads;
}
loads = PyImport_ImportModuleAttrString("pickle", "loads");
if (loads == NULL) {
return NULL;
}
state->pickle.loads = loads; // owns the reference
return loads;
}

struct _pickle_context {
PyThreadState *tstate;
};

static PyObject *
_PyPickle_Dumps(struct _pickle_context *ctx, PyObject *obj)
{
PyObject *dumps = PyImport_ImportModuleAttrString("pickle", "dumps");
PyObject *dumps = _get_pickle_dumps(ctx->tstate);
if (dumps == NULL) {
return NULL;
}
PyObject *bytes = PyObject_CallOneArg(dumps, obj);
Py_DECREF(dumps);
return bytes;
// dumps is a borrowed reference from the cache.
return PyObject_CallOneArg(dumps, obj);
}


Expand Down Expand Up @@ -636,7 +677,8 @@ _PyPickle_Loads(struct _unpickle_context *ctx, PyObject *pickled)
PyThreadState *tstate = ctx->tstate;

PyObject *exc = NULL;
PyObject *loads = PyImport_ImportModuleAttrString("pickle", "loads");
// loads is a borrowed reference from the per-interpreter cache.
PyObject *loads = _get_pickle_loads(tstate);
if (loads == NULL) {
return NULL;
}
Expand Down Expand Up @@ -682,7 +724,6 @@ _PyPickle_Loads(struct _unpickle_context *ctx, PyObject *pickled)
// It might make sense to chain it (__context__).
_PyErr_SetRaisedException(tstate, exc);
}
Py_DECREF(loads);
return obj;
}

Expand Down Expand Up @@ -3094,6 +3135,10 @@ _Py_xi_state_init(_PyXI_state_t *state, PyInterpreterState *interp)
assert(state != NULL);
assert(interp == NULL || state == _PyXI_GET_STATE(interp));

// Initialize pickle function cache (before any fallible ops).
state->pickle.dumps = NULL;
state->pickle.loads = NULL;

xid_lookup_init(&state->data_lookup);

// Initialize exceptions.
Expand All @@ -3116,6 +3161,11 @@ _Py_xi_state_fini(_PyXI_state_t *state, PyInterpreterState *interp)
assert(state != NULL);
assert(interp == NULL || state == _PyXI_GET_STATE(interp));

// Clear pickle function cache first: the cached functions may hold
// references to modules cleaned up by later finalization steps.
Py_CLEAR(state->pickle.dumps);
Py_CLEAR(state->pickle.loads);

fini_heap_exctypes(&state->exceptions);
if (interp != NULL) {
fini_static_exctypes(&state->exceptions, interp);
Expand Down
Loading