diff --git a/NEWS b/NEWS index 78f010b74ce7..9a529a29b0bb 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,8 @@ PHP NEWS . A flat unkeyed list assignment from an array literal with one value per target, such as [$a, $b] = [$b, $a], no longer builds the array when the result is unused (GH-23048). (Artem Ukrainskiy) + . Fixed bug GH-9993 (Bad performance with hard-coded GC_THRESHOLD_* values). + (Kamil Tekiela) - Curl: . Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire) diff --git a/UPGRADING b/UPGRADING index a1e30018cd69..e0bce481d953 100644 --- a/UPGRADING +++ b/UPGRADING @@ -183,6 +183,9 @@ PHP 8.7 UPGRADE NOTES is copied, and they are assigned to the targets directly. A value its target does not keep is now released at that assignment instead of after the last one. + . The garbage collector now raises its threshold when its runs traverse a + large graph of live objects, so that it runs less often. Before, it + traversed the whole graph again after every 10000 possible roots. - Standard: . Improved performance of strcspn() and strpbrk() when searching longer strings diff --git a/Zend/tests/gc/gh9993.phpt b/Zend/tests/gc/gh9993.phpt new file mode 100644 index 000000000000..d34677a25857 --- /dev/null +++ b/Zend/tests/gc/gh9993.phpt @@ -0,0 +1,70 @@ +--TEST-- +GH-9993: GC threshold follows the cost of traversing a large live graph +--SKIPIF-- + +--INI-- +zend.enable_gc=1 +memory_limit=256M +--FILE-- +data = $b; + $b->data = $a; + unset($a, $b); + + if ($container) { + // Adds a node of the live graph to the root buffer, so that the + // next GC run traverses the whole live graph. + $node = $container->nodes[$i % LIVE_NODES]; + unset($node); + } + } +} + +$container = new stdClass; +$container->nodes = []; +for ($i = 0; $i < LIVE_NODES; $i++) { + $node = new Node; + $node->owner = $container; + $container->nodes[] = $node; +} +unset($node); + +$before = gc_status(); +work($container, 500000); +$after = gc_status(); + +echo "Live graph:\n"; +var_dump($after['threshold'] >= LIVE_NODES / 8); +var_dump($after['buffer_size'] >= $after['threshold']); +// Without the cost rule the GC runs every 10000 roots, i.e. 140 times. +var_dump($after['runs'] - $before['runs'] < 100); + +unset($container); +work(null, 500000); +$after = gc_status(); + +echo "No live graph:\n"; +var_dump($after['threshold']); +?> +--EXPECT-- +Live graph: +bool(true) +bool(true) +bool(true) +No live graph: +int(10001) diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index 4ea0555957ef..944c03bb3965 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -191,6 +191,8 @@ #define GC_THRESHOLD_STEP 10000 #define GC_THRESHOLD_MAX 1000000000 #define GC_THRESHOLD_TRIGGER 100 +/* A GC run may traverse this many live nodes per root added since the last run */ +#define GC_THRESHOLD_COST_RATIO 8 /* GC flags */ #define GC_HAS_DESTRUCTORS (1<<0) @@ -278,11 +280,13 @@ typedef struct _zend_gc_globals { uint32_t unused; /* linked list of unused buffers */ uint32_t first_unused; /* first unused buffer */ uint32_t gc_threshold; /* GC collection threshold */ + uint32_t gc_base_threshold; /* threshold from the collected count */ uint32_t buf_size; /* size of the GC buffer */ uint32_t num_roots; /* number of roots in GC buffer */ uint32_t gc_runs; /* number of GC runs since reset */ uint32_t collected; /* number of collected nodes since reset */ + size_t visited; /* number of nodes traversed by the last run */ zend_hrtime_t activated_at; /* the timestamp of the last reset */ zend_hrtime_t collector_time; /* time spent running GC (ns) */ @@ -521,6 +525,7 @@ static void gc_globals_ctor_ex(zend_gc_globals *gc_globals) gc_globals->unused = GC_INVALID; gc_globals->first_unused = GC_INVALID; gc_globals->gc_threshold = GC_INVALID; + gc_globals->gc_base_threshold = GC_INVALID; gc_globals->buf_size = GC_INVALID; gc_globals->num_roots = 0; @@ -571,6 +576,8 @@ void gc_reset(void) GC_G(unused) = GC_INVALID; GC_G(first_unused) = GC_FIRST_ROOT; GC_G(num_roots) = 0; + /* The run cost depends on the live graph of the request */ + GC_G(gc_threshold) = GC_G(gc_base_threshold); GC_G(gc_runs) = 0; GC_G(collected) = 0; @@ -608,6 +615,7 @@ ZEND_API bool gc_enable(bool enable) GC_G(buf)[0].ref = NULL; GC_G(buf_size) = GC_DEFAULT_BUF_SIZE; GC_G(gc_threshold) = GC_THRESHOLD_DEFAULT; + GC_G(gc_base_threshold) = GC_THRESHOLD_DEFAULT; gc_reset(); } return old_enabled; @@ -656,35 +664,49 @@ static void gc_grow_root_buffer(void) GC_G(buf_size) = new_size; } +static void gc_grow_root_buffer_to(size_t min_size) +{ + size_t new_size = GC_G(buf_size); + + ZEND_ASSERT(min_size <= GC_MAX_BUF_SIZE); + if (new_size >= min_size) { + return; + } + while (new_size < min_size && new_size < GC_BUF_GROW_STEP) { + new_size *= 2; + } + if (new_size < min_size) { + new_size += ZEND_MM_ALIGNED_SIZE_EX(min_size - new_size, GC_BUF_GROW_STEP); + } + if (new_size > GC_MAX_BUF_SIZE) { + new_size = GC_MAX_BUF_SIZE; + } + GC_G(buf) = perealloc(GC_G(buf), sizeof(gc_root_buffer) * new_size, 1); + GC_G(buf_size) = new_size; +} + /* Adjust the GC activation threshold given the number of nodes collected by the last run */ static void gc_adjust_threshold(int count) { - uint32_t new_threshold; + uint32_t base = GC_G(gc_base_threshold); + size_t live, threshold; - /* TODO Very simple heuristic for dynamic GC buffer resizing: - * If there are "too few" collections, increase the collection threshold + /* If there are "too few" collections, increase the collection threshold * by a fixed step */ if (count < GC_THRESHOLD_TRIGGER || GC_G(num_roots) >= GC_G(gc_threshold)) { - /* increase */ - if (GC_G(gc_threshold) < GC_THRESHOLD_MAX) { - new_threshold = GC_G(gc_threshold) + GC_THRESHOLD_STEP; - if (new_threshold > GC_THRESHOLD_MAX) { - new_threshold = GC_THRESHOLD_MAX; - } - if (new_threshold > GC_G(buf_size)) { - gc_grow_root_buffer(); - } - if (new_threshold <= GC_G(buf_size)) { - GC_G(gc_threshold) = new_threshold; - } - } - } else if (GC_G(gc_threshold) > GC_THRESHOLD_DEFAULT) { - new_threshold = GC_G(gc_threshold) - GC_THRESHOLD_STEP; - if (new_threshold < GC_THRESHOLD_DEFAULT) { - new_threshold = GC_THRESHOLD_DEFAULT; - } - GC_G(gc_threshold) = new_threshold; + base = MIN(base + GC_THRESHOLD_STEP, GC_THRESHOLD_MAX); + } else if (base > GC_THRESHOLD_DEFAULT) { + base = MAX(base - GC_THRESHOLD_STEP, GC_THRESHOLD_DEFAULT); } + GC_G(gc_base_threshold) = base; + + /* Wait for enough new roots to pay for the live nodes that the run traversed */ + live = GC_G(visited) > (size_t)count ? GC_G(visited) - (size_t)count : 0; + threshold = MAX(live / GC_THRESHOLD_COST_RATIO + GC_FIRST_ROOT, base); + threshold = MIN(threshold, GC_THRESHOLD_MAX); + + gc_grow_root_buffer_to(threshold); + GC_G(gc_threshold) = (uint32_t)threshold; } /* Perform a GC run and then add a node as a possible root. */ @@ -1049,6 +1071,7 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) tail_call: GC_BENCH_INC(zval_marked_grey); + GC_G(visited)++; if (GC_TYPE(ref) == IS_OBJECT) { zend_object *obj = (zend_object*)ref; @@ -2010,6 +2033,9 @@ ZEND_API int zend_gc_collect_cycles(void) bool did_rerun_gc = false; zend_hrtime_t start_time = zend_hrtime(); + if (!GC_G(gc_active)) { + GC_G(visited) = 0; + } if (GC_G(num_roots) && !GC_G(gc_active)) { zend_gc_remove_root_tmpvars(); } @@ -2188,6 +2214,7 @@ ZEND_API int zend_gc_collect_cycles(void) * run GC another time. */ if (should_rerun_gc && !did_rerun_gc) { did_rerun_gc = true; + GC_G(visited) = 0; goto rerun_gc; }