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)