Skip to content

Commit c371d94

Browse files
[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.
1 parent 1195f27 commit c371d94

File tree

5 files changed

+183
-35
lines changed

5 files changed

+183
-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
296+
integer-only 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: 105 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,73 @@ 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 (op == add_function) {
6373+
zval *entry;
6374+
if (HT_IS_PACKED(input)) {
6375+
ZEND_HASH_PACKED_FOREACH_VAL(input, entry) {
6376+
if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) {
6377+
fast_long_add_function(return_value, return_value, entry);
6378+
continue;
6379+
}
6380+
php_array_binop_apply(return_value, entry, op_name, op);
6381+
} ZEND_HASH_FOREACH_END();
6382+
} else {
6383+
ZEND_HASH_MAP_FOREACH_VAL(input, entry) {
6384+
if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) {
6385+
fast_long_add_function(return_value, return_value, entry);
6386+
continue;
6387+
}
6388+
php_array_binop_apply(return_value, entry, op_name, op);
6389+
} ZEND_HASH_FOREACH_END();
63666390
}
6367-
} ZEND_HASH_FOREACH_END();
6391+
} else if (op == mul_function) {
6392+
zval *entry;
6393+
if (HT_IS_PACKED(input)) {
6394+
ZEND_HASH_PACKED_FOREACH_VAL(input, entry) {
6395+
if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) {
6396+
zend_long lval;
6397+
double dval;
6398+
int overflow;
6399+
ZEND_SIGNED_MULTIPLY_LONG(Z_LVAL_P(return_value), Z_LVAL_P(entry), lval, dval, overflow);
6400+
if (UNEXPECTED(overflow)) {
6401+
ZVAL_DOUBLE(return_value, dval);
6402+
} else {
6403+
Z_LVAL_P(return_value) = lval;
6404+
}
6405+
continue;
6406+
}
6407+
php_array_binop_apply(return_value, entry, op_name, op);
6408+
} ZEND_HASH_FOREACH_END();
6409+
} else {
6410+
ZEND_HASH_MAP_FOREACH_VAL(input, entry) {
6411+
if (EXPECTED(Z_TYPE_P(entry) == IS_LONG) && EXPECTED(Z_TYPE_P(return_value) == IS_LONG)) {
6412+
zend_long lval;
6413+
double dval;
6414+
int overflow;
6415+
ZEND_SIGNED_MULTIPLY_LONG(Z_LVAL_P(return_value), Z_LVAL_P(entry), lval, dval, overflow);
6416+
if (UNEXPECTED(overflow)) {
6417+
ZVAL_DOUBLE(return_value, dval);
6418+
} else {
6419+
Z_LVAL_P(return_value) = lval;
6420+
}
6421+
continue;
6422+
}
6423+
php_array_binop_apply(return_value, entry, op_name, op);
6424+
} ZEND_HASH_FOREACH_END();
6425+
}
6426+
} else {
6427+
zval *entry;
6428+
if (HT_IS_PACKED(input)) {
6429+
ZEND_HASH_PACKED_FOREACH_VAL(input, entry) {
6430+
php_array_binop_apply(return_value, entry, op_name, op);
6431+
} ZEND_HASH_FOREACH_END();
6432+
} else {
6433+
ZEND_HASH_MAP_FOREACH_VAL(input, entry) {
6434+
php_array_binop_apply(return_value, entry, op_name, op);
6435+
} ZEND_HASH_FOREACH_END();
6436+
}
6437+
}
63686438
}
63696439

63706440
/* {{{ 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)