Skip to content

Commit

Permalink
bpo-45522: Allow to disable freelists on build time (GH-29056)
Browse files Browse the repository at this point in the history
Freelists for object structs can now be disabled. A new ``configure``
option ``--without-freelists`` can be used to disable all freelists
except empty tuple singleton. Internal Py*_MAXFREELIST macros can now
be defined as 0 without causing compiler warnings and segfaults.

Signed-off-by: Christian Heimes <christian@python.org>
  • Loading branch information
tiran authored Oct 21, 2021
1 parent 5a14f71 commit 9942f42
Show file tree
Hide file tree
Showing 15 changed files with 218 additions and 35 deletions.
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,11 @@ Build Changes
``isinf()``, ``isnan()``, ``round()``.
(Contributed by Victor Stinner in :issue:`45440`.)

* Freelists for object structs can now be disabled. A new :program:`configure`
option :option:`!--without-freelists` can be used to disable all freelists
except empty tuple singleton.
(Contributed by Christian Heimes in :issue:`45522`)

C API Changes
=============

Expand Down
37 changes: 37 additions & 0 deletions Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,31 @@ struct _Py_unicode_state {
struct _Py_unicode_ids ids;
};

#ifndef WITH_FREELISTS
// without freelists
# define PyFloat_MAXFREELIST 0
// for tuples only store empty tuple singleton
# define PyTuple_MAXSAVESIZE 1
# define PyTuple_MAXFREELIST 1
# define PyList_MAXFREELIST 0
# define PyDict_MAXFREELIST 0
# define PyFrame_MAXFREELIST 0
# define _PyAsyncGen_MAXFREELIST 0
# define PyContext_MAXFREELIST 0
#endif

#ifndef PyFloat_MAXFREELIST
# define PyFloat_MAXFREELIST 100
#endif

struct _Py_float_state {
#if PyFloat_MAXFREELIST > 0
/* Special free list
free_list is a singly-linked list of available PyFloatObjects,
linked via abuse of their ob_type members. */
int numfree;
PyFloatObject *free_list;
#endif
};

