Commit a7915341e50 for php.net
commit a7915341e50c58b6bdc6fc7575f802c203ab1a97
Author: mehmetcan <2010841+mehmetcansahin@users.noreply.github.com>
Date: Mon May 18 14:21:21 2026 +0300
[ext/standard] Specialize array_sum()/array_product() for long arrays (#21787)
* [ext/standard] Specialize array_sum()/array_product() for long arrays
The per-element cost of array_sum() and array_product() is dominated by
the add_function/mul_function call dispatch. Add a specialized fast
path for the IS_LONG + IS_LONG case that inlines overflow-aware
arithmetic via fast_long_add_function() and ZEND_SIGNED_MULTIPLY_LONG()
-- the same engine helpers that add_function_fast() dispatches to
internally. The fast path applies to both packed and hash arrays.
On overflow, non-IS_LONG entries, objects, resources or strings,
execution falls through to the generic path in php_array_binop_apply(),
so the PHP 8.3 warning behavior, operator overloading and BC casts for
resources/non-numeric strings are preserved.
Benchmarks (Apple M1, -O2 -DNDEBUG release, median of 7 runs, n=10000
with ~20M total elements per case):
array_sum, packed long 42.24 ms -> 12.99 ms 3.25x
array_product, packed small long (0..9) 144.63 ms -> 19.39 ms 7.46x
array_product, packed range(1, 100) 82.94 ms -> 58.66 ms 1.41x
array_sum, hash long 41.95 ms -> 12.90 ms 3.25x
array_product, hash small long (0..9) 69.90 ms -> 19.40 ms 3.60x
packed/hash float, mixed IS_LONG/IS_DOUBLE ~ ~ ~1.00x
Tests cover the overflow transition from fast path to generic, and
the integration with array_column() together with the PHP 8.3
nested-array warning behavior.
* [ext/standard] Simplify array_sum/product iteration
* [ext/standard] Force inline array binop wrapper
diff --git a/UPGRADING b/UPGRADING
index de086c600f5..44d6981acbf 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -380,6 +380,8 @@ PHP 8.6 UPGRADE NOTES
- Standard:
. Improved performance of array_fill_keys().
. Improved performance of array_map() with multiple arrays passed.
+ . Improved performance of array_sum() and array_product() for
+ integer-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 49a3bbd557b..25259c47d61 100644
--- a/ext/standard/array.c
+++ b/ext/standard/array.c
@@ -6314,11 +6314,50 @@ PHP_FUNCTION(array_rand)
}
/* }}} */
+/* Apply a single array_sum/array_product step to return_value. */
+static zend_always_inline void php_array_binop_apply(
+ zval *return_value, zval *entry, const char *op_name, binary_op_type op)
+{
+ /* For objects we try to cast them to a numeric type */
+ if (Z_TYPE_P(entry) == IS_OBJECT) {
+ zval dst;
+ zend_result status = Z_OBJ_HT_P(entry)->cast_object(Z_OBJ_P(entry), &dst, _IS_NUMBER);
+
+ /* Do not type error for BC */
+ if (status == FAILURE || (Z_TYPE(dst) != IS_LONG && Z_TYPE(dst) != IS_DOUBLE)) {
+ php_error_docref(NULL, E_WARNING, "%s is not supported on type %s",
+ op_name, zend_zval_type_name(entry));
+ return;
+ }
+ op(return_value, return_value, &dst);
+ return;
+ }
+
+ zend_result status = op(return_value, return_value, entry);
+ if (status == FAILURE) {
+ ZEND_ASSERT(EG(exception));
+ zend_clear_exception();
+ /* BC resources: previously resources were cast to int */
+ if (Z_TYPE_P(entry) == IS_RESOURCE) {
+ zval tmp;
+ ZVAL_LONG(&tmp, Z_RES_HANDLE_P(entry));
+ op(return_value, return_value, &tmp);
+ }
+ /* BC non numeric strings: previously were cast to 0 */
+ else if (Z_TYPE_P(entry) == IS_STRING) {
+ zval tmp;
+ ZVAL_LONG(&tmp, 0);
+ op(return_value, return_value, &tmp);
+ }
+ php_error_docref(NULL, E_WARNING, "%s is not supported on type %s",
+ op_name, zend_zval_type_name(entry));
+ }
+}
+
/* Wrapper for array_sum and array_product */
-static void php_array_binop(INTERNAL_FUNCTION_PARAMETERS, const char *op_name, binary_op_type op, zend_long initial)
+static zend_always_inline void php_array_binop(INTERNAL_FUNCTION_PARAMETERS, const char *op_name, binary_op_type op, zend_long initial)
{
HashTable *input;
- zval *entry;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_HT(input)
@@ -6329,42 +6368,39 @@ static void php_array_binop(INTERNAL_FUNCTION_PARAMETERS, const char *op_name, b
}
ZVAL_LONG(return_value, initial);
- ZEND_HASH_FOREACH_VAL(input, entry) {
- /* For objects we try to cast them to a numeric type */
- if (Z_TYPE_P(entry) == IS_OBJECT) {
- zval dst;
- zend_result status = Z_OBJ_HT_P(entry)->cast_object(Z_OBJ_P(entry), &dst, _IS_NUMBER);
-
- /* Do not type error for BC */
- if (status == FAILURE || (Z_TYPE(dst) != IS_LONG && Z_TYPE(dst) != IS_DOUBLE)) {
- php_error_docref(NULL, E_WARNING, "%s is not supported on type %s",
- op_name, zend_zval_type_name(entry));
- continue;
- }
- op(return_value, return_value, &dst);
- continue;
- }
- zend_result status = op(return_value, return_value, entry);
- if (status == FAILURE) {
- ZEND_ASSERT(EG(exception));
- zend_clear_exception();
- /* BC resources: previously resources were cast to int */
- if (Z_TYPE_P(entry) == IS_RESOURCE) {
- zval tmp;
- ZVAL_LONG(&tmp, Z_RES_HANDLE_P(entry));
- op(return_value, return_value, &tmp);
+ if (op == add_function) {
+ zval *entry;
+ ZEND_HASH_FOREACH_VAL(input, entry) {
+ if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) {
+ fast_long_add_function(return_value, return_value, entry);
+ continue;
}
- /* BC non numeric strings: previously were cast to 0 */
- else if (Z_TYPE_P(entry) == IS_STRING) {
- zval tmp;
- ZVAL_LONG(&tmp, 0);
- op(return_value, return_value, &tmp);
+ php_array_binop_apply(return_value, entry, op_name, op);
+ } ZEND_HASH_FOREACH_END();
+ } else if (op == mul_function) {
+ zval *entry;
+ ZEND_HASH_FOREACH_VAL(input, entry) {
+ if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) {
+ zend_long lval;
+ double dval;
+ int overflow;
+ ZEND_SIGNED_MULTIPLY_LONG(Z_LVAL_P(return_value), Z_LVAL_P(entry), lval, dval, overflow);
+ if (UNEXPECTED(overflow)) {
+ ZVAL_DOUBLE(return_value, dval);
+ } else {
+ Z_LVAL_P(return_value) = lval;
+ }
+ continue;
}
- php_error_docref(NULL, E_WARNING, "%s is not supported on type %s",
- op_name, zend_zval_type_name(entry));
- }
- } ZEND_HASH_FOREACH_END();
+ php_array_binop_apply(return_value, entry, op_name, op);
+ } ZEND_HASH_FOREACH_END();
+ } else {
+ zval *entry;
+ ZEND_HASH_FOREACH_VAL(input, entry) {
+ php_array_binop_apply(return_value, entry, op_name, op);
+ } ZEND_HASH_FOREACH_END();
+ }
}
/* {{{ Returns the sum of the array entries */
diff --git a/ext/standard/tests/array/array_product_packed_long_overflow.phpt b/ext/standard/tests/array/array_product_packed_long_overflow.phpt
new file mode 100644
index 00000000000..7c1d8a0adeb
--- /dev/null
+++ b/ext/standard/tests/array/array_product_packed_long_overflow.phpt
@@ -0,0 +1,22 @@
+--TEST--
+array_product() packed long overflow continues in double mode
+--FILE--
+<?php
+
+$tests = [
+ [[PHP_INT_MAX, 2, 3], ((float) PHP_INT_MAX * 2) * 3],
+ [[PHP_INT_MIN, -1, 2], ((float) PHP_INT_MIN * -1) * 2],
+];
+
+foreach ($tests as [$input, $expected]) {
+ $result = array_product($input);
+ var_dump(is_float($result));
+ var_dump($result === $expected);
+}
+
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/standard/tests/array/array_sum_packed_long_overflow.phpt b/ext/standard/tests/array/array_sum_packed_long_overflow.phpt
new file mode 100644
index 00000000000..22e4f3be52a
--- /dev/null
+++ b/ext/standard/tests/array/array_sum_packed_long_overflow.phpt
@@ -0,0 +1,22 @@
+--TEST--
+array_sum() packed long overflow continues in double mode
+--FILE--
+<?php
+
+$tests = [
+ [[PHP_INT_MAX, 1, 4096], ((float) PHP_INT_MAX + 1) + 4096],
+ [[PHP_INT_MIN, -1, -4096], ((float) PHP_INT_MIN - 1) - 4096],
+];
+
+foreach ($tests as [$input, $expected]) {
+ $result = array_sum($input);
+ var_dump(is_float($result));
+ var_dump($result === $expected);
+}
+
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/standard/tests/array/array_sum_product_integration.phpt b/ext/standard/tests/array/array_sum_product_integration.phpt
new file mode 100644
index 00000000000..4b83042a3b4
--- /dev/null
+++ b/ext/standard/tests/array/array_sum_product_integration.phpt
@@ -0,0 +1,32 @@
+--TEST--
+array_sum()/array_product(): PHP 8.3 nested-array warning and array_column() integration
+--FILE--
+<?php
+
+echo "-- array_column() + array_sum()/array_product() integration --\n";
+$products = [
+ ['name' => 'Pen', 'price' => 3],
+ ['name' => 'Paper', 'price' => 5],
+];
+$prices = array_column($products, 'price');
+var_dump($prices === [3, 5]);
+var_dump(array_sum($prices) === 8);
+var_dump(array_product($prices) === 15);
+
+echo "-- PHP 8.3: nested array emits warning and is skipped --\n";
+var_dump(array_sum([1, [2], 3]));
+var_dump(array_product([2, [3], 4]));
+
+?>
+--EXPECTF--
+-- array_column() + array_sum()/array_product() integration --
+bool(true)
+bool(true)
+bool(true)
+-- PHP 8.3: nested array emits warning and is skipped --
+
+Warning: array_sum(): Addition is not supported on type array in %s on line %d
+int(4)
+
+Warning: array_product(): Multiplication is not supported on type array in %s on line %d
+int(8)