Skip to content

Add some comments to zend_gc.c #19412

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
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
81 changes: 74 additions & 7 deletions Zend/zend_gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
#define GC_IDX2PTR(idx) (GC_G(buf) + (idx))
#define GC_PTR2IDX(ptr) ((ptr) - GC_G(buf))

/* Get the value to be placed in an unused buffer entry with the specified next unused list index */
#define GC_IDX2LIST(idx) ((void*)(uintptr_t)(((idx) * sizeof(void*)) | GC_UNUSED))
/* Get the index of the next item in the unused list from the given root buffer entry. */
#define GC_LIST2IDX(list) (((uint32_t)(uintptr_t)(list)) / sizeof(void*))

/* GC buffers */
Expand Down Expand Up @@ -228,10 +230,16 @@
} while (0)

/* unused buffers */

/* Are there any unused root buffer entries? */
#define GC_HAS_UNUSED() \
(GC_G(unused) != GC_INVALID)

/* Get the next unused entry and remove it from the list */
#define GC_FETCH_UNUSED() \
gc_fetch_unused()

/* Add a root buffer entry to the unused list */
#define GC_LINK_UNUSED(root) \
gc_link_unused(root)

Expand All @@ -249,7 +257,18 @@ typedef struct _gc_root_buffer {
} gc_root_buffer;

