| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2a373da commit a8dd821
14 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -99,6 +99,8 @@ typedef struct _gc_stats { | |||
| 99 | 99 | uint64_t collections; | |
| 100 | 100 | uint64_t object_visits; | |
| 101 | 101 | uint64_t objects_collected; | |
| 102 | + uint64_t objects_transitively_reachable; | ||
| 103 | + uint64_t objects_not_transitively_reachable; | ||
| 102 | 104 | } GCStats; | |
| 103 | 105 | ||
| 104 | 106 | typedef struct _uop_stats { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -75,6 +75,7 @@ typedef struct _PyInterpreterFrame { | |||
| 75 | 75 | _PyStackRef *stackpointer; | |
| 76 | 76 | uint16_t return_offset; /* Only relevant during a function call */ | |
| 77 | 77 | char owner; | |
| 78 | + char visited; | ||
| 78 | 79 | /* Locals and stack */ | |
| 79 | 80 | _PyStackRef localsplus[1]; | |
| 80 | 81 | } _PyInterpreterFrame; | |
@@ -207,6 +208,7 @@ _PyFrame_Initialize( | |||
| 207 | 208 | #endif | |
| 208 | 209 | frame->return_offset = 0; | |
| 209 | 210 | frame->owner = FRAME_OWNED_BY_THREAD; | |
| 211 | + frame->visited = 0; | ||
| 210 | 212 | ||
| 211 | 213 | for (int i = null_locals_from; i < code->co_nlocalsplus; i++) { | |
| 212 | 214 | frame->localsplus[i] = PyStackRef_NULL; | |
@@ -389,6 +391,7 @@ _PyFrame_PushTrampolineUnchecked(PyThreadState *tstate, PyCodeObject *code, int | |||
| 389 | 391 | frame->instr_ptr = _PyCode_CODE(code); | |
| 390 | 392 | #endif | |
| 391 | 393 | frame->owner = FRAME_OWNED_BY_THREAD; | |
| 394 | + frame->visited = 0; | ||
| 392 | 395 | frame->return_offset = 0; | |
| 393 | 396 | ||
| 394 | 397 | #ifdef Py_GIL_DISABLED | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,11 +10,11 @@ extern "C" { | |||
| 10 | 10 | ||
| 11 | 11 | /* GC information is stored BEFORE the object structure. */ | |
| 12 | 12 | typedef struct { | |
| 13 | - // Pointer to next object in the list. | ||
| 13 | + // Tagged pointer to next object in the list. | ||
| 14 | 14 | // 0 means the object is not tracked | |
| 15 | 15 | uintptr_t _gc_next; | |
| 16 | 16 | ||
| 17 | - // Pointer to previous object in the list. | ||
| 17 | + // Tagged pointer to previous object in the list. | ||
| 18 | 18 | // Lowest two bits are used for flags documented later. | |
| 19 | 19 | uintptr_t _gc_prev; | |
| 20 | 20 | } PyGC_Head; | |
@@ -284,6 +284,11 @@ struct gc_generation_stats { | |||
| 284 | 284 | Py_ssize_t uncollectable; | |
| 285 | 285 | }; | |
| 286 | 286 | ||
| 287 | + enum _GCPhase { | ||
| 288 | + GC_PHASE_MARK = 0, | ||
| 289 | + GC_PHASE_COLLECT = 1 | ||
| 290 | + }; | ||
| 291 | + | ||
| 287 | 292 | struct _gc_runtime_state { | |
| 288 | 293 | /* List of objects that still need to be cleaned up, singly linked | |
| 289 | 294 | * via their gc headers' gc_prev pointers. */ | |
@@ -311,6 +316,7 @@ struct _gc_runtime_state { | |||
| 311 | 316 | Py_ssize_t work_to_do; | |
| 312 | 317 | /* Which of the old spaces is the visited space */ | |
| 313 | 318 | int visited_space; | |
| 319 | + int phase; | ||
| 314 | 320 | ||
| 315 | 321 | #ifdef Py_GIL_DISABLED | |
| 316 | 322 | /* This is the number of objects that survived the last full | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -471,8 +471,8 @@ static inline void _PyObject_GC_TRACK( | |||
| 471 | 471 | PyGC_Head *last = (PyGC_Head*)(generation0->_gc_prev); | |
| 472 | 472 | _PyGCHead_SET_NEXT(last, gc); | |
| 473 | 473 | _PyGCHead_SET_PREV(gc, last); | |
| 474 | - /* Young objects will be moved into the visited space during GC, so set the bit here */ | ||
| 475 | - gc->_gc_next = ((uintptr_t)generation0) | (uintptr_t)interp->gc.visited_space; | ||
| 474 | + uintptr_t not_visited = 1 ^ interp->gc.visited_space; | ||
| 475 | + gc->_gc_next = ((uintptr_t)generation0) | not_visited; | ||
| 476 | 476 | generation0->_gc_prev = (uintptr_t)gc; | |
| 477 | 477 | #endif | |
| 478 | 478 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -137,6 +137,7 @@ extern PyTypeObject _PyExc_MemoryError; | |||
| 137 | 137 | { .threshold = 0, }, \ | |
| 138 | 138 | }, \ | |
| 139 | 139 | .work_to_do = -5000, \ | |
| 140 | + .phase = GC_PHASE_MARK, \ | ||
| 140 | 141 | }, \ | |
| 141 | 142 | .qsbr = { \ | |
| 142 | 143 | .wr_seq = QSBR_INITIAL, \ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -477,6 +477,45 @@ specifically in a generation by calling `gc.collect(generation=NUM)`. | |||
| 477 | 477 | ``` | |
| 478 | 478 | ||
| 479 | 479 | ||
| 480 | + Optimization: visiting reachable objects | ||
| 481 | + ======================================== | ||
| 482 | + | ||
| 483 | + An object cannot be garbage if it can be reached. | ||
| 484 | + | ||
| 485 | + To avoid having to identify reference cycles across the whole heap, we can | ||
| 486 | + reduce the amount of work done considerably by first moving most reachable objects | ||
| 487 | + to the `visited` space. Empirically, most reachable objects can be reached from a | ||
| 488 | + small set of global objects and local variables. | ||
| 489 | + This step does much less work per object, so reduces the time spent | ||
| 490 | + performing garbage collection by at least half. | ||
| 491 | + | ||
| 492 | + > [!NOTE] | ||
| 493 | + > Objects that are not determined to be reachable by this pass are not necessarily | ||
| 494 | + > unreachable. We still need to perform the main algorithm to determine which objects | ||
| 495 | + > are actually unreachable. | ||
| 496 | + We use the same technique of forming a transitive closure as the incremental | ||
| 497 | + collector does to find reachable objects, seeding the list with some global | ||
| 498 | + objects and the currently executing frames. | ||
| 499 | + | ||
| 500 | + This phase moves objects to the `visited` space, as follows: | ||
| 501 | + | ||
| 502 | + 1. All objects directly referred to by any builtin class, the `sys` module, the `builtins` | ||
| 503 | + module and all objects directly referred to from stack frames are added to a working | ||
| 504 | + set of reachable objects. | ||
| 505 | + 2. Until this working set is empty: | ||
| 506 | + 1. Pop an object from the set and move it to the `visited` space | ||
| 507 | + 2. For each object directly reachable from that object: | ||
| 508 | + * If it is not already in `visited` space and it is a GC object, | ||
| 509 | + add it to the working set | ||
| 510 | + | ||
| 511 | + | ||
| 512 | + Before each increment of collection is performed, the stacks are scanned | ||
| 513 | + to check for any new stack frames that have been created since the last | ||
| 514 | + increment. All objects directly referred to from those stack frames are | ||
| 515 | + added to the working set. | ||
| 516 | + Then the above algorithm is repeated, starting from step 2. | ||
| 517 | + | ||
| 518 | + | ||
| 480 | 519 | Optimization: reusing fields to save memory | |
| 481 | 520 | =========================================== | |
| 482 | 521 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -123,9 +123,9 @@ def get_pooled_int(value): | |||
| 123 | 123 | xml_filename = 'refleak-xml.tmp' | |
| 124 | 124 | result = None | |
| 125 | 125 | dash_R_cleanup(fs, ps, pic, zdc, abcs) | |
| 126 | - support.gc_collect() | ||
| 127 | 126 | ||
| 128 | 127 | for i in rep_range: | |
| 128 | + support.gc_collect() | ||
| 129 | 129 | current = refleak_helper._hunting_for_refleaks | |
| 130 | 130 | refleak_helper._hunting_for_refleaks = True | |
| 131 | 131 | try: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -31,6 +31,11 @@ def __new__(cls, *args, **kwargs): | |||
| 31 | 31 | return C | |
| 32 | 32 | ContainerNoGC = None | |
| 33 | 33 | ||
| 34 | + try: | ||
| 35 | + import _testinternalcapi | ||
| 36 | + except ImportError: | ||
| 37 | + _testinternalcapi = None | ||
| 38 | + | ||
| 34 | 39 | ### Support code | |
| 35 | 40 | ############################################################################### | |
| 36 | 41 | ||
@@ -1130,6 +1135,7 @@ def setUp(self): | |||
| 1130 | 1135 | def tearDown(self): | |
| 1131 | 1136 | gc.disable() | |
| 1132 | 1137 | ||
| 1138 | + @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") | ||
| 1133 | 1139 | @requires_gil_enabled("Free threading does not support incremental GC") | |
| 1134 | 1140 | # Use small increments to emulate longer running process in a shorter time | |
| 1135 | 1141 | @gc_threshold(200, 10) | |
@@ -1167,20 +1173,15 @@ def make_ll(depth): | |||
| 1167 | 1173 | enabled = gc.isenabled() | |
| 1168 | 1174 | gc.enable() | |
| 1169 | 1175 | olds = [] | |
| 1176 | + initial_heap_size = _testinternalcapi.get_tracked_heap_size() | ||
| 1170 | 1177 | for i in range(20_000): | |
| 1171 | 1178 | newhead = make_ll(20) | |
| 1172 | 1179 | count += 20 | |
| 1173 | 1180 | newhead.surprise = head | |
| 1174 | 1181 | olds.append(newhead) | |
| 1175 | 1182 | if len(olds) == 20: | |
| 1176 | - stats = gc.get_stats() | ||
| 1177 | - young = stats[0] | ||
| 1178 | - incremental = stats[1] | ||
| 1179 | - old = stats[2] | ||
| 1180 | - collected = young['collected'] + incremental['collected'] + old['collected'] | ||
| 1181 | - count += CORRECTION | ||
| 1182 | - live = count - collected | ||
| 1183 | - self.assertLess(live, 25000) | ||
| 1183 | + new_objects = _testinternalcapi.get_tracked_heap_size() - initial_heap_size | ||
| 1184 | + self.assertLess(new_objects, 27_000, f"Heap growing. Reached limit after {i} iterations") | ||
| 1184 | 1185 | del olds[:] | |
| 1185 | 1186 | if not enabled: | |
| 1186 | 1187 | gc.disable() | |
@@ -1322,7 +1323,8 @@ def test_refcount_errors(self): | |||
| 1322 | 1323 | from test.support import gc_collect, SuppressCrashReport | |
| 1323 | 1324 | ||
| 1324 | 1325 | a = [1, 2, 3] | |
| 1325 | - b = [a] | ||
| 1326 | + b = [a, a] | ||
| 1327 | + a.append(b) | ||
| 1326 | 1328 | ||
| 1327 | 1329 | # Avoid coredump when Py_FatalError() calls abort() | |
| 1328 | 1330 | SuppressCrashReport().__enter__() | |
@@ -1332,6 +1334,8 @@ def test_refcount_errors(self): | |||
| 1332 | 1334 | # (to avoid deallocating it): | |
| 1333 | 1335 | import ctypes | |
| 1334 | 1336 | ctypes.pythonapi.Py_DecRef(ctypes.py_object(a)) | |
| 1337 | + del a | ||
| 1338 | + del b | ||
| 1335 | 1339 | ||
| 1336 | 1340 | # The garbage collector should now have a fatal error | |
| 1337 | 1341 | # when it reaches the broken object | |
@@ -1360,7 +1364,7 @@ def test_refcount_errors(self): | |||
| 1360 | 1364 | self.assertRegex(stderr, | |
| 1361 | 1365 | br'object type name: list') | |
| 1362 | 1366 | self.assertRegex(stderr, | |
| 1363 | - br'object repr : \[1, 2, 3\]') | ||
| 1367 | + br'object repr : \[1, 2, 3, \[\[...\], \[...\]\]\]') | ||
| 1364 | 1368 | ||
| 1365 | 1369 | ||
| 1366 | 1370 | class GCTogglingTests(unittest.TestCase): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,4 @@ | |||
| 1 | + Add a marking phase to the GC. All objects that can be transitively reached | ||
| 2 | + from builtin modules or the stacks are marked as reachable before cycle | ||
| 3 | + detection. This reduces the amount of work done by the GC by approximately | ||
| 4 | + half. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2076,6 +2076,11 @@ has_deferred_refcount(PyObject *self, PyObject *op) | |||
| 2076 | 2076 | return PyBool_FromLong(_PyObject_HasDeferredRefcount(op)); | |
| 2077 | 2077 | } | |
| 2078 | 2078 | ||
| 2079 | + static PyObject * | ||
| 2080 | + get_tracked_heap_size(PyObject *self, PyObject *Py_UNUSED(ignored)) | ||
| 2081 | + { | ||
| 2082 | + return PyLong_FromInt64(PyInterpreterState_Get()->gc.heap_size); | ||
| 2083 | + } | ||
| 2079 | 2084 | ||
| 2080 | 2085 | static PyMethodDef module_functions[] = { | |
| 2081 | 2086 | {"get_configs", get_configs, METH_NOARGS}, | |
@@ -2174,6 +2179,7 @@ static PyMethodDef module_functions[] = { | |||
| 2174 | 2179 | {"get_static_builtin_types", get_static_builtin_types, METH_NOARGS}, | |
| 2175 | 2180 | {"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS}, | |
| 2176 | 2181 | {"has_deferred_refcount", has_deferred_refcount, METH_O}, | |
| 2182 | + {"get_tracked_heap_size", get_tracked_heap_size, METH_NOARGS}, | ||
| 2177 | 2183 | {NULL, NULL} /* sentinel */ | |
| 2178 | 2184 | }; | |
| 2179 | 2185 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments