Skip to content

Commit fd7aab5

Browse files
committed
gh-159137: Traverse dictionary values as spans in the free-threaded GC
1 parent 8d11920 commit fd7aab5

4 files changed

Lines changed: 80 additions & 37 deletions

File tree

‎Include/internal/pycore_dict.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ typedef struct {
9494
PyObject *me_value; /* This field is only meaningful for combined tables */
9595
} PyDictUnicodeEntry;
9696

97+
extern int _PyDict_Traverse(PyObject *, visitproc, void *);
98+
9799
extern PyDictKeysObject *_PyDict_NewKeysForClass(PyHeapTypeObject *);
98100
extern void _PyDict_RemoveKeysForClass(PyHeapTypeObject *);
99101
extern PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Reduce dictionary traversal overhead in the free-threaded garbage collector
2+
with spans of dictionary values.

‎Objects/dictobject.c‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5089,8 +5089,8 @@ dict_popitem_impl(PyDictObject *self)
50895089
return res;
50905090
}
50915091

5092-
static int
5093-
dict_traverse(PyObject *op, visitproc visit, void *arg)
5092+
int
5093+
_PyDict_Traverse(PyObject *op, visitproc visit, void *arg)
50945094
{
50955095
PyDictObject *mp = (PyDictObject *)op;
50965096
PyDictKeysObject *keys = mp->ma_keys;
@@ -5522,7 +5522,7 @@ PyTypeObject PyDict_Type = {
55225522
Py_TPFLAGS_BASETYPE | Py_TPFLAGS_DICT_SUBCLASS |
55235523
_Py_TPFLAGS_MATCH_SELF | Py_TPFLAGS_MAPPING, /* tp_flags */
55245524
dictionary_doc, /* tp_doc */
5525-
dict_traverse, /* tp_traverse */
5525+
_PyDict_Traverse, /* tp_traverse */
55265526
dict_tp_clear, /* tp_clear */
55275527
dict_richcompare, /* tp_richcompare */
55285528
0, /* tp_weaklistoffset */
@@ -8632,7 +8632,7 @@ PyTypeObject PyFrozenDict_Type = {
86328632
| Py_TPFLAGS_BASETYPE
86338633
| _Py_TPFLAGS_MATCH_SELF | Py_TPFLAGS_MAPPING,
86348634
.tp_doc = frozendict_doc,
8635-
.tp_traverse = dict_traverse,
8635+
.tp_traverse = _PyDict_Traverse,
86368636
.tp_clear = dict_tp_clear,
86378637
.tp_richcompare = dict_richcompare,
86388638
.tp_iter = dict_iter,

‎Python/gc_free_threading.c‎

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -578,10 +578,11 @@ static_assert(BUFFER_HI < BUFFER_SIZE &&
578578
#define prefetch(ptr)
579579
#endif
580580

581-
// a contigous sequence of PyObject pointers, can contain NULLs
581+
// A strided sequence of PyObject pointers, which can contain NULLs.
582582
typedef struct {
583583
PyObject **start;
584-
PyObject **end;
584+
Py_ssize_t size;
585+
Py_ssize_t stride;
585586
} gc_span_t;
586587

587588
typedef struct {
@@ -660,9 +661,10 @@ gc_mark_stack_push(_PyObjectStack *ms, PyObject *op)
660661
}
661662

662663
static int
663-
gc_mark_span_push(gc_span_stack_t *ss, PyObject **start, PyObject **end)
664+
gc_mark_span_push(gc_span_stack_t *ss, PyObject **start, Py_ssize_t size,
665+
Py_ssize_t stride)
664666
{
665-
if (start == end) {
667+
if (size == 0) {
666668
return 0;
667669
}
668670
if (ss->size >= ss->capacity) {
@@ -678,9 +680,10 @@ gc_mark_span_push(gc_span_stack_t *ss, PyObject **start, PyObject **end)
678680
}
679681
ss->stack = new_stack;
680682
}
681-
assert(end > start);
683+
assert(size > 0);
682684
ss->stack[ss->size].start = start;
683-
ss->stack[ss->size].end = end;
685+
ss->stack[ss->size].size = size;
686+
ss->stack[ss->size].stride = stride;
684687
ss->size++;
685688
return 0;
686689
}
@@ -748,20 +751,21 @@ gc_mark_enqueue(PyObject *op, gc_mark_args_t *args)
748751
}
749752
}
750753

751-
// Called when we have a contigous sequence of PyObject pointers, either
752-
// a tuple or list object. This will add the items to the buffer if there
754+
// Called for a strided sequence of PyObject pointers, including list and
755+
// tuple items and dictionary values. This will add the items to the buffer if there
753756
// is space for them all otherwise push a new "span" on the span stack. Using
754757
// spans has the advantage of not creating a deep _PyObjectStack stack when
755758
// dealing with long sequences. Those sequences will be processed in smaller
756759
// chunks by the gc_prime_from_spans() function.
757760
static int
758-
gc_mark_enqueue_span(PyObject **item, Py_ssize_t size, gc_mark_args_t *args)
761+
gc_mark_enqueue_span_stride(PyObject **item, Py_ssize_t size,
762+
Py_ssize_t stride, gc_mark_args_t *args)
759763
{
760764
Py_ssize_t used = gc_mark_buffer_len(args);
761765
Py_ssize_t free = BUFFER_SIZE - used;
762766
if (free >= size) {
763767
for (Py_ssize_t i = 0; i < size; i++) {
764-
PyObject *op = item[i];
768+
PyObject *op = *(PyObject **)((char *)item + i * stride);
765769
if (op == NULL) {
766770
continue;
767771
}
@@ -770,14 +774,39 @@ gc_mark_enqueue_span(PyObject **item, Py_ssize_t size, gc_mark_args_t *args)
770774
}
771775
else {
772776
assert(size > 0);
773-
PyObject **end = &item[size];
774-
if (gc_mark_span_push(&args->spans, item, end) < 0) {
777+
if (gc_mark_span_push(&args->spans, item, size, stride) < 0) {
775778
return -1;
776779
}
777780
}
778781
return 0;
779782
}
780783

784+
static int
785+
gc_mark_enqueue_span(PyObject **item, Py_ssize_t size, gc_mark_args_t *args)
786+
{
787+
return gc_mark_enqueue_span_stride(item, size, sizeof(PyObject *), args);
788+
}
789+
790+
static int
791+
gc_mark_traverse_dict(PyObject *self, gc_mark_args_t *args)
792+
{
793+
PyDictObject *dict = (PyDictObject *)self;
794+
PyDictKeysObject *keys = dict->ma_keys;
795+
if (keys->dk_nentries == 0) {
796+
return 0;
797+
}
798+
if (!DK_IS_UNICODE(keys)) {
799+
return _PyDict_Traverse(self, gc_mark_enqueue_buffer_visitproc, args);
800+
}
801+
if (_PyDict_HasSplitTable(dict)) {
802+
return gc_mark_enqueue_span(dict->ma_values->values,
803+
keys->dk_nentries, args);
804+
}
805+
PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys);
806+
return gc_mark_enqueue_span_stride(&entries[0].me_value, keys->dk_nentries,
807+
sizeof(PyDictUnicodeEntry), args);
808+
}
809+
781810
static bool
782811
gc_clear_alive_bits(const mi_heap_t *heap, const mi_heap_area_t *area,
783812
void *block, size_t block_size, void *args)
@@ -1250,44 +1279,49 @@ gc_prime_from_spans(gc_mark_args_t *args)
12501279
assert(space <= BUFFER_HI);
12511280
gc_span_t entry = args->spans.stack[--args->spans.size];
12521281
// spans on the stack should always have one or more elements
1253-
assert(entry.start < entry.end);
1282+
assert(entry.size > 0);
12541283
do {
12551284
PyObject *op = *entry.start;
1256-
entry.start++;
1285+
entry.size--;
1286+
if (entry.size > 0) {
1287+
entry.start = (PyObject **)((char *)entry.start + entry.stride);
1288+
}
12571289
if (op != NULL) {
12581290
gc_mark_buffer_push(op, args);
12591291
space--;
12601292
if (space == 0) {
12611293
// buffer is as full as we want and not done with span
1262-
gc_mark_span_push(&args->spans, entry.start, entry.end);
1294+
gc_mark_span_push(&args->spans, entry.start, entry.size, entry.stride);
12631295
return;
12641296
}
12651297
}
1266-
} while (entry.start < entry.end);
1298+
} while (entry.size > 0);
12671299
}
12681300

12691301
static void
12701302
gc_prime_buffer(gc_mark_args_t *args)
12711303
{
1272-
if (args->spans.size > 0) {
1304+
// Dictionary spans can contain only deleted entries.
1305+
while (args->spans.size > 0) {
12731306
gc_prime_from_spans(args);
1307+
if (!gc_mark_buffer_is_empty(args)) {
1308+
return;
1309+
}
12741310
}
1275-
else {
1276-
// When priming, don't fill the buffer too full since that would
1277-
// likely cause the stack to be used shortly after when it
1278-
// fills. We want to use the buffer as much as possible and so
1279-
// we only fill to BUFFER_HI, not BUFFER_SIZE.
1280-
Py_ssize_t space = BUFFER_HI - gc_mark_buffer_len(args);
1281-
assert(space > 0);
1282-
do {
1283-
PyObject *op = _PyObjectStack_Pop(&args->stack);
1284-
if (op == NULL) {
1285-
return;
1286-
}
1287-
gc_mark_buffer_push(op, args);
1288-
space--;
1289-
} while (space > 0);
1290-
}
1311+
// When priming, don't fill the buffer too full since that would
1312+
// likely cause the stack to be used shortly after when it
1313+
// fills. We want to use the buffer as much as possible and so
1314+
// we only fill to BUFFER_HI, not BUFFER_SIZE.
1315+
Py_ssize_t space = BUFFER_HI - gc_mark_buffer_len(args);
1316+
assert(space > 0);
1317+
do {
1318+
PyObject *op = _PyObjectStack_Pop(&args->stack);
1319+
if (op == NULL) {
1320+
return;
1321+
}
1322+
gc_mark_buffer_push(op, args);
1323+
space--;
1324+
} while (space > 0);
12911325
}
12921326

12931327
static int
@@ -1331,6 +1365,11 @@ gc_propagate_alive_prefetch(gc_mark_args_t *args)
13311365
return -1;
13321366
}
13331367
}
1368+
else if (traverse == _PyDict_Traverse) {
1369+
if (gc_mark_traverse_dict(op, args) < 0) {
1370+
return -1;
1371+
}
1372+
}
13341373
else if (traverse(op, gc_mark_enqueue_buffer_visitproc, args) < 0) {
13351374
return -1;
13361375
}

0 commit comments

Comments
 (0)