typedef struct _zend_gc_globals {
gc_root_buffer *buf; /* preallocated arrays of buffers */
/*
* The root buffer, which stores possible roots of reference cycles. It is
* also used to store garbage to be collected at the end of a run.
* A single array which is reallocated as necessary.
*
* The lower two bits in each entry are used for flags and need to be masked
* out to reconstruct a pointer.
*
* When an object in the root buffer is removed, the non-flag bits of the
* unused entry are used to store the index of the next entry in the unused
* list. */
gc_root_buffer *buf;

bool gc_enabled;
bool gc_active; /* GC currently running, forbid nested GC */
Expand All @@ -262,13 +281,13 @@ typedef struct _zend_gc_globals {
uint32_t buf_size; /* size of the GC buffer */
uint32_t num_roots; /* number of roots in GC buffer */

uint32_t gc_runs;
uint32_t collected;
uint32_t gc_runs; /* number of GC runs since reset */
uint32_t collected; /* number of collected objects since reset */

zend_hrtime_t activated_at;
zend_hrtime_t collector_time;
zend_hrtime_t dtor_time;
zend_hrtime_t free_time;
zend_hrtime_t activated_at; /* the timestamp of the last reset */
zend_hrtime_t collector_time; /* time spent running GC (ns) */
zend_hrtime_t dtor_time; /* time spent calling destructors (ns) */
zend_hrtime_t free_time; /* time spent destroying objects and freeing memory (ns) */

uint32_t dtor_idx; /* root buffer index */
uint32_t dtor_end;
Expand Down Expand Up @@ -313,6 +332,8 @@ static zend_gc_globals gc_globals;

typedef struct _gc_stack gc_stack;

/*
* The stack used for graph traversal is stored as a linked list of segments */
struct _gc_stack {
gc_stack *prev;
gc_stack *next;
Expand Down Expand Up @@ -375,6 +396,12 @@ static void gc_stack_free(gc_stack *stack)
}
}

/*
* Map a full index to a compressed index.
*
* The root buffer can have up to 2^30 entries, but we only have 20 bits to
* store the index. So we use the 1<<19 as a compression flag and use the other
* 19 bits to store the index modulo 2^19. */
static zend_always_inline uint32_t gc_compress(uint32_t idx)
{
if (EXPECTED(idx < GC_MAX_UNCOMPRESSED)) {
Expand All @@ -383,6 +410,10 @@ static zend_always_inline uint32_t gc_compress(uint32_t idx)
return (idx % GC_MAX_UNCOMPRESSED) | GC_MAX_UNCOMPRESSED;
}

/*
* Find the root buffer entry given a pointer and a compressed index.
* Iterate through the root buffer in steps of 2^19 until the pointer
* matches. */
static zend_always_inline gc_root_buffer* gc_decompress(zend_refcounted *ref, uint32_t idx)
{
gc_root_buffer *root = GC_IDX2PTR(idx);
Expand All @@ -401,6 +432,9 @@ static zend_always_inline gc_root_buffer* gc_decompress(zend_refcounted *ref, ui
}
}

/*
* Get the index of the next unused root buffer entry, and remove it from the
* unused list. GC_HAS_UNUSED() must be true before calling this. */
static zend_always_inline uint32_t gc_fetch_unused(void)
{
uint32_t idx;
Expand All @@ -414,6 +448,7 @@ static zend_always_inline uint32_t gc_fetch_unused(void)
return idx;
}

/* Add a root buffer entry to the unused list */
static zend_always_inline void gc_link_unused(gc_root_buffer *root)
{
root->ref = GC_IDX2LIST(GC_G(unused));
Expand Down Expand Up @@ -463,13 +498,15 @@ static void gc_trace_ref(zend_refcounted *ref) {
}
#endif

/* Mark a root buffer entry unused */
static zend_always_inline void gc_remove_from_roots(gc_root_buffer *root)
{
GC_LINK_UNUSED(root);
GC_G(num_roots)--;
GC_BENCH_DEC(root_buf_length);
}

/* Destroy the root buffer */
static void root_buffer_dtor(zend_gc_globals *gc_globals)
{
if (gc_globals->buf) {
Expand Down Expand Up @@ -565,6 +602,8 @@ void gc_reset(void)
GC_G(activated_at) = zend_hrtime();
}

/* Enable/disable the garbage collector.
* Initialize globals if necessary. */
ZEND_API bool gc_enable(bool enable)
{
bool old_enabled = GC_G(gc_enabled);
Expand All @@ -584,6 +623,7 @@ ZEND_API bool gc_enabled(void)
return GC_G(gc_enabled);
}

/* Protect the GC root buffer (prevent additions) */
ZEND_API bool gc_protect(bool protect)
{
bool old_protected = GC_G(gc_protected);
Expand All @@ -596,6 +636,7 @@ ZEND_API bool gc_protected(void)
return GC_G(gc_protected);
}

/* Reallocate the GC root buffer */
static void gc_grow_root_buffer(void)
{
size_t new_size;
Expand All @@ -621,6 +662,7 @@ static void gc_grow_root_buffer(void)
GC_G(buf_size) = new_size;
}

/* Adjust the GC activation threshold given the number of objects collected by the last run */
static void gc_adjust_threshold(int count)
{
uint32_t new_threshold;
Expand Down Expand Up @@ -651,6 +693,7 @@ static void gc_adjust_threshold(int count)
}
}

/* Add an object as a possible root, and perform a GC run unless one is active already. */
static zend_never_inline void ZEND_FASTCALL gc_possible_root_when_full(zend_refcounted *ref)
{
uint32_t idx;
Expand Down Expand Up @@ -695,6 +738,8 @@ static zend_never_inline void ZEND_FASTCALL gc_possible_root_when_full(zend_refc
GC_BENCH_PEAK(root_buf_peak, root_buf_length);
}

/* Add a possible root object to the buffer.
* Maybe perform a GC run. */
ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref)
{
uint32_t idx;
Expand Down Expand Up @@ -731,6 +776,7 @@ ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref)
GC_BENCH_PEAK(root_buf_peak, root_buf_length);
}

/* Add an extra root during a GC run */
static void ZEND_FASTCALL gc_extra_root(zend_refcounted *ref)
{
uint32_t idx;
Expand Down Expand Up @@ -764,12 +810,14 @@ static void ZEND_FASTCALL gc_extra_root(zend_refcounted *ref)
GC_BENCH_PEAK(root_buf_peak, root_buf_length);
}

/* Remove an object from the root buffer given its compressed index */
static zend_never_inline void ZEND_FASTCALL gc_remove_compressed(zend_refcounted *ref, uint32_t idx)
{
gc_root_buffer *root = gc_decompress(ref, idx);
gc_remove_from_roots(root);
}

/* Remove an object from the root buffer */
ZEND_API void ZEND_FASTCALL gc_remove_from_buffer(zend_refcounted *ref)
{
gc_root_buffer *root;
Expand All @@ -793,6 +841,10 @@ ZEND_API void ZEND_FASTCALL gc_remove_from_buffer(zend_refcounted *ref)
gc_remove_from_roots(root);
}

/* Traverse the graph of objects referred to by ref. Change grey objects back
* to black, and restore their reference counts. See ScanBlack() in Bacon & Rajan.
* To implement a depth-first search, discovered objects are added to a stack which
* is processed iteratively. */
static void gc_scan_black(zend_refcounted *ref, gc_stack *stack)
{
HashTable *ht;
Expand Down Expand Up @@ -992,6 +1044,8 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack)
}
}

