Skip to content

Commit

Permalink
gc: Traverese mimalloc heaps to find all objects.
Browse files Browse the repository at this point in the history
The GC now uses separate mimalloc heaps for GC and non-GC objects and
only maintains gc lists during garbage collection.

 - PyGC_Head._gc_prev is now the first word in the object. This ensures
   that a deallocated memory block does not look like a tracked object.
   The first word is used for _gc_prev when allocated and for the
   free-list when not allocated. The free-list never has the
   least-significant bit (_PyGC_PREV_MASK_TRACKED) set because
   objects are naturally aligned.
  • Loading branch information
colesbury committed Apr 23, 2023
1 parent 654be8f commit 967fe31
Show file tree
Hide file tree
Showing 14 changed files with 652 additions and 332 deletions.
2 changes: 1 addition & 1 deletion Doc/c-api/memory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ Customize Memory Allocators
Get the memory block allocator of the specified domain.
.. c:function:: void PyMem_SetAllocator(PyMemAllocatorDomain domain, PyMemAllocatorEx *allocator)
.. c:function:: void PyMem_SetAllocator(PyMemAllocatorDomain domain, const PyMemAllocatorEx *allocator)
Set the memory block allocator of the specified domain.
Expand Down
40 changes: 21 additions & 19 deletions Include/internal/pycore_gc.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,43 @@ extern "C" {

/* GC information is stored BEFORE the object structure. */
typedef struct {
// Pointer to previous object in the list.
// Lowest three bits are used for flags documented later.
uintptr_t _gc_prev;

// Pointer to next object in the list.
// 0 means the object is not tracked
uintptr_t _gc_next;

// Pointer to previous object in the list.
// Lowest two bits are used for flags documented later.
uintptr_t _gc_prev;
} PyGC_Head;

typedef struct {
PyGC_Head _gc_head;
PyObject *_dict_or_values;
PyObject *_weakref;
} _PyGC_Preheader_UNUSED;
#define _PyGC_Head_UNUSED _PyGC_Preheader_UNUSED

#define PyGC_Head_OFFSET (((Py_ssize_t)sizeof(PyObject *))*-4)

/* Bit 0 is set if the object is tracked by the GC */
#define _PyGC_PREV_MASK_TRACKED (1)
/* Bit 1 is set when tp_finalize is called */
#define _PyGC_PREV_MASK_FINALIZED (2)
/* Bit 2 is set when the object is not currently reachable */
#define _PyGC_PREV_MASK_UNREACHABLE (4)
/* The (N-3) most significant bits contain the real address. */
#define _PyGC_PREV_SHIFT (3)
#define _PyGC_PREV_MASK (((uintptr_t) -1) << _PyGC_PREV_SHIFT)

static inline PyGC_Head* _Py_AS_GC(PyObject *op) {
char *mem = _Py_STATIC_CAST(char*, op);
return _Py_STATIC_CAST(PyGC_Head*, mem + PyGC_Head_OFFSET);
}
#define _PyGC_Head_UNUSED _PyGC_Preheader_UNUSED

/* True if the object is currently tracked by the GC. */
static inline int _PyObject_GC_IS_TRACKED(PyObject *op) {
PyGC_Head *gc = _Py_AS_GC(op);
return (gc->_gc_next != 0);
return (gc->_gc_prev & _PyGC_PREV_MASK_TRACKED) != 0;
}
#define _PyObject_GC_IS_TRACKED(op) _PyObject_GC_IS_TRACKED(_Py_CAST(PyObject*, op))

Expand All @@ -54,16 +64,6 @@ static inline int _PyObject_GC_MAY_BE_TRACKED(PyObject *obj) {
return 1;
}


/* Bit flags for _gc_prev */
/* Bit 0 is set when tp_finalize is called */
#define _PyGC_PREV_MASK_FINALIZED (1)
/* Bit 1 is set when the object is in generation which is GCed currently. */
#define _PyGC_PREV_MASK_COLLECTING (2)
/* The (N-2) most significant bits contain the real address. */
#define _PyGC_PREV_SHIFT (2)
#define _PyGC_PREV_MASK (((uintptr_t) -1) << _PyGC_PREV_SHIFT)

// Lowest bit of _gc_next is used for flags only in GC.
// But it is always 0 for normal code.
static inline PyGC_Head* _PyGCHead_NEXT(PyGC_Head *gc) {
Expand Down Expand Up @@ -175,8 +175,6 @@ struct _gc_runtime_state {
/* Is automatic collection enabled? */
int enabled;
int debug;
/* linked lists of container objects */
PyGC_Head head;
/* a permanent generation which won't be collected */
struct gc_generation_stats stats;
/* true if we are currently running the collector */
Expand Down Expand Up @@ -211,12 +209,16 @@ struct _gc_runtime_state {
extern void _PyGC_InitState(struct _gc_runtime_state *);

extern Py_ssize_t _PyGC_CollectNoFail(PyThreadState *tstate);
extern void _PyGC_ResetHeap(void);

static inline int
_PyGC_ShouldCollect(struct _gc_runtime_state *gcstate)
{
Py_ssize_t live = _Py_atomic_load_ssize_relaxed(&gcstate->gc_live);
return live >= gcstate->gc_threshold && gcstate->enabled && gcstate->gc_threshold && !gcstate->collecting;
return (live >= gcstate->gc_threshold &&
gcstate->enabled &&
gcstate->gc_threshold &&
!gcstate->collecting);
}

// Functions to clear types free lists
Expand Down
28 changes: 11 additions & 17 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,7 @@ static inline void _PyObject_GC_TRACK(
filename, lineno, __func__);

PyGC_Head *gc = _Py_AS_GC(op);
_PyObject_ASSERT_FROM(op,
(gc->_gc_prev & _PyGC_PREV_MASK_COLLECTING) == 0,
"object is in generation which is garbage collected",
filename, lineno, __func__);

PyInterpreterState *interp = _PyInterpreterState_GET();
PyGC_Head *head = &interp->gc.head;
PyGC_Head *last = (PyGC_Head*)(head->_gc_prev);
_PyGCHead_SET_NEXT(last, gc);
_PyGCHead_SET_PREV(gc, last);
_PyGCHead_SET_NEXT(gc, head);
head->_gc_prev = (uintptr_t)gc;
gc->_gc_prev |= _PyGC_PREV_MASK_TRACKED;
}

/* Tell the GC to stop tracking this object.
Expand All @@ -176,11 +165,16 @@ static inline void _PyObject_GC_UNTRACK(
filename, lineno, __func__);

PyGC_Head *gc = _Py_AS_GC(op);
PyGC_Head *prev = _PyGCHead_PREV(gc);
PyGC_Head *next = _PyGCHead_NEXT(gc);
_PyGCHead_SET_NEXT(prev, next);
_PyGCHead_SET_PREV(next, prev);
gc->_gc_next = 0;
if (gc->_gc_next != 0) {
PyGC_Head *prev = _PyGCHead_PREV(gc);
PyGC_Head *next = _PyGCHead_NEXT(gc);

_PyGCHead_SET_NEXT(prev, next);
_PyGCHead_SET_PREV(next, prev);

gc->_gc_next = 0;
}

gc->_gc_prev &= _PyGC_PREV_MASK_FINALIZED;
}

Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ PyAPI_FUNC(void) _PyThreadState_Init(
PyAPI_FUNC(void) _PyThreadState_DeleteExcept(
_PyRuntimeState *runtime,
PyThreadState *tstate);
PyAPI_FUNC(PyThreadState *) _PyThreadState_UnlinkExcept(
_PyRuntimeState *runtime,
PyThreadState *tstate,
int already_dead);
PyAPI_FUNC(void) _PyThreadState_DeleteGarbage(PyThreadState *garbage);

static inline void
_PyThreadState_Signal(PyThreadState *tstate, uintptr_t bit)
Expand Down
7 changes: 6 additions & 1 deletion Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1658,7 +1658,12 @@ class C(): pass
# Issue #30817: Abort in PyErr_PrintEx() when no memory.
# Span a large range of tests as the CPython code always evolves with
# changes that add or remove memory allocations.
for i in range(1, 20):
#
# TODO(sgross): this test is flaky with the allocator changes. If the
# memory error happens during GC (such as from Py_FinalizeEx), it may
# fail with an assertion error because the list gc.garbage can't be
# created.
for i in range(1, 15):
rc, out, err = script_helper.assert_python_failure("-c", code % i)
self.assertIn(rc, (1, 120))
self.assertIn(b'MemoryError', err)
Expand Down
1 change: 1 addition & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ static PyObject *
raiseTestError(const char* test_name, const char* msg)
{
PyErr_Format(TestError, "%s: %s", test_name, msg);

return NULL;
}

Expand Down
Loading

0 comments on commit 967fe31

Please sign in to comment.