From 233129c4d61e7646c76acebb677b687906144928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 21 Jan 2025 08:58:13 +0100 Subject: [PATCH 1/3] array_find: Fix data type for `retval_true` --- ext/standard/array.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 6bfc0dc9c0403..dbd917028f793 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6629,7 +6629,7 @@ static zend_result php_array_find(const HashTable *array, zend_fcall_info fci, z zend_result result = zend_call_function(&fci, &fci_cache); if (EXPECTED(result == SUCCESS)) { - int retval_true; + bool retval_true; retval_true = zend_is_true(&retval); zval_ptr_dtor(&retval); From 59bf78d4aca4b2bdc511a7964de010a8649bdf18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 21 Jan 2025 08:59:59 +0100 Subject: [PATCH 2/3] array_find: Remove unnecessary refcounting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a post on LinkedIn [1], Bohuslav Šimek reported that the native implementation of `array_find()` was about 3× slower than the equivalent userland implementation. While I was not able to verify this claim, due to a lack of reproducer provided, I could confirm that the native `array_find()` was indeed slower than the equivalent userland implementation. For the following example script: $value) { if ($callback($value, $key)) { return $value; } } return null; } $array = range(1, 10000); $result = 0; for ($i = 0; $i < 5000; $i++) { $result += array_find($array, static function ($item) { return $item === 5000; }); } var_dump($result); with the `array_find()` call appropriately replaced for each case, a PHP-8.4 release build provided the following results: Benchmark 1: /tmp/before native.php Time (mean ± σ): 765.9 ms ± 7.9 ms [User: 761.1 ms, System: 4.4 ms] Range (min … max): 753.2 ms … 774.7 ms 10 runs Benchmark 2: /tmp/before userland.php Time (mean ± σ): 588.0 ms ± 17.9 ms [User: 583.6 ms, System: 4.1 ms] Range (min … max): 576.0 ms … 633.3 ms 10 runs Summary /tmp/before userland.php ran 1.30 ± 0.04 times faster than /tmp/before native.php Running `native.php` with perf reports that a third of the time is spent in `zend_call_function()` and another 20% in `execute_ex()`, however `php_array_find()` comes next at 14%: # Samples: 3K of event 'cpu_core/cycles/' # Event count (approx.): 2895247444 # # Overhead Command Shared Object Symbol # ........ ....... ................. ........................................... # 32.47% before before [.] zend_call_function 20.63% before before [.] execute_ex 14.06% before before [.] php_array_find 7.89% before before [.] ZEND_IS_IDENTICAL_SPEC_CV_CONST_HANDLER 7.31% before before [.] zend_init_func_execute_data 6.50% before before [.] zend_copy_extra_args which was surprising, because the function doesn’t too all that much. Looking at the implementation, the refcounting stood out and it turns out that it is not actually necessary. The `array` is passed by value to `array_find()` and thus cannot magically change within the callback. This also means that the array will continue to hold a reference to string keys and values, preventing these values from being collected. The refcounting inside of `php_array_find()` thus will never do anything useful. Comparing the updated implementation against the original implementation shows that this change results in a 1.14× improvement: Benchmark 1: /tmp/before native.php Time (mean ± σ): 775.4 ms ± 29.6 ms [User: 771.6 ms, System: 3.5 ms] Range (min … max): 740.2 ms … 844.4 ms 10 runs Benchmark 2: /tmp/after native.php Time (mean ± σ): 677.3 ms ± 16.7 ms [User: 673.9 ms, System: 3.1 ms] Range (min … max): 655.9 ms … 705.0 ms 10 runs Summary /tmp/after native.php ran 1.14 ± 0.05 times faster than /tmp/before native.php Comparing the native implementation against the userland implementation with the new implementation shows that while the native implementation still is slower, the difference reduced to 15% (down from 30%): Benchmark 1: /tmp/after native.php Time (mean ± σ): 670.4 ms ± 9.3 ms [User: 666.7 ms, System: 3.4 ms] Range (min … max): 657.1 ms … 689.0 ms 10 runs Benchmark 2: /tmp/after userland.php Time (mean ± σ): 576.7 ms ± 7.6 ms [User: 572.5 ms, System: 3.7 ms] Range (min … max): 563.9 ms … 588.1 ms 10 runs Summary /tmp/after userland.php ran 1.16 ± 0.02 times faster than /tmp/after native.php Looking at the updated perf results shows that `php_array_find()` now only takes up 8% of the time: # Samples: 2K of event 'cpu_core/cycles/' # Event count (approx.): 2540947159 # # Overhead Command Shared Object Symbol # ........ ....... .................... ........................................... # 34.77% after after [.] zend_call_function 18.57% after after [.] execute_ex 12.28% after after [.] zend_copy_extra_args 10.91% after after [.] zend_init_func_execute_data 8.77% after after [.] php_array_find 6.70% after after [.] ZEND_IS_IDENTICAL_SPEC_CV_CONST_HANDLER 4.68% after after [.] zend_is_identical [1] https://www.linkedin.com/posts/bohuslav-%C5%A1imek-kambo_the-surprising-performance-of-php-84-activity-7287044532280414209-6WnA --- ext/standard/array.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index dbd917028f793..5d3b998bc6b40 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6622,10 +6622,10 @@ static zend_result php_array_find(const HashTable *array, zend_fcall_info fci, z if (!str_key) { ZVAL_LONG(&args[1], num_key); } else { - ZVAL_STR_COPY(&args[1], str_key); + ZVAL_STR(&args[1], str_key); } - ZVAL_COPY(&args[0], operand); + ZVAL_COPY_VALUE(&args[0], operand); zend_result result = zend_call_function(&fci, &fci_cache); if (EXPECTED(result == SUCCESS)) { @@ -6646,16 +6646,10 @@ static zend_result php_array_find(const HashTable *array, zend_fcall_info fci, z ZVAL_COPY(result_key, &args[1]); } - zval_ptr_dtor(&args[0]); - zval_ptr_dtor(&args[1]); - return SUCCESS; } } - zval_ptr_dtor(&args[0]); - zval_ptr_dtor(&args[1]); - if (UNEXPECTED(result != SUCCESS)) { return FAILURE; } From 96673275177a75f59a25937aa4da9ac1cbe437af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 21 Jan 2025 10:20:40 +0100 Subject: [PATCH 3/3] array_find: Clean up exception handling This change has no effect on performance, but greatly improves readability of the implementation. --- ext/standard/array.c | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 5d3b998bc6b40..add6c6b2c5a7e 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6628,30 +6628,28 @@ static zend_result php_array_find(const HashTable *array, zend_fcall_info fci, z ZVAL_COPY_VALUE(&args[0], operand); zend_result result = zend_call_function(&fci, &fci_cache); - if (EXPECTED(result == SUCCESS)) { - bool retval_true; + ZEND_ASSERT(result == SUCCESS); - retval_true = zend_is_true(&retval); - zval_ptr_dtor(&retval); + if (UNEXPECTED(EG(exception))) { + return FAILURE; + } - /* This negates the condition, if negate_condition is true. Otherwise it does nothing with `retval_true`. */ - retval_true ^= negate_condition; + bool retval_true = zend_is_true(&retval); + zval_ptr_dtor(&retval); - if (retval_true) { - if (result_value != NULL) { - ZVAL_COPY_DEREF(result_value, &args[0]); - } + /* This negates the condition, if negate_condition is true. Otherwise it does nothing with `retval_true`. */ + retval_true ^= negate_condition; - if (result_key != NULL) { - ZVAL_COPY(result_key, &args[1]); - } + if (retval_true) { + if (result_value != NULL) { + ZVAL_COPY_DEREF(result_value, &args[0]); + } - return SUCCESS; + if (result_key != NULL) { + ZVAL_COPY(result_key, &args[1]); } - } - if (UNEXPECTED(result != SUCCESS)) { - return FAILURE; + break; } } ZEND_HASH_FOREACH_END();