Skip to content

Commit fce40a5

Browse files
[ext/standard] Specialize array_sum()/array_product() for packed long arrays
The per-element cost of array_sum()/array_product() is dominated by the add_function/mul_function dispatch. The HashTable iteration itself is already well-tuned via ZEND_HASH_FOREACH_VAL's branchless size computation. Split php_array_binop() into packed and hash paths, and within the packed path add a specialized IS_LONG fast path that uses inlined overflow-aware arithmetic (fast_long_add_function and ZEND_SIGNED_MULTIPLY_LONG). Falls through to the generic helper on overflow, non-LONG types, objects, resources, or strings, so the PHP 8.3 warning behavior, operator overloading and BC casts for resources/strings are preserved. This follows the pattern in the recent array function optimizations (PR #18158 array_map, PR #18157 array_find, PR #18180 array_reduce), and was suggested in TimWolla's review of PR #18180. Benchmarks (packed IS_LONG, -O2, median of 7 runs, Apple M1): array_sum(range(1, N)) ~3.2x (N >= 1000) array_product(small values) ~7.5x array_product(range(1, N)) ~1.4x (N=100), neutral otherwise packed_float / hash / mixed neutral (within noise) 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.
1 parent 1195f27 commit fce40a5

File tree

5 files changed

+156
-35
lines changed

5 files changed

+156
-35
lines changed

UPGRADING

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ PHP 8.6 UPGRADE NOTES
292292
- Standard:
293293
. Improved performance of array_fill_keys().
294294
. Improved performance of array_map() with multiple arrays passed.
295+
. Improved performance of array_sum() and array_product() for packed
296+
integer arrays.
295297
. Improved performance of array_unshift().
296298
. Improved performance of array_walk().
297299
. Improved performance of intval('+0b...', 2) and intval('0b...', 2).

ext/standard/array.c

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6314,11 +6314,50 @@ PHP_FUNCTION(array_rand)
63146314
}
63156315
/* }}} */
63166316

6317+
/* Apply a single array_sum/array_product step to return_value. */
6318+
static zend_always_inline void php_array_binop_apply(
6319+
zval *return_value, zval *entry, const char *op_name, binary_op_type op)
6320+
{
6321+
/* For objects we try to cast them to a numeric type */
6322+
if (Z_TYPE_P(entry) == IS_OBJECT) {
6323+
zval dst;
6324+
zend_result status = Z_OBJ_HT_P(entry)->cast_object(Z_OBJ_P(entry), &dst, _IS_NUMBER);
6325+
6326+
/* Do not type error for BC */
6327+
if (status == FAILURE || (Z_TYPE(dst) != IS_LONG && Z_TYPE(dst) != IS_DOUBLE)) {
6328+
php_error_docref(NULL, E_WARNING, "%s is not supported on type %s",
6329+
op_name, zend_zval_type_name(entry));
6330+
return;
6331+
}
6332+
op(return_value, return_value, &dst);
6333+
return;
6334+
}
6335+
6336+
zend_result status = op(return_value, return_value, entry);
6337+
if (status == FAILURE) {
6338+
ZEND_ASSERT(EG(exception));
6339+
zend_clear_exception();
6340+
/* BC resources: previously resources were cast to int */
6341+
if (Z_TYPE_P(entry) == IS_RESOURCE) {
6342+
zval tmp;
6343+
ZVAL_LONG(&tmp, Z_RES_HANDLE_P(entry));
6344+
op(return_value, return_value, &tmp);
6345+
}
6346+
/* BC non numeric strings: previously were cast to 0 */
6347+
else if (Z_TYPE_P(entry) == IS_STRING) {
6348+
zval tmp;
6349+
ZVAL_LONG(&tmp, 0);
6350+
op(return_value, return_value, &tmp);
6351+
}
6352+
php_error_docref(NULL, E_WARNING, "%s is not supported on type %s",
6353+
op_name, zend_zval_type_name(entry));
6354+
}
6355+
}
6356+
63176357
/* Wrapper for array_sum and array_product */
63186358
static void php_array_binop(INTERNAL_FUNCTION_PARAMETERS, const char *op_name, binary_op_type op, zend_long initial)
63196359
{
63206360
HashTable *input;
6321-
zval *entry;
63226361

63236362
ZEND_PARSE_PARAMETERS_START(1, 1)
63246363
Z_PARAM_ARRAY_HT(input)
@@ -6329,42 +6368,46 @@ static void php_array_binop(INTERNAL_FUNCTION_PARAMETERS, const char *op_name, b
63296368
}
63306369

63316370
ZVAL_LONG(return_value, initial);
6332-
ZEND_HASH_FOREACH_VAL(input, entry) {
6333-
/* For objects we try to cast them to a numeric type */
6334-
if (Z_TYPE_P(entry) == IS_OBJECT) {
6335-
zval dst;
6336-
zend_result status = Z_OBJ_HT_P(entry)->cast_object(Z_OBJ_P(entry), &dst, _IS_NUMBER);
6337-
6338-
/* Do not type error for BC */
6339-
if (status == FAILURE || (Z_TYPE(dst) != IS_LONG && Z_TYPE(dst) != IS_DOUBLE)) {
6340-
php_error_docref(NULL, E_WARNING, "%s is not supported on type %s",
6341-
op_name, zend_zval_type_name(entry));
6342-
continue;
6343-
}
6344-
op(return_value, return_value, &dst);
6345-
continue;
6346-
}
63476371

6348-
zend_result status = op(return_value, return_value, entry);
6349-
if (status == FAILURE) {
6350-
ZEND_ASSERT(EG(exception));
6351-
zend_clear_exception();
6352-
/* BC resources: previously resources were cast to int */
6353-
if (Z_TYPE_P(entry) == IS_RESOURCE) {
6354-
zval tmp;
6355-
ZVAL_LONG(&tmp, Z_RES_HANDLE_P(entry));
6356-
op(return_value, return_value, &tmp);
6357-
}
6358-
/* BC non numeric strings: previously were cast to 0 */
6359-
else if (Z_TYPE_P(entry) == IS_STRING) {
6360-
zval tmp;
6361-
ZVAL_LONG(&tmp, 0);
6362-
op(return_value, return_value, &tmp);
6363-
}
6364-
php_error_docref(NULL, E_WARNING, "%s is not supported on type %s",
6365-
op_name, zend_zval_type_name(entry));
6372+
if (HT_IS_PACKED(input)) {
6373+
zval *entry;
6374+
/* Specialized fast path for IS_LONG values using inlined overflow-aware arithmetic. */
6375+
if (op == add_function) {
6376+
ZEND_HASH_PACKED_FOREACH_VAL(input, entry) {
6377+
if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) {
6378+
fast_long_add_function(return_value, return_value, entry);
6379+
continue;
6380+
}
6381+
php_array_binop_apply(return_value, entry, op_name, op);
6382+
} ZEND_HASH_FOREACH_END();
6383+
} else if (op == mul_function) {
6384+
ZEND_HASH_PACKED_FOREACH_VAL(input, entry) {
6385+
if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) {
6386+
zend_long lval;
6387+
double dval;
6388+
int overflow;
6389+
ZEND_SIGNED_MULTIPLY_LONG(Z_LVAL_P(return_value), Z_LVAL_P(entry), lval, dval, overflow);
6390+
if (UNEXPECTED(overflow)) {
6391+
ZVAL_DOUBLE(return_value, dval);
6392+
} else {
6393+
Z_LVAL_P(return_value) = lval;
6394+
}
6395+
continue;
6396+
}
6397+
php_array_binop_apply(return_value, entry, op_name, op);
6398+
} ZEND_HASH_FOREACH_END();
6399+
} else {
6400+
ZEND_HASH_PACKED_FOREACH_VAL(input, entry) {
6401+
php_array_binop_apply(return_value, entry, op_name, op);
6402+
} ZEND_HASH_FOREACH_END();
63666403
}
6367-
} ZEND_HASH_FOREACH_END();
6404+
} else {
6405+
/* Hash path: iterate over Bucket entries. */
6406+
zval *entry;
6407+
ZEND_HASH_MAP_FOREACH_VAL(input, entry) {
6408+
php_array_binop_apply(return_value, entry, op_name, op);
6409+
} ZEND_HASH_FOREACH_END();
6410+
}
63686411
}
63696412

