Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-111968: Use per-thread freelists for dict in free-threading #114323

Merged
merged 25 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 22 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
3 changes: 2 additions & 1 deletion Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#include "pycore_freelist.h" // _PyFreeListState
#include "pycore_identifier.h" // _Py_Identifier
#include "pycore_object.h" // PyDictOrValues

Expand Down Expand Up @@ -69,7 +70,7 @@ extern PyObject* _PyDictView_Intersect(PyObject* self, PyObject *other);

/* runtime lifecycle */

extern void _PyDict_Fini(PyInterpreterState *interp);
extern void _PyDict_Fini(PyInterpreterState *state);


/* other API */
Expand Down
19 changes: 0 additions & 19 deletions Include/internal/pycore_dict_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif


#ifndef WITH_FREELISTS
// without freelists
# define PyDict_MAXFREELIST 0
#endif

#ifndef PyDict_MAXFREELIST
# define PyDict_MAXFREELIST 80
#endif

#define DICT_MAX_WATCHERS 8

struct _Py_dict_state {
Expand All @@ -26,15 +16,6 @@ struct _Py_dict_state {
* time that a dictionary is modified. */
uint64_t global_version;
uint32_t next_keys_version;

#if PyDict_MAXFREELIST > 0
/* Dictionary reuse scheme to save calls to malloc and free */
PyDictObject *free_list[PyDict_MAXFREELIST];
PyDictKeysObject *keys_free_list[PyDict_MAXFREELIST];
int numfree;
int keys_numfree;
#endif

PyDict_WatchCallback watchers[DICT_MAX_WATCHERS];
};

Expand Down
13 changes: 13 additions & 0 deletions Include/internal/pycore_freelist.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ extern "C" {
# define PyTuple_NFREELISTS PyTuple_MAXSAVESIZE
# define PyTuple_MAXFREELIST 2000
# define PyList_MAXFREELIST 80
# define PyDict_MAXFREELIST 80
# define PyFloat_MAXFREELIST 100
# define PyContext_MAXFREELIST 255
# define _PyAsyncGen_MAXFREELIST 80
Expand All @@ -25,6 +26,7 @@ extern "C" {
# define PyTuple_NFREELISTS 0
# define PyTuple_MAXFREELIST 0
# define PyList_MAXFREELIST 0
# define PyDict_MAXFREELIST 0
# define PyFloat_MAXFREELIST 0
# define PyContext_MAXFREELIST 0
# define _PyAsyncGen_MAXFREELIST 0
Expand Down Expand Up @@ -65,6 +67,16 @@ struct _Py_float_state {
#endif
};

struct _Py_dict_freelist {
#ifdef WITH_FREELISTS
/* Dictionary reuse scheme to save calls to malloc and free */
PyDictObject *free_list[PyDict_MAXFREELIST];
PyDictKeysObject *keys_free_list[PyDict_MAXFREELIST];
int numfree;
int keys_numfree;
#endif
};
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved

struct _Py_slice_state {
#ifdef WITH_FREELISTS
/* Using a cache is very effective since typically only a single slice is
Expand Down Expand Up @@ -106,6 +118,7 @@ typedef struct _Py_freelist_state {
struct _Py_float_state floats;
struct _Py_tuple_state tuples;
struct _Py_list_state lists;
struct _Py_dict_freelist dicts;
struct _Py_slice_state slices;
struct _Py_context_state contexts;
struct _Py_async_gen_state async_gens;
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_gc.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ extern void _PyTuple_ClearFreeList(_PyFreeListState *state, int is_finalization)
extern void _PyFloat_ClearFreeList(_PyFreeListState *state, int is_finalization);
extern void _PyList_ClearFreeList(_PyFreeListState *state, int is_finalization);
extern void _PySlice_ClearCache(_PyFreeListState *state);
extern void _PyDict_ClearFreeList(PyInterpreterState *interp);
extern void _PyDict_ClearFreeList(_PyFreeListState *state, int is_finalization);
extern void _PyAsyncGen_ClearFreeLists(_PyFreeListState *state, int is_finalization);
extern void _PyContext_ClearFreeList(_PyFreeListState *state, int is_finalization);
extern void _Py_ScheduleGC(PyInterpreterState *interp);
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ extern "C" {
#include "pycore_dtoa.h" // struct _dtoa_state
#include "pycore_exceptions.h" // struct _Py_exc_state
#include "pycore_floatobject.h" // struct _Py_float_state
#include "pycore_freelist.h" // struct _Py_freelist_state
#include "pycore_function.h" // FUNC_MAX_WATCHERS
#include "pycore_gc.h" // struct _gc_runtime_state
#include "pycore_genobject.h" // struct _Py_async_gen_state
Expand Down Expand Up @@ -230,7 +231,6 @@ struct _is {
struct _dtoa_state dtoa;
struct _py_func_state func_state;

struct _Py_tuple_state tuple;
corona10 marked this conversation as resolved.
Show resolved Hide resolved
struct _Py_dict_state dict_state;
struct _Py_exc_state exc_state;

Expand Down
85 changes: 37 additions & 48 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
#include "pycore_ceval.h" // _PyEval_GetBuiltin()
#include "pycore_code.h" // stats
#include "pycore_dict.h" // export _PyDict_SizeOf()
#include "pycore_freelist.h" // _PyFreeListState_GET()
#include "pycore_gc.h" // _PyObject_GC_IS_TRACKED()
#include "pycore_object.h" // _PyObject_GC_TRACK(), _PyDebugAllocatorStats()
#include "pycore_pyerrors.h" // _PyErr_GetRaisedException()
Expand Down Expand Up @@ -242,40 +243,43 @@
#include "clinic/dictobject.c.h"


#if PyDict_MAXFREELIST > 0
static struct _Py_dict_state *
get_dict_state(PyInterpreterState *interp)
#ifdef WITH_FREELISTS
static struct _Py_dict_freelist *
get_dict_state(void)
{
return &interp->dict_state;
_PyFreeListState *state = _PyFreeListState_GET();
return &state->dicts;
}
#endif


void
_PyDict_ClearFreeList(PyInterpreterState *interp)
_PyDict_ClearFreeList(_PyFreeListState *freelist_state, int is_finalization)
{
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = &interp->dict_state;
while (state->numfree) {
#ifdef WITH_FREELISTS
struct _Py_dict_freelist *state = &freelist_state->dicts;
while (state->numfree > 0) {
PyDictObject *op = state->free_list[--state->numfree];
assert(PyDict_CheckExact(op));
PyObject_GC_Del(op);
}
while (state->keys_numfree) {
while (state->keys_numfree > 0) {
PyMem_Free(state->keys_free_list[--state->keys_numfree]);
}
if (is_finalization) {
state->numfree = -1;
state->keys_numfree = -1;
}
#endif
}


void
_PyDict_Fini(PyInterpreterState *interp)
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
{
_PyDict_ClearFreeList(interp);
#if defined(Py_DEBUG) && PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = &interp->dict_state;
state->numfree = -1;
state->keys_numfree = -1;
// With Py_GIL_DISABLED:
// the freelists for the current thread state have already been cleared.
#ifndef Py_GIL_DISABLED
_PyDict_ClearFreeList(freelist_state, 1);

Check failure on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Address sanitizer

‘freelist_state’ undeclared (first use in this function); did you mean ‘atexit_state’?

Check failure on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Ubuntu SSL tests with OpenSSL (1.1.1w)

‘freelist_state’ undeclared (first use in this function); did you mean ‘atexit_state’?

Check failure on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Ubuntu SSL tests with OpenSSL (3.0.11)

‘freelist_state’ undeclared (first use in this function); did you mean ‘atexit_state’?

Check failure on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Ubuntu SSL tests with OpenSSL (3.1.3)

‘freelist_state’ undeclared (first use in this function); did you mean ‘atexit_state’?

Check failure on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Hypothesis tests on Ubuntu

‘freelist_state’ undeclared (first use in this function); did you mean ‘atexit_state’?

Check failure on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'freelist_state': undeclared identifier [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check warning on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'function': '_PyFreeListState *' differs in levels of indirection from 'int' [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check warning on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Windows / build and test (x64)

'_PyDict_ClearFreeList': different types for formal and actual parameter 1 [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check failure on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Windows / build (arm64)

'freelist_state': undeclared identifier [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check warning on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Windows / build (arm64)

'function': '_PyFreeListState *' differs in levels of indirection from 'int' [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check warning on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Windows / build (arm64)

'_PyDict_ClearFreeList': different types for formal and actual parameter 1 [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check failure on line 282 in Objects/dictobject.c

View workflow job for this annotation

GitHub Actions / Ubuntu / build and test

‘freelist_state’ undeclared (first use in this function); did you mean ‘atexit_state’?
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
#endif
}

Expand All @@ -290,17 +294,16 @@
void
_PyDict_DebugMallocStats(FILE *out)
{
#if PyDict_MAXFREELIST > 0
PyInterpreterState *interp = _PyInterpreterState_GET();
struct _Py_dict_state *state = get_dict_state(interp);
#ifdef WITH_FREELISTS
struct _Py_dict_freelist *state = get_dict_state();
_PyDebugAllocatorStats(out, "free PyDictObject",
state->numfree, sizeof(PyDictObject));
#endif
}

#define DK_MASK(dk) (DK_SIZE(dk)-1)

static void free_keys_object(PyInterpreterState *interp, PyDictKeysObject *keys);
static void free_keys_object(PyDictKeysObject *keys);

/* PyDictKeysObject has refcounts like PyObject does, so we have the
following two functions to mirror what Py_INCREF() and Py_DECREF() do.
Expand Down Expand Up @@ -348,7 +351,7 @@
Py_XDECREF(entries[i].me_value);
}
}
free_keys_object(interp, dk);
free_keys_object(dk);
}
}

Expand Down Expand Up @@ -643,12 +646,8 @@
log2_bytes = log2_size + 2;
}

#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state(interp);
#ifdef Py_DEBUG
// new_keys_object() must not be called after _PyDict_Fini()
assert(state->keys_numfree != -1);
#endif
#ifdef WITH_FREELISTS
struct _Py_dict_freelist *state = get_dict_state();
if (log2_size == PyDict_LOG_MINSIZE && unicode && state->keys_numfree > 0) {
dk = state->keys_free_list[--state->keys_numfree];
OBJECT_STAT_INC(from_freelist);
Expand Down Expand Up @@ -680,16 +679,13 @@
}

static void
free_keys_object(PyInterpreterState *interp, PyDictKeysObject *keys)
free_keys_object(PyDictKeysObject *keys)
{
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state(interp);
#ifdef Py_DEBUG
// free_keys_object() must not be called after _PyDict_Fini()
assert(state->keys_numfree != -1);
#endif
#ifdef WITH_FREELISTS
struct _Py_dict_freelist *state = get_dict_state();
if (DK_LOG_SIZE(keys) == PyDict_LOG_MINSIZE
&& state->keys_numfree < PyDict_MAXFREELIST
&& state->keys_numfree >= 0
&& DK_IS_UNICODE(keys)) {
state->keys_free_list[state->keys_numfree++] = keys;
OBJECT_STAT_INC(to_freelist);
Expand Down Expand Up @@ -730,13 +726,9 @@
{
PyDictObject *mp;
assert(keys != NULL);
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state(interp);
#ifdef Py_DEBUG
// new_dict() must not be called after _PyDict_Fini()
assert(state->numfree != -1);
#endif
if (state->numfree) {
#ifdef WITH_FREELISTS
struct _Py_dict_freelist *state = get_dict_state();
if (state->numfree > 0) {
mp = state->free_list[--state->numfree];
assert (mp != NULL);
assert (Py_IS_TYPE(mp, &PyDict_Type));
Expand Down Expand Up @@ -1549,7 +1541,7 @@
#endif
assert(oldkeys->dk_kind != DICT_KEYS_SPLIT);
assert(oldkeys->dk_refcnt == 1);
free_keys_object(interp, oldkeys);
free_keys_object(oldkeys);
}
}

Expand Down Expand Up @@ -2460,13 +2452,10 @@
assert(keys->dk_refcnt == 1 || keys == Py_EMPTY_KEYS);
dictkeys_decref(interp, keys);
}
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state(interp);
#ifdef Py_DEBUG
// new_dict() must not be called after _PyDict_Fini()
assert(state->numfree != -1);
#endif
if (state->numfree < PyDict_MAXFREELIST && Py_IS_TYPE(mp, &PyDict_Type)) {
#ifdef WITH_FREELISTS
struct _Py_dict_freelist *state = get_dict_state();
if (state->numfree < PyDict_MAXFREELIST && state->numfree >=0 &&
Py_IS_TYPE(mp, &PyDict_Type)) {
state->free_list[state->numfree++] = mp;
OBJECT_STAT_INC(to_freelist);
}
Expand Down
4 changes: 4 additions & 0 deletions Objects/floatobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2013,7 +2013,11 @@ _PyFloat_ClearFreeList(_PyFreeListState *freelist_state, int is_finalization)
void
_PyFloat_Fini(_PyFreeListState *state)
{
// With Py_GIL_DISABLED:
// the freelists for the current thread state have already been cleared.
#ifndef Py_GIL_DISABLED
_PyFloat_ClearFreeList(state, 1);
#endif
}

void
Expand Down
4 changes: 4 additions & 0 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1685,7 +1685,11 @@ _PyAsyncGen_ClearFreeLists(_PyFreeListState *freelist_state, int is_finalization
void
_PyAsyncGen_Fini(_PyFreeListState *state)
{
// With Py_GIL_DISABLED:
// the freelists for the current thread state have already been cleared.
#ifndef Py_GIL_DISABLED
_PyAsyncGen_ClearFreeLists(state, 1);
#endif
}


Expand Down
4 changes: 4 additions & 0 deletions Objects/listobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,11 @@ _PyList_ClearFreeList(_PyFreeListState *freelist_state, int is_finalization)
void
_PyList_Fini(_PyFreeListState *state)
{
// With Py_GIL_DISABLED:
// the freelists for the current thread state have already been cleared.
#ifndef Py_GIL_DISABLED
_PyList_ClearFreeList(state, 1);
#endif
}

/* Print summary info about the state of the optimized allocator */
Expand Down
4 changes: 4 additions & 0 deletions Python/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,11 @@ _PyContext_ClearFreeList(_PyFreeListState *freelist_state, int is_finalization)
void
_PyContext_Fini(_PyFreeListState *state)
{
// With Py_GIL_DISABLED:
// the freelists for the current thread state have already been cleared.
#ifndef Py_GIL_DISABLED
_PyContext_ClearFreeList(state, 1);
#endif
}


Expand Down
2 changes: 0 additions & 2 deletions Python/gc_free_threading.c
Original file line number Diff line number Diff line change
Expand Up @@ -1709,8 +1709,6 @@ PyUnstable_GC_VisitObjects(gcvisitobjects_t callback, void *arg)
void
_PyGC_ClearAllFreeLists(PyInterpreterState *interp)
{
_PyDict_ClearFreeList(interp);

HEAD_LOCK(&_PyRuntime);
_PyThreadStateImpl *tstate = (_PyThreadStateImpl *)interp->threads.head;
while (tstate != NULL) {
Expand Down
2 changes: 0 additions & 2 deletions Python/gc_gil.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
void
_PyGC_ClearAllFreeLists(PyInterpreterState *interp)
{
_PyDict_ClearFreeList(interp);

_Py_ClearFreeLists(&interp->freelist_state, 0);
}

Expand Down
3 changes: 3 additions & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -1462,9 +1462,12 @@ clear_datastack(PyThreadState *tstate)
void
_Py_ClearFreeLists(_PyFreeListState *state, int is_finalization)
{
// In the free-threaded build, freelists are per-PyThreadState and cleared in PyThreadState_Clear()
// In the default build, freelists are per-interpreter and cleared in finalize_interp_types()
_PyFloat_ClearFreeList(state, is_finalization);
_PyTuple_ClearFreeList(state, is_finalization);
_PyList_ClearFreeList(state, is_finalization);
_PyDict_ClearFreeList(state, is_finalization);
_PyContext_ClearFreeList(state, is_finalization);
_PyAsyncGen_ClearFreeLists(state, is_finalization);
_PyObjectStackChunk_ClearFreeList(state, is_finalization);
Expand Down
Loading