Skip to content

Commit 1d378e7

Browse files
committed
gh-148072: Cache pickle.dumps/loads per interpreter in XIData
Store references to pickle.dumps and pickle.loads in _PyXI_state_t so they are looked up only once per interpreter lifetime, avoiding repeated PyImport_ImportModuleAttrString calls on every cross-interpreter data transfer via pickle fallback. Benchmarks show 1.7x-3.3x speedup for InterpreterPoolExecutor when transferring mutable types (list, dict) through XIData.
1 parent 7e275d4 commit 1d378e7

File tree

3 files changed

+66
-6
lines changed

3 files changed

+66
-6
lines changed

Include/internal/pycore_crossinterp.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,12 @@ typedef struct {
265265
// heap types
266266
PyObject *PyExc_NotShareableError;
267267
} exceptions;
268+
269+
// Cached references to pickle.dumps/loads (per-interpreter).
270+
struct {
271+
PyObject *dumps;
272+
PyObject *loads;
273+
} pickle;
268274
} _PyXI_state_t;
269275

270276
#define _PyXI_GET_GLOBAL_STATE(interp) (&(interp)->runtime->xi)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Cache ``pickle.dumps`` and ``pickle.loads`` per interpreter in the XIData
2+
framework, avoiding repeated module lookups on every cross-interpreter data
3+
transfer. This speeds up :class:`~concurrent.futures.InterpreterPoolExecutor`
4+
for mutable types (``list``, ``dict``) by 1.7x--3.3x.

Python/crossinterp.c

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -568,20 +568,61 @@ _PyObject_GetXIData(PyThreadState *tstate,
568568

569569
/* pickle C-API */
570570

571+
/* Per-interpreter cache for pickle.dumps and pickle.loads.
572+
*
573+
* Each interpreter has its own cache in _PyXI_state_t.pickle, preserving
574+
* interpreter isolation. The cache is populated lazily on first use and
575+
* cleared during interpreter finalization in _Py_xi_state_fini().
576+
*
577+
* Note: the cached references are captured at first use and not invalidated
578+
* on module reload. This matches the caching pattern used elsewhere in
579+
* CPython (e.g. arraymodule.c, _decimal.c). */
580+
581+
static PyObject *
582+
_get_pickle_dumps(PyThreadState *tstate)
583+
{
584+
_PyXI_state_t *state = _PyXI_GET_STATE(tstate->interp);
585+
PyObject *dumps = state->pickle.dumps;
586+
if (dumps != NULL) {
587+
return dumps;
588+
}
589+
dumps = PyImport_ImportModuleAttrString("pickle", "dumps");
590+
if (dumps == NULL) {
591+
return NULL;
592+
}
593+
state->pickle.dumps = dumps; // owns the reference
594+
return dumps;
595+
}
596+
597+
static PyObject *
598+
_get_pickle_loads(PyThreadState *tstate)
599+
{
600+
_PyXI_state_t *state = _PyXI_GET_STATE(tstate->interp);
601+
PyObject *loads = state->pickle.loads;
602+
if (loads != NULL) {
603+
return loads;
604+
}
605+
loads = PyImport_ImportModuleAttrString("pickle", "loads");
606+
if (loads == NULL) {
607+
return NULL;
608+
}
609+
state->pickle.loads = loads; // owns the reference
610+
return loads;
611+
}
612+
571613
struct _pickle_context {
572614
PyThreadState *tstate;
573615
};
574616

575617
static PyObject *
576618
_PyPickle_Dumps(struct _pickle_context *ctx, PyObject *obj)
577619
{
578-
PyObject *dumps = PyImport_ImportModuleAttrString("pickle", "dumps");
620+
PyObject *dumps = _get_pickle_dumps(ctx->tstate);
579621
if (dumps == NULL) {
580622
return NULL;
581623
}
582-
PyObject *bytes = PyObject_CallOneArg(dumps, obj);
583-
Py_DECREF(dumps);
584-
return bytes;
624+
// dumps is a borrowed reference from the cache.
625+
return PyObject_CallOneArg(dumps, obj);
585626
}
586627

587628

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

638679
PyObject *exc = NULL;
639-
PyObject *loads = PyImport_ImportModuleAttrString("pickle", "loads");
680+
// loads is a borrowed reference from the per-interpreter cache.
681+
PyObject *loads = _get_pickle_loads(tstate);
640682
if (loads == NULL) {
641683
return NULL;
642684
}
@@ -682,7 +724,6 @@ _PyPickle_Loads(struct _unpickle_context *ctx, PyObject *pickled)
682724
// It might make sense to chain it (__context__).
683725
_PyErr_SetRaisedException(tstate, exc);
684726
}
685-
Py_DECREF(loads);
686727
return obj;
687728
}
688729

@@ -3094,6 +3135,10 @@ _Py_xi_state_init(_PyXI_state_t *state, PyInterpreterState *interp)
30943135
assert(state != NULL);
30953136
assert(interp == NULL || state == _PyXI_GET_STATE(interp));
30963137

3138+
// Initialize pickle function cache (before any fallible ops).
3139+
state->pickle.dumps = NULL;
3140+
state->pickle.loads = NULL;
3141+
30973142
xid_lookup_init(&state->data_lookup);
30983143

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

3164+
// Clear pickle function cache first: the cached functions may hold
3165+
// references to modules cleaned up by later finalization steps.
3166+
Py_CLEAR(state->pickle.dumps);
3167+
Py_CLEAR(state->pickle.loads);
3168+
31193169
fini_heap_exctypes(&state->exceptions);
31203170
if (interp != NULL) {
31213171
fini_static_exctypes(&state->exceptions, interp);

0 commit comments

Comments
 (0)