Skip to content

GH-126491: Lower heap size limit with faster marking #127519

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

Merged
merged 24 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3038a78
Faster marking of reachable objects
markshannon Nov 9, 2024
c024484
Handle more classes in fast marking
markshannon Nov 10, 2024
e8497ae
Add support for asyn generators on fast path. Simplify counting
markshannon Nov 11, 2024
4c1a6bc
Check stackref before converting to PyObject *
markshannon Nov 11, 2024
6efb4c0
Rename stuff
markshannon Nov 13, 2024
b1c7ab0
Remove expand_region_transitively_reachable and use move_all_transiti…
markshannon Nov 13, 2024
07f228b
Merge branch 'main' into faster-marking
markshannon Dec 2, 2024
51ff78e
Fix compiler warnings and linkage
markshannon Dec 2, 2024
df907b5
Fix another linkage issue
markshannon Dec 2, 2024
9ca64f5
Try 'extern'
markshannon Dec 2, 2024
bda13f4
Go back to PyAPI_FUNC and move functions together
markshannon Dec 2, 2024
d9d63c8
Use _Py_FALLTHROUGH
markshannon Dec 2, 2024
57b8820
Add necessary #ifndef Py_GIL_DISABLED
markshannon Dec 2, 2024
a607059
Go back to using tp_traverse, but make traversal more efficient
markshannon Dec 3, 2024
1545508
Tidy up
markshannon Dec 3, 2024
a1a38c8
A bit more tidying up
markshannon Dec 3, 2024
68fc90b
Move all work to do calculations to one place
markshannon Dec 3, 2024
8893cf5
Assume that increments are 50% garbage for work done calculation
markshannon Dec 3, 2024
ba20c7c
Elaborate comment
markshannon Dec 4, 2024
8262bf0
More tweaking of thresholds
markshannon Dec 4, 2024
3c2116e
Do some algebra
markshannon Dec 4, 2024
72d0284
Revert to 2M+I from 3M+I
markshannon Dec 4, 2024
0f182e2
Address review comments
markshannon Dec 5, 2024
d3c21bb
Address review comments and clarify code a bit
markshannon Dec 5, 2024
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
Prev Previous commit
Next Next commit
Assume that increments are 50% garbage for work done calculation
  • Loading branch information
markshannon committed Dec 3, 2024
commit 8893cf53bec4645245f11707bc0b11704cb40494
14 changes: 3 additions & 11 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1161,27 +1161,19 @@ def make_ll(depth):
return head

head = make_ll(1000)
count = 1000

# There will be some objects we aren't counting,
# e.g. the gc stats dicts. This test checks
# that the counts don't grow, so we try to
# correct for the uncounted objects
# This is just an estimate.
CORRECTION = 20

enabled = gc.isenabled()
gc.enable()
olds = []
initial_heap_size = _testinternalcapi.get_tracked_heap_size()
for i in range(20_000):
iterations = max(20_000, initial_heap_size)
for i in range(iterations):
newhead = make_ll(20)
count += 20
newhead.surprise = head
olds.append(newhead)
if len(olds) == 20:
new_objects = _testinternalcapi.get_tracked_heap_size() - initial_heap_size
self.assertLess(new_objects, 27_000, f"Heap growing. Reached limit after {i} iterations")
self.assertLess(new_objects, initial_heap_size/2, f"Heap growing. Reached limit after {i} iterations")
del olds[:]
if not enabled:
gc.disable()
Expand Down
2 changes: 1 addition & 1 deletion Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -4156,7 +4156,7 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type)
assert((type->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0);
type->tp_flags |= Py_TPFLAGS_MANAGED_DICT;
type->tp_dictoffset = -1;
if (type->tp_basicsize == sizeof(PyObject *)) {
if (type->tp_basicsize == sizeof(PyObject)) {
type->tp_traverse = plain_object_traverse;
}
}
Expand Down
46 changes: 23 additions & 23 deletions Python/gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1277,17 +1277,12 @@ gc_list_set_space(PyGC_Head *list, int space)
* space faster than objects are added to the old space.
*
* Each young or incremental collection adds a number of
* objects, S (for survivors) to the old space, and
* incremental collectors scan I objects from the old space.
* I > S must be true. We also want I > S * N to be where
* N > 1. Higher values of N mean that the old space is
* new objects (N) to the heap, and incremental collectors
* scan I objects from the old space.
* I > N must be true. We also want I > N * K to be where
* K > 2. Higher values of K mean that the old space is
* scanned more rapidly.
* The default incremental threshold of 10 translates to
* N == 1.4 (1 + 4/threshold)
*/

/* Divide by 10, so that the default incremental threshold of 10
* scans objects at 1% of the heap size */
#define SCAN_RATE_DIVISOR 10

static void
Expand Down Expand Up @@ -1335,6 +1330,7 @@ typedef struct work_stack {
int visited_space;
} WorkStack;

/* Remove gc from the list it is currently in and push it to the stack */
static inline void
push_to_stack(PyGC_Head *gc, WorkStack *stack)
{
Expand All @@ -1346,6 +1342,14 @@ push_to_stack(PyGC_Head *gc, WorkStack *stack)
stack->top = gc;
}

static inline PyGC_Head *
pop_from_stack(WorkStack *stack)
{
PyGC_Head *gc = stack->top;
stack->top = _PyGCHead_PREV(gc);
return gc;
}

/* append list `from` to `stack`; `from` becomes an empty list */
static void
move_list_to_stack(PyGC_Head *from, WorkStack *stack)
Expand Down Expand Up @@ -1418,7 +1422,6 @@ completed_scavenge(GCState *gcstate)
gc_list_set_space(&gcstate->old[not_visited].head, not_visited);
}
assert(gc_list_is_empty(&gcstate->old[visited].head));
gcstate->work_to_do = 0;
gcstate->phase = GC_PHASE_MARK;
}