/* Speed optimization to avoid frequent malloc/free of small tuples */
Expand Down Expand Up @@ -119,33 +138,44 @@ struct _Py_tuple_state {
#endif

struct _Py_list_state {
#if PyList_MAXFREELIST > 0
PyListObject *free_list[PyList_MAXFREELIST];
int numfree;
#endif
};

#ifndef PyDict_MAXFREELIST
# define PyDict_MAXFREELIST 80
#endif

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

#ifndef PyFrame_MAXFREELIST
# define PyFrame_MAXFREELIST 200
#endif

struct _Py_frame_state {
#if PyFrame_MAXFREELIST > 0
PyFrameObject *free_list;
/* number of frames currently in free_list */
int numfree;
#endif
};

#ifndef _PyAsyncGen_MAXFREELIST
# define _PyAsyncGen_MAXFREELIST 80
#endif

struct _Py_async_gen_state {
#if _PyAsyncGen_MAXFREELIST > 0
/* Freelists boost performance 6-10%; they also reduce memory
fragmentation, as _PyAsyncGenWrappedValue and PyAsyncGenASend
are short-living objects that are instantiated for every
Expand All @@ -155,12 +185,19 @@ struct _Py_async_gen_state {

struct PyAsyncGenASend* asend_freelist[_PyAsyncGen_MAXFREELIST];
int asend_numfree;
#endif
};

#ifndef PyContext_MAXFREELIST
# define PyContext_MAXFREELIST 255
#endif

struct _Py_context_state {
#if PyContext_MAXFREELIST > 0
// List of free PyContext objects
PyContext *freelist;
int numfree;
#endif
};

struct _Py_exc_state {
Expand Down
13 changes: 12 additions & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,18 @@ def test_debugmallocstats(self):
from test.support.script_helper import assert_python_ok
args = ['-c', 'import sys; sys._debugmallocstats()']
ret, out, err = assert_python_ok(*args)
self.assertIn(b"free PyDictObjects", err)

# Output of sys._debugmallocstats() depends on configure flags.
# The sysconfig vars are not available on Windows.
if sys.platform != "win32":
with_freelists = sysconfig.get_config_var("WITH_FREELISTS")
with_pymalloc = sysconfig.get_config_var("WITH_PYMALLOC")
if with_freelists:
self.assertIn(b"free PyDictObjects", err)
if with_pymalloc:
self.assertIn(b'Small block threshold', err)
if not with_freelists and not with_pymalloc:
self.assertFalse(err)

# The function has no parameter
self.assertRaises(TypeError, sys._debugmallocstats, True)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The internal freelists for frame, float, list, dict, async generators, and
context objects can now be disabled.
27 changes: 23 additions & 4 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -240,17 +240,20 @@ uint64_t _pydict_global_version = 0;
#include "clinic/dictobject.c.h"


#if PyDict_MAXFREELIST > 0
static struct _Py_dict_state *
get_dict_state(void)
{
PyInterpreterState *interp = _PyInterpreterState_GET();
return &interp->dict_state;
}
#endif


void
_PyDict_ClearFreeList(PyInterpreterState *interp)
{
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = &interp->dict_state;
while (state->numfree) {
PyDictObject *op = state->free_list[--state->numfree];
Expand All @@ -260,14 +263,15 @@ _PyDict_ClearFreeList(PyInterpreterState *interp)
while (state->keys_numfree) {
PyObject_Free(state->keys_free_list[--state->keys_numfree]);
}
#endif
}


void
_PyDict_Fini(PyInterpreterState *interp)
{
_PyDict_ClearFreeList(interp);
#ifdef Py_DEBUG
#if defined(Py_DEBUG) && PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = &interp->dict_state;
state->numfree = -1;
state->keys_numfree = -1;
Expand All @@ -279,9 +283,11 @@ _PyDict_Fini(PyInterpreterState *interp)
void
_PyDict_DebugMallocStats(FILE *out)
{
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state();
_PyDebugAllocatorStats(out, "free PyDictObject",
state->numfree, sizeof(PyDictObject));
#endif
}

#define DK_MASK(dk) (DK_SIZE(dk)-1)
Expand Down Expand Up @@ -570,6 +576,7 @@ new_keys_object(uint8_t log2_size)
es = sizeof(Py_ssize_t);
}

#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state();
#ifdef Py_DEBUG
// new_keys_object() must not be called after _PyDict_Fini()
Expand All @@ -579,6 +586,7 @@ new_keys_object(uint8_t log2_size)
dk = state->keys_free_list[--state->keys_numfree];
}
else
#endif
{
dk = PyObject_Malloc(sizeof(PyDictKeysObject)
+ (es<<log2_size)
Expand Down Expand Up @@ -611,6 +619,7 @@ free_keys_object(PyDictKeysObject *keys)
Py_XDECREF(entries[i].me_key);
Py_XDECREF(entries[i].me_value);
}
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state();
#ifdef Py_DEBUG
// free_keys_object() must not be called after _PyDict_Fini()
Expand All @@ -620,6 +629,7 @@ free_keys_object(PyDictKeysObject *keys)
state->keys_free_list[state->keys_numfree++] = keys;
return;
}
#endif
PyObject_Free(keys);
}

Expand All @@ -638,6 +648,7 @@ new_dict(PyDictKeysObject *keys, PyDictValues *values, Py_ssize_t used, int free
{
PyDictObject *mp;
assert(keys != NULL);
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state();
#ifdef Py_DEBUG
// new_dict() must not be called after _PyDict_Fini()
Expand All @@ -649,7 +660,9 @@ new_dict(PyDictKeysObject *keys, PyDictValues *values, Py_ssize_t used, int free
assert (Py_IS_TYPE(mp, &PyDict_Type));
_Py_NewReference((PyObject *)mp);
}
else {
else
#endif
{
mp = PyObject_GC_New(PyDictObject, &PyDict_Type);
if (mp == NULL) {
dictkeys_decref(keys);
Expand Down Expand Up @@ -1259,6 +1272,7 @@ dictresize(PyDictObject *mp, uint8_t log2_newsize)
#ifdef Py_REF_DEBUG
_Py_RefTotal--;
#endif
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state();
#ifdef Py_DEBUG
// dictresize() must not be called after _PyDict_Fini()
Expand All @@ -1269,7 +1283,9 @@ dictresize(PyDictObject *mp, uint8_t log2_newsize)
{
state->keys_free_list[state->keys_numfree++] = oldkeys;
}
else {
else
#endif
{
PyObject_Free(oldkeys);
}
}
Expand Down Expand Up @@ -1987,6 +2003,7 @@ dict_dealloc(PyDictObject *mp)
assert(keys->dk_refcnt == 1);
dictkeys_decref(keys);
}
#if PyDict_MAXFREELIST > 0
struct _Py_dict_state *state = get_dict_state();
#ifdef Py_DEBUG
// new_dict() must not be called after _PyDict_Fini()
Expand All @@ -1995,7 +2012,9 @@ dict_dealloc(PyDictObject *mp)
if (state->numfree < PyDict_MAXFREELIST && Py_IS_TYPE(mp, &PyDict_Type)) {
state->free_list[state->numfree++] = mp;
}
else {
else
#endif
{
Py_TYPE(mp)->tp_free((PyObject *)mp);
}
Py_TRASHCAN_END
Expand Down
21 changes: 17 additions & 4 deletions Objects/floatobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ class float "PyObject *" "&PyFloat_Type"
#endif


#if PyFloat_MAXFREELIST > 0
static struct _Py_float_state *
get_float_state(void)
{
PyInterpreterState *interp = _PyInterpreterState_GET();
return &interp->float_state;
}
#endif


double
Expand Down Expand Up @@ -126,8 +128,10 @@ PyFloat_GetInfo(void)
PyObject *
PyFloat_FromDouble(double fval)
{
PyFloatObject *op;
#if PyFloat_MAXFREELIST > 0
struct _Py_float_state *state = get_float_state();
PyFloatObject *op = state->free_list;
op = state->free_list;
if (op != NULL) {
#ifdef Py_DEBUG
// PyFloat_FromDouble() must not be called after _PyFloat_Fini()
Expand All @@ -136,7 +140,9 @@ PyFloat_FromDouble(double fval)
state->free_list = (PyFloatObject *) Py_TYPE(op);
state->numfree--;
}
else {
else
#endif
{
op = PyObject_Malloc(sizeof(PyFloatObject));
if (!op) {
return PyErr_NoMemory();
Expand Down Expand Up @@ -233,6 +239,7 @@ PyFloat_FromString(PyObject *v)
static void
float_dealloc(PyFloatObject *op)
{
#if PyFloat_MAXFREELIST > 0
if (PyFloat_CheckExact(op)) {
struct _Py_float_state *state = get_float_state();
#ifdef Py_DEBUG
Expand All @@ -247,7 +254,9 @@ float_dealloc(PyFloatObject *op)
Py_SET_TYPE(op, (PyTypeObject *)state->free_list);
state->free_list = op;
}
else {
else
#endif
{
Py_TYPE(op)->tp_free((PyObject *)op);
}
}
Expand Down Expand Up @@ -2036,6 +2045,7 @@ _PyFloat_InitTypes(void)
void
_PyFloat_ClearFreeList(PyInterpreterState *interp)
{
#if PyFloat_MAXFREELIST > 0
struct _Py_float_state *state = &interp->float_state;
PyFloatObject *f = state->free_list;
while (f != NULL) {
Expand All @@ -2045,13 +2055,14 @@ _PyFloat_ClearFreeList(PyInterpreterState *interp)
}
state->free_list = NULL;
state->numfree = 0;
#endif
}

void
_PyFloat_Fini(PyInterpreterState *interp)
{
_PyFloat_ClearFreeList(interp);
#ifdef Py_DEBUG
#if defined(Py_DEBUG) && PyFloat_MAXFREELIST > 0
struct _Py_float_state *state = &interp->float_state;
state->numfree = -1;
#endif
Expand All @@ -2061,10 +2072,12 @@ _PyFloat_Fini(PyInterpreterState *interp)
void
_PyFloat_DebugMallocStats(FILE *out)
{
#if PyFloat_MAXFREELIST > 0
struct _Py_float_state *state = get_float_state();
_PyDebugAllocatorStats(out,
"free PyFloatObject",
state->numfree, sizeof(PyFloatObject));
#endif
}


Expand Down
Loading

0 comments on commit 9942f42

Please sign in to comment.