Skip to content

Commit c39755e

Browse files
committed
pythongh-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 c39755e

File tree

2 files changed

+53
-6
lines changed

2 files changed

+53
-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)

Python/crossinterp.c

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

569569
/* pickle C-API */
570570

571+
/* Get a cached reference to pickle.dumps for the current interpreter. */
572+
static PyObject *
573+
_get_pickle_dumps(PyThreadState *tstate)
574+
{
575+
_PyXI_state_t *state = _PyXI_GET_STATE(tstate->interp);
576+
PyObject *dumps = state->pickle.dumps;
577+
if (dumps != NULL) {
578+
return dumps;
579+
}
580+
dumps = PyImport_ImportModuleAttrString("pickle", "dumps");
581+
if (dumps == NULL) {
582+
return NULL;
583+
}
584+
state->pickle.dumps = dumps; // owns the reference
585+
return dumps;
586+
}
587+
588+
/* Get a cached reference to pickle.loads for the current interpreter. */
589+
static PyObject *
590+
_get_pickle_loads(PyThreadState *tstate)
591+
{
592+
_PyXI_state_t *state = _PyXI_GET_STATE(tstate->interp);
593+
PyObject *loads = state->pickle.loads;
594+
if (loads != NULL) {
595+
return loads;
596+
}
597+
loads = PyImport_ImportModuleAttrString("pickle", "loads");
598+
if (loads == NULL) {
599+
return NULL;
600+
}
601+
state->pickle.loads = loads; // owns the reference
602+
return loads;
603+
}
604+
571605
struct _pickle_context {
572606
PyThreadState *tstate;
573607
};
574608

575609
static PyObject *
576610
_PyPickle_Dumps(struct _pickle_context *ctx, PyObject *obj)
577611
{
578-
PyObject *dumps = PyImport_ImportModuleAttrString("pickle", "dumps");
612+
PyObject *dumps = _get_pickle_dumps(ctx->tstate);
579613
if (dumps == NULL) {
580614
return NULL;
581615
}
582-
PyObject *bytes = PyObject_CallOneArg(dumps, obj);
583-
Py_DECREF(dumps);
584-
return bytes;
616+
// dumps is a borrowed reference from the cache.
617+
return PyObject_CallOneArg(dumps, obj);
585618
}
586619

587620

@@ -636,7 +669,8 @@ _PyPickle_Loads(struct _unpickle_context *ctx, PyObject *pickled)
636669
PyThreadState *tstate = ctx->tstate;
637670

638671
PyObject *exc = NULL;
639-
PyObject *loads = PyImport_ImportModuleAttrString("pickle", "loads");
672+
// loads is a borrowed reference from the per-interpreter cache.
673+
PyObject *loads = _get_pickle_loads(tstate);
640674
if (loads == NULL) {
641675
return NULL;
642676
}
@@ -682,7 +716,6 @@ _PyPickle_Loads(struct _unpickle_context *ctx, PyObject *pickled)
682716
// It might make sense to chain it (__context__).
683717
_PyErr_SetRaisedException(tstate, exc);
684718
}
685-
Py_DECREF(loads);
686719
return obj;
687720
}
688721

@@ -3107,6 +3140,10 @@ _Py_xi_state_init(_PyXI_state_t *state, PyInterpreterState *interp)
31073140
return -1;
31083141
}
31093142

3143+
// Initialize pickle function cache.
3144+
state->pickle.dumps = NULL;
3145+
state->pickle.loads = NULL;
3146+
31103147
return 0;
31113148
}
31123149

@@ -3116,6 +3153,10 @@ _Py_xi_state_fini(_PyXI_state_t *state, PyInterpreterState *interp)
31163153
assert(state != NULL);
31173154
assert(interp == NULL || state == _PyXI_GET_STATE(interp));
31183155

3156+
// Clear pickle function cache before other cleanup.
3157+
Py_CLEAR(state->pickle.dumps);
3158+
Py_CLEAR(state->pickle.loads);
3159+
31193160
fini_heap_exctypes(&state->exceptions);
31203161
if (interp != NULL) {
31213162
fini_static_exctypes(&state->exceptions, interp);

0 commit comments

Comments
 (0)