From e0bdde04d88bd7b714bef670ec3e08b44bf7734e Mon Sep 17 00:00:00 2001 From: Dmitry Stogov Date: Mon, 29 Jan 2024 10:02:58 +0300 Subject: [PATCH 1/2] Fix GH-13193: Significant performance degradation in 'foreach' starting from PHP 8.2.13 (caused by garbage collection) --- Zend/zend_gc.c | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index ab5a04898de6e..e1b44f9a6e0c1 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -1465,7 +1465,8 @@ static int gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffe } static void zend_get_gc_buffer_release(void); -static void zend_gc_root_tmpvars(void); +static void zend_gc_check_root_tmpvars(void); +static void zend_gc_remove_root_tmpvars(void); ZEND_API int zend_gc_collect_cycles(void) { @@ -1473,6 +1474,8 @@ ZEND_API int zend_gc_collect_cycles(void) bool should_rerun_gc = 0; bool did_rerun_gc = 0; + zend_gc_remove_root_tmpvars(); + rerun_gc: if (GC_G(num_roots)) { int count; @@ -1669,7 +1672,7 @@ ZEND_API int zend_gc_collect_cycles(void) finish: zend_get_gc_buffer_release(); - zend_gc_root_tmpvars(); + zend_gc_check_root_tmpvars(); return total_count; } @@ -1707,7 +1710,7 @@ static void zend_get_gc_buffer_release(void) { * cycles. However, there are some rare exceptions where this is possible, in which case we rely * on the producing code to root the value. If a GC run occurs between the rooting and consumption * of the value, we would end up leaking it. To avoid this, root all live TMPVAR values here. */ -static void zend_gc_root_tmpvars(void) { +static void zend_gc_check_root_tmpvars(void) { zend_execute_data *ex = EG(current_execute_data); for (; ex; ex = ex->prev_execute_data) { zend_function *func = ex->func; @@ -1737,6 +1740,36 @@ static void zend_gc_root_tmpvars(void) { } } +static void zend_gc_remove_root_tmpvars(void) { + zend_execute_data *ex = EG(current_execute_data); + for (; ex; ex = ex->prev_execute_data) { + zend_function *func = ex->func; + if (!func || !ZEND_USER_CODE(func->type)) { + continue; + } + + uint32_t op_num = ex->opline - ex->func->op_array.opcodes; + for (uint32_t i = 0; i < func->op_array.last_live_range; i++) { + const zend_live_range *range = &func->op_array.live_range[i]; + if (range->start > op_num) { + break; + } + if (range->end <= op_num) { + continue; + } + + uint32_t kind = range->var & ZEND_LIVE_MASK; + if (kind == ZEND_LIVE_TMPVAR || kind == ZEND_LIVE_LOOP) { + uint32_t var_num = range->var & ~ZEND_LIVE_MASK; + zval *var = ZEND_CALL_VAR(ex, var_num); + if (Z_REFCOUNTED_P(var)) { + GC_REMOVE_FROM_BUFFER(Z_COUNTED_P(var)); + } + } + } + } +} + #ifdef ZTS size_t zend_gc_globals_size(void) { From a7559debbb598c8e1efeb87447af91f847a0795d Mon Sep 17 00:00:00 2001 From: Dmitry Stogov Date: Mon, 29 Jan 2024 19:33:28 +0300 Subject: [PATCH 2/2] Don't run zend_gc_remove_root_tmpvars() if GC is not active or GC buffer is empty --- Zend/zend_gc.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index e1b44f9a6e0c1..e7d9c8ef29257 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -1474,7 +1474,9 @@ ZEND_API int zend_gc_collect_cycles(void) bool should_rerun_gc = 0; bool did_rerun_gc = 0; - zend_gc_remove_root_tmpvars(); + if (GC_G(num_roots) && GC_G(gc_active)) { + zend_gc_remove_root_tmpvars(); + } rerun_gc: if (GC_G(num_roots)) {