63706413
/* {{{ Returns the sum of the array entries */
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
array_product() packed long overflow continues in double mode
3+
--FILE--
4+
<?php
5+
6+
$tests = [
7+
[[PHP_INT_MAX, 2, 3], ((float) PHP_INT_MAX * 2) * 3],
8+
[[PHP_INT_MIN, -1, 2], ((float) PHP_INT_MIN * -1) * 2],
9+
];
10+
11+
foreach ($tests as [$input, $expected]) {
12+
$result = array_product($input);
13+
var_dump(is_float($result));
14+
var_dump($result === $expected);
15+
}
16+
17+
?>
18+
--EXPECT--
19+
bool(true)
20+
bool(true)
21+
bool(true)
22+
bool(true)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
array_sum() packed long overflow continues in double mode
3+
--FILE--
4+
<?php
5+
6+
$tests = [
7+
[[PHP_INT_MAX, 1, 4096], ((float) PHP_INT_MAX + 1) + 4096],
8+
[[PHP_INT_MIN, -1, -4096], ((float) PHP_INT_MIN - 1) - 4096],
9+
];
10+
11+
foreach ($tests as [$input, $expected]) {
12+
$result = array_sum($input);
13+
var_dump(is_float($result));
14+
var_dump($result === $expected);
15+
}
16+
17+
?>
18+
--EXPECT--
19+
bool(true)
20+
bool(true)
21+
bool(true)
22+
bool(true)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
array_sum()/array_product(): PHP 8.3 nested-array warning and array_column() integration
3+
--FILE--
4+
<?php
5+
6+
echo "-- array_column() + array_sum()/array_product() integration --\n";
7+
$products = [
8+
['name' => 'Pen', 'price' => 3],
9+
['name' => 'Paper', 'price' => 5],
10+
];
11+
$prices = array_column($products, 'price');
12+
var_dump($prices === [3, 5]);
13+
var_dump(array_sum($prices) === 8);
14+
var_dump(array_product($prices) === 15);
15+
16+
echo "-- PHP 8.3: nested array emits warning and is skipped --\n";
17+
var_dump(array_sum([1, [2], 3]));
18+
var_dump(array_product([2, [3], 4]));
19+
20+
?>
21+
--EXPECTF--
22+
-- array_column() + array_sum()/array_product() integration --
23+
bool(true)
24+
bool(true)
25+
bool(true)
26+
-- PHP 8.3: nested array emits warning and is skipped --
27+
28+
Warning: array_sum(): Addition is not supported on type array in %s on line %d
29+
int(4)
30+
31+
Warning: array_product(): Multiplication is not supported on type array in %s on line %d
32+
int(8)

0 commit comments

Comments
 (0)