/* Traverse the graph of objects referred to by ref. Decrement the reference
* counts and mark visited objects grey. See MarkGray() in Bacon & Rajan. */
static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack)
{
HashTable *ht;
Expand Down Expand Up @@ -1204,6 +1258,8 @@ static void gc_compact(void)
}
}

/* For all roots marked purple, traverse the graph, marking referred objects grey.
* See MarkRoots() in Bacon & Rajan. */
Copy link
Member

Choose a reason for hiding this comment

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

Also, for each traversed node, this decrements the refcount of child nodes. The purpose of marking grey is to remember which nodes where already traversed during this step.

On a higher level this simulates the effect (on refcounts) of deleting roots. Nodes that end up with a refcount > 0, and nodes reachable from them, are proven to not be part of a garbage cycle, and to be live. gc_scan_roots() will then restore the refcount of live nodes and mark other ones as garbage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I clarified this.

Copy link
Member

Choose a reason for hiding this comment

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

/* For all roots marked purple, traverse the graph, decrementing the reference
 * count of visited nodes. Mark visited nodes grey so that their reference
 * counts will only be decremented once. See MarkRoots() in Bacon & Rajan. */

This is slightly incorrect as it can be understood that we are decrementing the refcount of non-grey nodes themselves. This would also imply that we decrement the refcount at most once per node. In reality we decrement the refcount of their child nodes ("internal reference counts" in the paper).

I suggest:

For all roots marked purple, traverse the graph, decrementing the reference
count of their child nodes. Mark visited nodes grey so that they are not
visited again. See MarkRoots() in Bacon & Rajan.

static void gc_mark_roots(gc_stack *stack)
{
gc_root_buffer *current, *last;
Expand All @@ -1223,6 +1279,10 @@ static void gc_mark_roots(gc_stack *stack)
}
}

/* Traverse the reference graph of ref. Evaluate grey nodes and mark them
* black (to keep) or white (to free). Note that nodes initially marked white
* may later become black if they are visited from a live node.
* See Scan() in Bacon & Rajan. */
static void gc_scan(zend_refcounted *ref, gc_stack *stack)
{
HashTable *ht;
Expand Down Expand Up @@ -1376,6 +1436,7 @@ static void gc_scan(zend_refcounted *ref, gc_stack *stack)
}
}

/* Scan all roots, coloring grey nodes black or white */
static void gc_scan_roots(gc_stack *stack)
{
uint32_t idx, end;
Expand Down Expand Up @@ -1409,6 +1470,8 @@ static void gc_scan_roots(gc_stack *stack)
}
}

/* Add an object to the buffer with the garbage flag, so that it will be
* destroyed and freed when the scan is complete. */
static void gc_add_garbage(zend_refcounted *ref)
{
uint32_t idx;
Expand All @@ -1434,6 +1497,7 @@ static void gc_add_garbage(zend_refcounted *ref)
GC_G(num_roots)++;
}

/* Traverse the reference graph from ref, marking any white objects as garbage. */
static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *stack)
{
int count = 0;
Expand Down Expand Up @@ -1622,6 +1686,7 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta
return count;
}

/* Traverse the reference graph from all roots, marking white nodes as garbage. */
static int gc_collect_roots(uint32_t *flags, gc_stack *stack)
{
uint32_t idx, end;
Expand Down Expand Up @@ -1808,6 +1873,7 @@ static ZEND_COLD ZEND_NORETURN void gc_start_destructor_fiber_error(void)
zend_error_noreturn(E_ERROR, "Unable to start destructor fiber");
}

/* Call destructors for garbage in the buffer. */
static zend_always_inline zend_result gc_call_destructors(uint32_t idx, uint32_t end, zend_fiber *fiber)
{
gc_root_buffer *current;
Expand Down Expand Up @@ -1910,6 +1976,7 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
}
}

/* Perform a garbage collection run. The default implementation of gc_collect_cycles. */
ZEND_API int zend_gc_collect_cycles(void)
{
int total_count = 0;
Expand Down
Loading