-
-
Notifications
You must be signed in to change notification settings - Fork 33.2k
GH-117759: Update GC docs for incremental collection #126695
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
markshannon
merged 5 commits into
python:main
from
faster-cpython:update-gc-internal-docs
Nov 14, 2024
Merged
Changes from 3 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -108,7 +108,7 @@ As is explained later in the | |
| [Optimization: reusing fields to save memory](#optimization-reusing-fields-to-save-memory) | ||
| section, these two extra fields are normally used to keep doubly linked lists of all the | ||
| objects tracked by the garbage collector (these lists are the GC generations, more on | ||
| that in the [Optimization: generations](#Optimization-generations) section), but | ||
| that in the [Optimization: incremental collection](#Optimization-incremental-collection) section), but | ||
| they are also reused to fulfill other purposes when the full doubly linked list | ||
| structure is not needed as a memory optimization. | ||
|
|
||
|
|
@@ -351,38 +351,82 @@ follows these steps in order: | |
| the reference counts fall to 0, triggering the destruction of all unreachable | ||
| objects. | ||
|
|
||
| Optimization: generations | ||
| ========================= | ||
| Optimization: incremental collection | ||
| ==================================== | ||
|
|
||
| In order to limit the time each garbage collection takes, the GC | ||
| implementation for the default build uses a popular optimization: | ||
| generations. The main idea behind this concept is the assumption that most | ||
| objects have a very short lifespan and can thus be collected soon after their | ||
| creation. This has proven to be very close to the reality of many Python | ||
| In order to limit the time each garbage collection takes, the GC implementation | ||
| for the default build uses incremental collection with two generations. | ||
|
|
||
| Generational garbage collection takes advantage of what is known as the weak | ||
| generational hypothesis: Most objects die young. | ||
| This has proven to be very close to the reality of many Python | ||
| programs as many temporary objects are created and destroyed very quickly. | ||
|
|
||
| To take advantage of this fact, all container objects are segregated into | ||
| three spaces/generations. Every new | ||
| object starts in the first generation (generation 0). The previous algorithm is | ||
| executed only over the objects of a particular generation and if an object | ||
| survives a collection of its generation it will be moved to the next one | ||
| (generation 1), where it will be surveyed for collection less often. If | ||
| the same object survives another GC round in this new generation (generation 1) | ||
| it will be moved to the last generation (generation 2) where it will be | ||
| surveyed the least often. | ||
|
|
||
| The GC implementation for the free-threaded build does not use multiple | ||
| generations. Every collection operates on the entire heap. | ||
| two generations: young and old. Every new object starts in the young generation. | ||
iritkatriel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| In order to keep pause times down, scanning of the old generation of the heap | ||
| occurs in increments. To keep track of what has been scanned, | ||
| the old generation contains two lists: | ||
|
|
||
| * Those objects that have not yet been scanned, referred to as the `pending` list. | ||
| * Those objects that have been scanned, referred to as the `visited` list. | ||
|
|
||
| To detect and collect all unreachable objects in the heap, the garbage collector | ||
| must scan the whole heap. This whole heap scan is called a full scavenge. | ||
iritkatriel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Increments | ||
| ---------- | ||
|
|
||
| Each full scavenge is performed in a series of increments. | ||
| For each full scavenge, the combined increments will cover the whole heap. | ||
|
|
||
| For each increment, the portion of the heap scanned by a single collection is | ||
|
||
| made up of three parts: | ||
|
|
||
| * The young generation | ||
| * The old generation's least recently scanned objects | ||
| * All objects reachable from those objects that have not yet been scanned this full scavenge | ||
|
|
||
| The surviving objects (those that are not collected) are moved to the back of the | ||
| `visited` list in the old generation. | ||
|
|
||
| When a full scavenge starts, no objects in the heap are considered to have been scanned, | ||
| so all objects in the old generation must be in the `pending` space. | ||
| When all objects in the heap have been scanned a cycle ends, and all objects are moved | ||
| to the `pending` list again. To avoid having to traverse the entire list, which list is | ||
| `pending` and which is `visited` is determined by a field in the `GCState` struct. | ||
| The `visited` and `pending` lists can be swapped by toggling this bit. | ||
|
|
||
| Correctness | ||
| ----------- | ||
|
|
||
| In order to collect all unreachable cycles, each increment must contain all of | ||
| an unreachable cycle, or none of it. | ||
iritkatriel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| In order to make sure that the whole of any unreachable cycle is contained in an | ||
| increment, all unscanned objects reachable from any object in the increment must | ||
iritkatriel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| be included in the increment. | ||
| Thus, to form a complete increment we perform a | ||
| [transitive closure](https://en.wikipedia.org/wiki/Transitive_closure) | ||
| over reachable, unscanned objects from the initial increment. | ||
| We can exclude scanned objects, as they must have been reachable when scanned. | ||
| If a scanned object becomes part of an unreachable cycle after being scanned, it | ||
| will not be collected this cycle, but it will be collected in the next full scavenge. | ||
|
|
||
| > [!NOTE] | ||
| > The GC implementation for the free-threaded build does not use incremental collection. | ||
| > Every collection operates on the entire heap. | ||
|
|
||
| In order to decide when to run, the collector keeps track of the number of object | ||
| allocations and deallocations since the last collection. When the number of | ||
| allocations minus the number of deallocations exceeds `threshold_0`, | ||
| collection starts. Initially only generation 0 is examined. If generation 0 has | ||
| been examined more than `threshold_1` times since generation 1 has been | ||
| examined, then generation 1 is examined as well. With generation 2, | ||
| things are a bit more complicated; see | ||
| [Collecting the oldest generation](#Collecting-the-oldest-generation) for | ||
| more information. These thresholds can be examined using the | ||
| allocations minus the number of deallocations exceeds `threshold0`, | ||
| collection starts. `threshold1` determines the fraction of the old | ||
| collection that is included in the increment. | ||
| The fraction is inversely proportional to `threshold1`, | ||
| as historically a larger `threshold1` meant that old generation | ||
| collections were performed less frequently. | ||
| `threshold2` is ignored. | ||
|
|
||
| These thresholds can be examined using the | ||
| [`gc.get_threshold()`](https://docs.python.org/3/library/gc.html#gc.get_threshold) | ||
| function: | ||
|
|
||
|
|
@@ -402,8 +446,8 @@ specifically in a generation by calling `gc.collect(generation=NUM)`. | |
| ... pass | ||
| ... | ||
|
|
||
| # Move everything to the last generation so it's easier to inspect | ||
| # the younger generations. | ||
| # Move everything to the old generation so it's easier to inspect | ||
| # the young generation. | ||
|
|
||
| >>> gc.collect() | ||
| 0 | ||
|
|
@@ -413,40 +457,24 @@ specifically in a generation by calling `gc.collect(generation=NUM)`. | |
| >>> x = MyObj() | ||
| >>> x.self = x | ||
|
|
||
| # Initially the object is in the youngest generation. | ||
| # Initially the object is in the young generation. | ||
|
|
||
| >>> gc.get_objects(generation=0) | ||
| [..., <__main__.MyObj object at 0x7fbcc12a3400>, ...] | ||
|
|
||
| # After a collection of the youngest generation the object | ||
| # moves to the next generation. | ||
| # moves to the old generation. | ||
|
|
||
| >>> gc.collect(generation=0) | ||
| 0 | ||
| >>> gc.get_objects(generation=0) | ||
| [] | ||
| >>> gc.get_objects(generation=1) | ||
| [] | ||
| >>> gc.get_objects(generation=2) | ||
| [..., <__main__.MyObj object at 0x7fbcc12a3400>, ...] | ||
| ``` | ||
|
|
||
| Collecting the oldest generation | ||
| -------------------------------- | ||
|
|
||
| In addition to the various configurable thresholds, the GC only triggers a full | ||
| collection of the oldest generation if the ratio `long_lived_pending / long_lived_total` | ||
| is above a given value (hardwired to 25%). The reason is that, while "non-full" | ||
| collections (that is, collections of the young and middle generations) will always | ||
| examine roughly the same number of objects (determined by the aforementioned | ||
| thresholds) the cost of a full collection is proportional to the total | ||
| number of long-lived objects, which is virtually unbounded. Indeed, it has | ||
| been remarked that doing a full collection every <constant number> of object | ||
| creations entails a dramatic performance degradation in workloads which consist | ||
| of creating and storing lots of long-lived objects (for example, building a large list | ||
| of GC-tracked objects would show quadratic performance, instead of linear as | ||
| expected). Using the above ratio, instead, yields amortized linear performance | ||
| in the total number of objects (the effect of which can be summarized thusly: | ||
| "each full garbage collection is more and more costly as the number of objects | ||
| grows, but we do fewer and fewer of them"). | ||
|
|
||
| Optimization: reusing fields to save memory | ||
| =========================================== | ||
|
|
@@ -588,9 +616,9 @@ heap. | |
| be more difficult. | ||
|
|
||
|
|
||
| > [!NOTE] | ||
| > [!NOTE] | ||
| > **Document history** | ||
| > | ||
| > | ||
| > Pablo Galindo Salgado - Original author | ||
| > | ||
| > | ||
| > Irit Katriel - Convert to Markdown | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.