Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1eb8a0c

Browse files
committed
gh-112529: Implement GC for free-threaded builds
This implements a mark and sweep GC for the free-threaded builds of CPython. The implementation relies on mimalloc to find GC tracked objects (i.e., "containers").
1 parent 412920a commit 1eb8a0c

17 files changed

+1899
-20
lines changed

Include/internal/pycore_freelist.h

+10
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ extern "C" {
2020
# define PyFloat_MAXFREELIST 100
2121
# define PyContext_MAXFREELIST 255
2222
# define _PyAsyncGen_MAXFREELIST 80
23+
# define _PyObjectStackChunk_MAXFREELIST 4
2324
#else
2425
# define PyTuple_NFREELISTS 0
2526
# define PyTuple_MAXFREELIST 0
2627
# define PyList_MAXFREELIST 0
2728
# define PyFloat_MAXFREELIST 0
2829
# define PyContext_MAXFREELIST 0
2930
# define _PyAsyncGen_MAXFREELIST 0
31+
# define _PyObjectStackChunk_MAXFREELIST 0
3032
#endif
3133

3234
struct _Py_list_state {
@@ -93,13 +95,21 @@ struct _Py_async_gen_state {
9395
#endif
9496
};
9597

98+
struct _PyObjectStackChunk;
99+
100+
struct _Py_object_stack_state {
101+
struct _PyObjectStackChunk *free_list;
102+
Py_ssize_t numfree;
103+
};
104+
96105
typedef struct _Py_freelist_state {
97106
struct _Py_float_state float_state;
98107
struct _Py_tuple_state tuple_state;
99108
struct _Py_list_state list_state;
100109
struct _Py_slice_state slice_state;
101110
struct _Py_context_state context_state;
102111
struct _Py_async_gen_state async_gen_state;
112+
struct _Py_object_stack_state object_stack_state;
103113
} _PyFreeListState;
104114

105115
#ifdef __cplusplus

Include/internal/pycore_gc.h

+26-9
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,22 @@ static inline PyObject* _Py_FROM_GC(PyGC_Head *gc) {
3737
}
3838

3939

40+
/* Bit flags for ob_gc_bits (in Py_GIL_DISABLED builds) */
41+
#ifdef Py_GIL_DISABLED
42+
# define _PyGC_BITS_TRACKED (1)
43+
# define _PyGC_BITS_FINALIZED (2)
44+
# define _PyGC_BITS_UNREACHABLE (4)
45+
# define _PyGC_BITS_FROZEN (8)
46+
#endif
47+
4048
/* True if the object is currently tracked by the GC. */
4149
static inline int _PyObject_GC_IS_TRACKED(PyObject *op) {
50+
#ifdef Py_GIL_DISABLED
51+
return (op->ob_gc_bits & _PyGC_BITS_TRACKED) != 0;
52+
#else
4253
PyGC_Head *gc = _Py_AS_GC(op);
4354
return (gc->_gc_next != 0);
55+
#endif
4456
}
4557
#define _PyObject_GC_IS_TRACKED(op) _PyObject_GC_IS_TRACKED(_Py_CAST(PyObject*, op))
4658

@@ -107,24 +119,29 @@ static inline void _PyGCHead_SET_PREV(PyGC_Head *gc, PyGC_Head *prev) {
107119
gc->_gc_prev = ((gc->_gc_prev & ~_PyGC_PREV_MASK) | uprev);
108120
}
109121

110-
static inline int _PyGCHead_FINALIZED(PyGC_Head *gc) {
111-
return ((gc->_gc_prev & _PyGC_PREV_MASK_FINALIZED) != 0);
112-
}
113-
static inline void _PyGCHead_SET_FINALIZED(PyGC_Head *gc) {
114-
gc->_gc_prev |= _PyGC_PREV_MASK_FINALIZED;
115-
}
116-
117122
static inline int _PyGC_FINALIZED(PyObject *op) {
123+
#ifdef Py_GIL_DISABLED
124+
return (op->ob_gc_bits & _PyGC_BITS_FINALIZED) != 0;
125+
#else
118126
PyGC_Head *gc = _Py_AS_GC(op);
119-
return _PyGCHead_FINALIZED(gc);
127+
return ((gc->_gc_prev & _PyGC_PREV_MASK_FINALIZED) != 0);
128+
#endif
120129
}
121130
static inline void _PyGC_SET_FINALIZED(PyObject *op) {
131+
#ifdef Py_GIL_DISABLED
132+
op->ob_gc_bits |= _PyGC_BITS_FINALIZED;
133+
#else
122134
PyGC_Head *gc = _Py_AS_GC(op);
123-
_PyGCHead_SET_FINALIZED(gc);
135+
gc->_gc_prev |= _PyGC_PREV_MASK_FINALIZED;
136+
#endif
124137
}
125138
static inline void _PyGC_CLEAR_FINALIZED(PyObject *op) {
139+
#ifdef Py_GIL_DISABLED
140+
op->ob_gc_bits &= ~_PyGC_BITS_FINALIZED;
141+
#else
126142
PyGC_Head *gc = _Py_AS_GC(op);
127143
gc->_gc_prev &= ~_PyGC_PREV_MASK_FINALIZED;
144+
#endif
128145
}
129146

130147

Include/internal/pycore_object.h

+8
Original file line numberDiff line numberDiff line change
@@ -322,13 +322,17 @@ static inline void _PyObject_GC_TRACK(
322322
"object is in generation which is garbage collected",
323323
filename, lineno, __func__);
324324

325+
#ifdef Py_GIL_DISABLED
326+
op->ob_gc_bits |= _PyGC_BITS_TRACKED;
327+
#else
325328
PyInterpreterState *interp = _PyInterpreterState_GET();
326329
PyGC_Head *generation0 = interp->gc.generation0;
327330
PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev);
328331
_PyGCHead_SET_NEXT(last, gc);
329332
_PyGCHead_SET_PREV(gc, last);
330333
_PyGCHead_SET_NEXT(gc, generation0);
331334
generation0->_gc_prev = (uintptr_t)gc;
335+
#endif
332336
}
333337

334338
/* Tell the GC to stop tracking this object.
@@ -352,13 +356,17 @@ static inline void _PyObject_GC_UNTRACK(
352356
"object not tracked by the garbage collector",
353357
filename, lineno, __func__);
354358

359+
#ifdef Py_GIL_DISABLED
360+
op->ob_gc_bits &= ~_PyGC_BITS_TRACKED;
361+
#else
355362
PyGC_Head *gc = _Py_AS_GC(op);
356363
PyGC_Head *prev = _PyGCHead_PREV(gc);
357364
PyGC_Head *next = _PyGCHead_NEXT(gc);
358365
_PyGCHead_SET_NEXT(prev, next);
359366
_PyGCHead_SET_PREV(next, prev);
360367
gc->_gc_next = 0;
361368
gc->_gc_prev &= _PyGC_PREV_MASK_FINALIZED;
369+
#endif
362370
}
363371

364372
// Macros to accept any type for the parameter, and to automatically pass
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#ifndef Py_INTERNAL_OBJECT_STACK_H
2+
#define Py_INTERNAL_OBJECT_STACK_H
3+
4+
#ifdef __cplusplus
5+
extern "C" {
6+
#endif
7+
8+
#ifndef Py_BUILD_CORE
9+
# error "this header requires Py_BUILD_CORE define"
10+
#endif
11+
12+
// _PyObjectStack is a stack of Python objects implemented as a linked list of
13+
// fixed size buffers.
14+
15+
// Chosen so that _PyObjectStackChunk is a power-of-two size.
16+
#define _Py_OBJECT_STACK_CHUNK_SIZE 254
17+
18+
typedef struct _PyObjectStackChunk {
19+
struct _PyObjectStackChunk *prev;
20+
Py_ssize_t n;
21+
PyObject *objs[_Py_OBJECT_STACK_CHUNK_SIZE];
22+
} _PyObjectStackChunk;
23+
24+
typedef struct _PyObjectStack {
25+
_PyObjectStackChunk *head;
26+
} _PyObjectStack;
27+
28+
29+
extern _PyObjectStackChunk *
30+
_PyObjectStackChunk_New(void);
31+
32+
extern void
33+
_PyObjectStackChunk_Free(_PyObjectStackChunk *);
34+
35+
extern void
36+
_PyObjectStackChunk_ClearFreeList(_PyFreeListState *state, int is_finalization);
37+
38+
// Push an item onto the stack. Return -1 on allocation failure, 0 on success.
39+
static inline int
40+
_PyObjectStack_Push(_PyObjectStack *stack, PyObject *obj)
41+
{
42+
_PyObjectStackChunk *buf = stack->head;
43+
if (buf == NULL || buf->n == _Py_OBJECT_STACK_CHUNK_SIZE) {
44+
buf = _PyObjectStackChunk_New();
45+
if (buf == NULL) {
46+
return -1;
47+
}
48+
buf->prev = stack->head;
49+
buf->n = 0;
50+
stack->head = buf;
51+
}
52+
53+
assert(buf->n >= 0 && buf->n < _Py_OBJECT_STACK_CHUNK_SIZE);
54+
buf->objs[buf->n] = obj;
55+
buf->n++;
56+
return 0;
57+
}
58+
59+
// Pop the top item from the stack. Return NULL if the stack is empty.
60+
static inline PyObject *
61+
_PyObjectStack_Pop(_PyObjectStack *stack)
62+
{
63+
_PyObjectStackChunk *buf = stack->head;
64+
if (buf == NULL) {
65+
return NULL;
66+
}
67+
assert(buf->n > 0 && buf->n <= _Py_OBJECT_STACK_CHUNK_SIZE);
68+
buf->n--;
69+
PyObject *obj = buf->objs[buf->n];
70+
if (buf->n == 0) {
71+
stack->head = buf->prev;
72+
_PyObjectStackChunk_Free(buf);
73+
}
74+
return obj;
75+
}
76+
77+
// Remove all items from the stack
78+
extern void
79+
_PyObjectStack_Clear(_PyObjectStack *stack);
80+
81+
#ifdef __cplusplus
82+
}
83+
#endif
84+
#endif // !Py_INTERNAL_OBJECT_STACK_H

Lib/gzip.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ def closed(self):
349349

350350
def close(self):
351351
fileobj = self.fileobj
352-
if fileobj is None:
352+
if fileobj is None or self._buffer.closed:
353353
return
354354
try:
355355
if self.mode == WRITE:

Lib/test/test_gc.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import unittest
22
import unittest.mock
33
from test.support import (verbose, refcount_test,
4-
cpython_only, requires_subprocess)
4+
cpython_only, requires_subprocess, Py_GIL_DISABLED)
55
from test.support.import_helper import import_module
66
from test.support.os_helper import temp_dir, TESTFN, unlink
77
from test.support.script_helper import assert_python_ok, make_script
@@ -815,6 +815,15 @@ def test_freeze(self):
815815
self.assertEqual(gc.get_freeze_count(), 0)
816816

817817
def test_get_objects(self):
818+
gc.collect()
819+
l = []
820+
l.append(l)
821+
self.assertTrue(
822+
any(l is element for element in gc.get_objects())
823+
)
824+
825+
@unittest.skipIf(Py_GIL_DISABLED, 'need generational GC')
826+
def test_get_objects_generations(self):
818827
gc.collect()
819828
l = []
820829
l.append(l)
@@ -1225,7 +1234,7 @@ def test_refcount_errors(self):
12251234
p.stderr.close()
12261235
# Verify that stderr has a useful error message:
12271236
self.assertRegex(stderr,
1228-
br'gc\.c:[0-9]+: gc_decref: Assertion "gc_get_refs\(g\) > 0" failed.')
1237+
br'gc.*\.c:[0-9]+: .*: Assertion "gc_get_refs\(.+\) .*" failed.')
12291238
self.assertRegex(stderr,
12301239
br'refcount is too small')
12311240
# "address : 0x7fb5062efc18"

Lib/test/test_io.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -3652,10 +3652,8 @@ def _check_create_at_shutdown(self, **kwargs):
36523652
codecs.lookup('utf-8')
36533653
36543654
class C:
3655-
def __init__(self):
3656-
self.buf = io.BytesIO()
36573655
def __del__(self):
3658-
io.TextIOWrapper(self.buf, **{kwargs})
3656+
io.TextIOWrapper(io.BytesIO(), **{kwargs})
36593657
print("ok")
36603658
c = C()
36613659
""".format(iomod=iomod, kwargs=kwargs)

Makefile.pre.in

+3
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ PYTHON_OBJS= \
439439
Python/modsupport.o \
440440
Python/mysnprintf.o \
441441
Python/mystrtoul.o \
442+
Python/object_stack.o \
442443
Python/optimizer.o \
443444
Python/optimizer_analysis.o \
444445
Python/parking_lot.o \
@@ -1832,6 +1833,7 @@ PYTHON_HEADERS= \
18321833
$(srcdir)/Include/internal/pycore_frame.h \
18331834
$(srcdir)/Include/internal/pycore_freelist.h \
18341835
$(srcdir)/Include/internal/pycore_function.h \
1836+
$(srcdir)/Include/internal/pycore_gc.h \
18351837
$(srcdir)/Include/internal/pycore_genobject.h \
18361838
$(srcdir)/Include/internal/pycore_getopt.h \
18371839
$(srcdir)/Include/internal/pycore_gil.h \
@@ -1853,6 +1855,7 @@ PYTHON_HEADERS= \
18531855
$(srcdir)/Include/internal/pycore_namespace.h \
18541856
$(srcdir)/Include/internal/pycore_object.h \
18551857
$(srcdir)/Include/internal/pycore_object_alloc.h \
1858+
$(srcdir)/Include/internal/pycore_object_stack.h \
18561859
$(srcdir)/Include/internal/pycore_object_state.h \
18571860
$(srcdir)/Include/internal/pycore_obmalloc.h \
18581861
$(srcdir)/Include/internal/pycore_obmalloc_init.h \
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The free-threaded build now has its own thread-safe GC implementation that
2+
uses mimalloc to find GC tracked objects. It is non-generational, unlike the
3+
existing GC implementation.

PCbuild/_freeze_module.vcxproj

+1
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
<ClCompile Include="..\Python\modsupport.c" />
231231
<ClCompile Include="..\Python\mysnprintf.c" />
232232
<ClCompile Include="..\Python\mystrtoul.c" />
233+
<ClCompile Include="..\Python\object_stack.c" />
233234
<ClCompile Include="..\Python\optimizer.c" />
234235
<ClCompile Include="..\Python\optimizer_analysis.c" />
235236
<ClCompile Include="..\Python\parking_lot.c" />

PCbuild/_freeze_module.vcxproj.filters

+3
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,9 @@
289289
<ClCompile Include="..\Objects\object.c">
290290
<Filter>Source Files</Filter>
291291
</ClCompile>
292+
<ClCompile Include="..\Python\object_stack.c">
293+
<Filter>Source Files</Filter>
294+
</ClCompile>
292295
<ClCompile Include="..\Objects\obmalloc.c">
293296
<Filter>Source Files</Filter>
294297
</ClCompile>

PCbuild/pythoncore.vcxproj

+1
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@
591591
<ClCompile Include="..\Python\modsupport.c" />
592592
<ClCompile Include="..\Python\mysnprintf.c" />
593593
<ClCompile Include="..\Python\mystrtoul.c" />
594+
<ClCompile Include="..\Python\object_stack.c" />
594595
<ClCompile Include="..\Python\optimizer.c" />
595596
<ClCompile Include="..\Python\optimizer_analysis.c" />
596597
<ClCompile Include="..\Python\parking_lot.c" />

PCbuild/pythoncore.vcxproj.filters

+3
Original file line numberDiff line numberDiff line change
@@ -1355,6 +1355,9 @@
13551355
<ClCompile Include="..\Python\mystrtoul.c">
13561356
<Filter>Python</Filter>
13571357
</ClCompile>
1358+
<ClCompile Include="..\Python\object_stack.c">
1359+
<Filter>Python</Filter>
1360+
</ClCompile>
13581361
<ClCompile Include="..\Python\optimizer.c">
13591362
<Filter>Python</Filter>
13601363
</ClCompile>

Python/gc.c

+6-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#include "pycore_weakref.h" // _PyWeakref_ClearRef()
1616
#include "pydtrace.h"
1717

18+
#ifndef Py_GIL_DISABLED
19+
1820
typedef struct _gc_runtime_state GCState;
1921

2022
#ifdef Py_DEBUG
@@ -964,10 +966,10 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable)
964966
PyGC_Head *gc = GC_NEXT(collectable);
965967
PyObject *op = FROM_GC(gc);
966968
gc_list_move(gc, &seen);
967-
if (!_PyGCHead_FINALIZED(gc) &&
969+
if (!_PyGC_FINALIZED(op) &&
968970
(finalize = Py_TYPE(op)->tp_finalize) != NULL)
969971
{
970-
_PyGCHead_SET_FINALIZED(gc);
972+
_PyGC_SET_FINALIZED(op);
971973
Py_INCREF(op);
972974
finalize(op);
973975
assert(!_PyErr_Occurred(tstate));
@@ -1942,3 +1944,5 @@ PyUnstable_GC_VisitObjects(gcvisitobjects_t callback, void *arg)
19421944
done:
19431945
gcstate->enabled = origenstate;
19441946
}
1947+
1948+
#endif // Py_GIL_DISABLED

0 commit comments

Comments
 (0)