Expand Down Expand Up @@ -1450,9 +1453,7 @@ move_all_transitively_reachable(WorkStack *stack, PyGC_Head *visited, int visite
// Transitively traverse all objects from reachable, until empty
Py_ssize_t objects_marked = 0;
while (stack->top != NULL) {
/* Pop from the stack, to do depth first traversal */
PyGC_Head *gc = stack->top;
stack->top = _PyGCHead_PREV(gc);
PyGC_Head *gc = pop_from_stack(stack);
assert(gc_old_space(gc) == visited_space);
gc_list_append(gc, visited);
objects_marked++;
Expand Down Expand Up @@ -1513,7 +1514,6 @@ mark_global_roots(PyInterpreterState *interp, PyGC_Head *visited, int visited_sp
WorkStack stack;
stack.top = NULL;
stack.visited_space = visited_space;
Py_ssize_t objects_marked = 0;
MOVE_UNVISITED(interp->sysdict, &stack, visited_space);
MOVE_UNVISITED(interp->builtins, &stack, visited_space);
MOVE_UNVISITED(interp->dict, &stack, visited_space);
Expand All @@ -1526,7 +1526,7 @@ mark_global_roots(PyInterpreterState *interp, PyGC_Head *visited, int visited_sp
MOVE_UNVISITED(types->for_extensions.initialized[i].tp_dict, &stack, visited_space);
MOVE_UNVISITED(types->for_extensions.initialized[i].tp_subclasses, &stack, visited_space);
}
objects_marked += move_all_transitively_reachable(&stack, visited, visited_space);
Py_ssize_t objects_marked = move_all_transitively_reachable(&stack, visited, visited_space);
assert(stack.top == NULL);
return objects_marked;
}
Expand All @@ -1547,12 +1547,11 @@ mark_at_start(PyThreadState *tstate)
static intptr_t
assess_work_to_do(GCState *gcstate)
{
/* The amount of work we want to do depends on three things.
/* The amount of work we want to do depends on two things.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth linking to the doc from here?

* 1. The number of new objects created
* 2. The growth in heap size since the last collection
* 3. The heap size (up to the number of new objects, to avoid quadratic effects)
* 2. The heap size (up to twice the number of new objects, to avoid quadratic effects)
*
* For a steady state heap, the amount of work to do is three times the number
* For a large, steady state heap, the amount of work to do is three times the number
* of new objects added to the heap. This ensures that we stay ahead in the
* worst case of all new objects being garbage.
*
Expand All @@ -1564,13 +1563,13 @@ assess_work_to_do(GCState *gcstate)
scale_factor = 2;
}
intptr_t new_objects = gcstate->young.count;
intptr_t max_heap_fraction = new_objects*3/2;
intptr_t max_heap_fraction = new_objects*2;
intptr_t heap_fraction = gcstate->heap_size / SCAN_RATE_DIVISOR / scale_factor;
if (heap_fraction > max_heap_fraction) {
heap_fraction = max_heap_fraction;
}
gcstate->young.count = 0;
return new_objects + heap_fraction * 2;
return new_objects + heap_fraction;
}

static void
Expand Down Expand Up @@ -1606,7 +1605,7 @@ gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
Py_ssize_t increment_size = move_all_transitively_reachable(&working, &increment, gcstate->visited_space);
gc_list_validate_space(&increment, gcstate->visited_space);
assert(working.top == NULL);
while (increment_size < gcstate->work_to_do) {
while (increment_size < gcstate->work_to_do * 2) {
if (gc_list_is_empty(not_visited)) {
break;
}
Expand All @@ -1626,7 +1625,7 @@ gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats)
gc_collect_region(tstate, &increment, &survivors, stats);
gc_list_merge(&survivors, visited);
assert(gc_list_is_empty(&increment));
gcstate->work_to_do -= increment_size;
gcstate->work_to_do -= increment_size/2;

add_stats(gcstate, 1, stats);
if (gc_list_is_empty(not_visited)) {
Expand Down Expand Up @@ -1661,6 +1660,7 @@ gc_collect_full(PyThreadState *tstate,
gcstate->old[0].count = 0;
gcstate->old[1].count = 0;
completed_scavenge(gcstate);
gcstate->work_to_do = -gcstate->young.threshold;
_PyGC_ClearAllFreeLists(tstate->interp);
validate_spaces(gcstate);
add_stats(gcstate, 2, stats);
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy