From 4a23f9a676a1dc7daac423ef9cb7d0407b0c5a5b Mon Sep 17 00:00:00 2001 From: mehmetcansahin Date: Fri, 22 May 2026 12:06:04 +0300 Subject: [PATCH 1/3] Specialize min and max for long arrays --- ext/standard/array.c | 72 ++++++++++++++++++- .../array/min_max_array_long_fast_path.phpt | 44 ++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/array/min_max_array_long_fast_path.phpt diff --git a/ext/standard/array.c b/ext/standard/array.c index a233e6c4dcb5..14a48e803120 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -1109,6 +1109,64 @@ static zval *php_array_data_minmax(HashTable *array, bool max) /* {{{ */ } /* }}} */ +static zend_always_inline bool php_array_minmax_long(HashTable *array, zval *return_value, bool max) /* {{{ */ +{ + zval *entry, *result = NULL; + zend_long result_lval; + bool long_mode = true; + + ZEND_HASH_FOREACH_VAL(array, entry) { + zval *value = entry; + zend_long value_lval; + + ZVAL_DEREF(value); + if (!result) { + if (Z_TYPE_P(value) != IS_LONG) { + return false; + } + + result = value; + result_lval = Z_LVAL_P(value); + continue; + } + + if (long_mode && EXPECTED(Z_TYPE_P(value) == IS_LONG)) { + value_lval = Z_LVAL_P(value); + if (max) { + if (result_lval < value_lval) { + result = value; + result_lval = value_lval; + } + } else { + if (result_lval > value_lval) { + result = value; + result_lval = value_lval; + } + } + continue; + } + + long_mode = false; + + int cmp = zend_compare(result, value); + if (max ? cmp < 0 : cmp > 0) { + result = value; + } + } ZEND_HASH_FOREACH_END(); + + if (!result) { + return false; + } + + if (long_mode) { + ZVAL_LONG(return_value, result_lval); + } else { + ZVAL_COPY_DEREF(return_value, result); + } + return true; +} +/* }}} */ + /* {{{ * proto mixed min(array values) * proto mixed min(mixed arg1 [, mixed arg2 [, mixed ...]]) @@ -1128,7 +1186,12 @@ PHP_FUNCTION(min) zend_wrong_parameter_type_error(1, Z_EXPECTED_ARRAY, &args[0]); RETURN_THROWS(); } else { - zval *result = php_array_data_minmax(Z_ARRVAL(args[0]), false); + HashTable *array = Z_ARRVAL(args[0]); + if (php_array_minmax_long(array, return_value, false)) { + return; + } + + zval *result = php_array_data_minmax(array, false); if (result) { RETURN_COPY_DEREF(result); } else { @@ -1256,7 +1319,12 @@ PHP_FUNCTION(max) zend_wrong_parameter_type_error(1, Z_EXPECTED_ARRAY, &args[0]); RETURN_THROWS(); } else { - zval *result = php_array_data_minmax(Z_ARRVAL(args[0]), true); + HashTable *array = Z_ARRVAL(args[0]); + if (php_array_minmax_long(array, return_value, true)) { + return; + } + + zval *result = php_array_data_minmax(array, true); if (result) { RETURN_COPY_DEREF(result); } else { diff --git a/ext/standard/tests/array/min_max_array_long_fast_path.phpt b/ext/standard/tests/array/min_max_array_long_fast_path.phpt new file mode 100644 index 000000000000..26ace9bad0df --- /dev/null +++ b/ext/standard/tests/array/min_max_array_long_fast_path.phpt @@ -0,0 +1,44 @@ +--TEST-- +min() and max() array long fast path preserves comparison behavior +--FILE-- + $values) { + echo "-- $name --\n"; + var_dump(min($values)); + var_dump(max($values)); +} + +?> +--EXPECT-- +-- packed -- +int(-3) +int(7) +-- sparse -- +int(-2) +int(7) +-- refs -- +int(3) +int(9) +-- first non-long -- +int(3) +string(1) "5" +-- fallback after longs -- +int(2) +int(5) From 6f9b2d222b5f28b71d355d100b1ed06925ff1a48 Mon Sep 17 00:00:00 2001 From: mehmetcansahin Date: Fri, 22 May 2026 16:07:33 +0300 Subject: [PATCH 2/3] Fix min/max long fast path warning --- ext/standard/array.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 14a48e803120..62ce46105560 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -1113,7 +1113,7 @@ static zend_always_inline bool php_array_minmax_long(HashTable *array, zval *ret { zval *entry, *result = NULL; zend_long result_lval; - bool long_mode = true; + bool long_mode = false; ZEND_HASH_FOREACH_VAL(array, entry) { zval *value = entry; @@ -1127,6 +1127,7 @@ static zend_always_inline bool php_array_minmax_long(HashTable *array, zval *ret result = value; result_lval = Z_LVAL_P(value); + long_mode = true; continue; } From d9e94d0395eb29dce45e19e21347e2429d4bca2d Mon Sep 17 00:00:00 2001 From: mehmetcansahin Date: Fri, 22 May 2026 18:38:37 +0300 Subject: [PATCH 3/3] Extend min/max array fast path to doubles --- UPGRADING | 2 + ext/standard/array.c | 67 ++++++----- .../tests/array/min_max_array_fast_path.phpt | 107 ++++++++++++++++++ .../array/min_max_array_long_fast_path.phpt | 44 ------- 4 files changed, 150 insertions(+), 70 deletions(-) create mode 100644 ext/standard/tests/array/min_max_array_fast_path.phpt delete mode 100644 ext/standard/tests/array/min_max_array_long_fast_path.phpt diff --git a/UPGRADING b/UPGRADING index 1c75894512f9..379c389547c8 100644 --- a/UPGRADING +++ b/UPGRADING @@ -519,6 +519,8 @@ PHP 8.6 UPGRADE NOTES . Improved performance of array_map() with multiple arrays passed. . Improved performance of array_sum() and array_product() for integer-only arrays. + . Improved performance of min() and max() for integer-only and float-only + arrays. . Improved performance of array_unshift(). . Improved performance of array_walk(). . Improved performance of intval('+0b...', 2) and intval('0b...', 2). diff --git a/ext/standard/array.c b/ext/standard/array.c index 62ce46105560..832958f86dd0 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -1109,45 +1109,58 @@ static zval *php_array_data_minmax(HashTable *array, bool max) /* {{{ */ } /* }}} */ -static zend_always_inline bool php_array_minmax_long(HashTable *array, zval *return_value, bool max) /* {{{ */ +static zend_always_inline bool php_array_minmax(HashTable *array, zval *return_value, bool max) /* {{{ */ { zval *entry, *result = NULL; - zend_long result_lval; - bool long_mode = false; + zend_long result_lval = 0; + double result_dval = 0.0; + /* IS_LONG or IS_DOUBLE while every value scanned so far has that exact type, + * IS_UNDEF once a value forces the generic zend_compare() fallback. */ + uint8_t fast_type = IS_UNDEF; ZEND_HASH_FOREACH_VAL(array, entry) { zval *value = entry; - zend_long value_lval; ZVAL_DEREF(value); - if (!result) { - if (Z_TYPE_P(value) != IS_LONG) { - return false; - } - result = value; - result_lval = Z_LVAL_P(value); - long_mode = true; + if (fast_type == IS_LONG && EXPECTED(Z_TYPE_P(value) == IS_LONG)) { + zend_long value_lval = Z_LVAL_P(value); + if (max ? result_lval < value_lval : result_lval > value_lval) { + result = value; + result_lval = value_lval; + } continue; } - if (long_mode && EXPECTED(Z_TYPE_P(value) == IS_LONG)) { - value_lval = Z_LVAL_P(value); - if (max) { - if (result_lval < value_lval) { + if (fast_type == IS_DOUBLE && EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) { + double value_dval = Z_DVAL_P(value); + /* NaN ordering differs from zend_compare(); hand it to the fallback. */ + if (EXPECTED(!zend_isnan(value_dval))) { + if (max ? result_dval < value_dval : result_dval > value_dval) { result = value; - result_lval = value_lval; - } - } else { - if (result_lval > value_lval) { - result = value; - result_lval = value_lval; + result_dval = value_dval; } + continue; } - continue; } - long_mode = false; + if (!result) { + if (Z_TYPE_P(value) == IS_LONG) { + result = value; + result_lval = Z_LVAL_P(value); + fast_type = IS_LONG; + continue; + } + if (Z_TYPE_P(value) == IS_DOUBLE && !zend_isnan(Z_DVAL_P(value))) { + result = value; + result_dval = Z_DVAL_P(value); + fast_type = IS_DOUBLE; + continue; + } + return false; + } + + fast_type = IS_UNDEF; int cmp = zend_compare(result, value); if (max ? cmp < 0 : cmp > 0) { @@ -1159,8 +1172,10 @@ static zend_always_inline bool php_array_minmax_long(HashTable *array, zval *ret return false; } - if (long_mode) { + if (fast_type == IS_LONG) { ZVAL_LONG(return_value, result_lval); + } else if (fast_type == IS_DOUBLE) { + ZVAL_DOUBLE(return_value, result_dval); } else { ZVAL_COPY_DEREF(return_value, result); } @@ -1188,7 +1203,7 @@ PHP_FUNCTION(min) RETURN_THROWS(); } else { HashTable *array = Z_ARRVAL(args[0]); - if (php_array_minmax_long(array, return_value, false)) { + if (php_array_minmax(array, return_value, false)) { return; } @@ -1321,7 +1336,7 @@ PHP_FUNCTION(max) RETURN_THROWS(); } else { HashTable *array = Z_ARRVAL(args[0]); - if (php_array_minmax_long(array, return_value, true)) { + if (php_array_minmax(array, return_value, true)) { return; } diff --git a/ext/standard/tests/array/min_max_array_fast_path.phpt b/ext/standard/tests/array/min_max_array_fast_path.phpt new file mode 100644 index 000000000000..c9b613911c55 --- /dev/null +++ b/ext/standard/tests/array/min_max_array_fast_path.phpt @@ -0,0 +1,107 @@ +--TEST-- +min() and max() array long/double fast path preserves comparison behavior +--FILE-- + $values) { + echo "-- $name --\n"; + var_dump(min($values)); + var_dump(max($values)); +} + +?> +--EXPECT-- +-- packed long -- +int(-3) +int(7) +-- sparse long -- +int(-2) +int(7) +-- long refs -- +int(3) +int(9) +-- packed double -- +float(-3.5) +float(7.5) +-- sparse double -- +float(-2.5) +float(7.5) +-- double refs -- +float(3.5) +float(9.5) +-- double equal -- +float(2.5) +float(2.5) +-- nan first -- +float(1) +float(NAN) +-- nan middle -- +float(1) +float(3) +-- nan last -- +float(NAN) +float(3) +-- inf -- +float(-INF) +float(INF) +-- single long -- +int(7) +int(7) +-- single double -- +float(7.5) +float(7.5) +-- first non-long -- +int(3) +string(1) "5" +-- fallback after longs -- +int(2) +int(5) +-- fallback after doubles -- +float(2.5) +float(5.5) +-- long then double -- +float(2.5) +int(9) +-- double then long -- +int(2) +int(9) diff --git a/ext/standard/tests/array/min_max_array_long_fast_path.phpt b/ext/standard/tests/array/min_max_array_long_fast_path.phpt deleted file mode 100644 index 26ace9bad0df..000000000000 --- a/ext/standard/tests/array/min_max_array_long_fast_path.phpt +++ /dev/null @@ -1,44 +0,0 @@ ---TEST-- -min() and max() array long fast path preserves comparison behavior ---FILE-- - $values) { - echo "-- $name --\n"; - var_dump(min($values)); - var_dump(max($values)); -} - -?> ---EXPECT-- --- packed -- -int(-3) -int(7) --- sparse -- -int(-2) -int(7) --- refs -- -int(3) -int(9) --- first non-long -- -int(3) -string(1) "5" --- fallback after longs -- -int(2) -int(5)