Skip to content

Commit c076dea

Browse files
Extend min/max array fast path to doubles
1 parent bfb7e51 commit c076dea

4 files changed

Lines changed: 150 additions & 75 deletions

File tree

UPGRADING

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ PHP 8.6 UPGRADE NOTES
392392
. Improved performance of array_map() with multiple arrays passed.
393393
. Improved performance of array_sum() and array_product() for
394394
integer-only arrays.
395+
. Improved performance of min() and max() for integer-only and float-only
396+
arrays.
395397
. Improved performance of array_unshift().
396398
. Improved performance of array_walk().
397399
. Improved performance of intval('+0b...', 2) and intval('0b...', 2).

ext/standard/array.c

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,63 +1095,73 @@ static int php_data_compare(const void *f, const void *s) /* {{{ */
10951095
}
10961096
/* }}} */
10971097

1098-
static zend_always_inline bool php_array_minmax_long(HashTable *array, zval *return_value, bool max) /* {{{ */
1098+
static zend_always_inline bool php_array_minmax(HashTable *array, zval *return_value, bool max) /* {{{ */
10991099
{
11001100
zval *entry, *result = NULL;
1101-
zend_long result_lval;
1102-
bool long_mode = false;
1101+
zend_long result_lval = 0;
1102+
double result_dval = 0.0;
1103+
/* IS_LONG or IS_DOUBLE while every value scanned so far has that exact type,
1104+
* IS_UNDEF once a value forces the generic php_data_compare() fallback. */
1105+
uint8_t fast_type = IS_UNDEF;
11031106

11041107
ZEND_HASH_FOREACH_VAL(array, entry) {
11051108
zval *value = entry;
1106-
zend_long value_lval;
11071109

11081110
ZVAL_DEREF(value);
1109-
if (!result) {
1110-
if (Z_TYPE_P(value) != IS_LONG) {
1111-
return false;
1112-
}
11131111

1114-
result = value;
1115-
result_lval = Z_LVAL_P(value);
1116-
long_mode = true;
1112+
if (fast_type == IS_LONG && EXPECTED(Z_TYPE_P(value) == IS_LONG)) {
1113+
zend_long value_lval = Z_LVAL_P(value);
1114+
if (max ? result_lval < value_lval : result_lval > value_lval) {
1115+
result = value;
1116+
result_lval = value_lval;
1117+
}
11171118
continue;
11181119
}
11191120

1120-
if (long_mode && EXPECTED(Z_TYPE_P(value) == IS_LONG)) {
1121-
value_lval = Z_LVAL_P(value);
1122-
if (max) {
1123-
if (result_lval < value_lval) {
1121+
if (fast_type == IS_DOUBLE && EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) {
1122+
double value_dval = Z_DVAL_P(value);
1123+
/* NaN ordering differs from zend_compare(); hand it to the fallback. */
1124+
if (EXPECTED(!zend_isnan(value_dval))) {
1125+
if (max ? result_dval < value_dval : result_dval > value_dval) {
11241126
result = value;
1125-
result_lval = value_lval;
1126-
}
1127-
} else {
1128-
if (result_lval > value_lval) {
1129-
result = value;
1130-
result_lval = value_lval;
1127+
result_dval = value_dval;
11311128
}
1129+
continue;
11321130
}
1133-
continue;
11341131
}
11351132

1136-
long_mode = false;
1137-
1138-
if (max) {
1139-
if (php_data_compare(result, value) < 0) {
1133+
if (!result) {
1134+
if (Z_TYPE_P(value) == IS_LONG) {
11401135
result = value;
1136+
result_lval = Z_LVAL_P(value);
1137+
fast_type = IS_LONG;
1138+
continue;
11411139
}
1142-
} else {
1143-
if (php_data_compare(result, value) > 0) {
1140+
if (Z_TYPE_P(value) == IS_DOUBLE && !zend_isnan(Z_DVAL_P(value))) {
11441141
result = value;
1142+
result_dval = Z_DVAL_P(value);
1143+
fast_type = IS_DOUBLE;
1144+
continue;
11451145
}
1146+
return false;
1147+
}
1148+
1149+
fast_type = IS_UNDEF;
1150+
1151+
int cmp = php_data_compare(result, value);
1152+
if (max ? cmp < 0 : cmp > 0) {
1153+
result = value;
11461154
}
11471155
} ZEND_HASH_FOREACH_END();
11481156

11491157
if (!result) {
11501158
return false;
11511159
}
11521160

1153-
if (long_mode) {
1161+
if (fast_type == IS_LONG) {
11541162
ZVAL_LONG(return_value, result_lval);
1163+
} else if (fast_type == IS_DOUBLE) {
1164+
ZVAL_DOUBLE(return_value, result_dval);
11551165
} else {
11561166
ZVAL_COPY_DEREF(return_value, result);
11571167
}
@@ -1179,7 +1189,7 @@ PHP_FUNCTION(min)
11791189
RETURN_THROWS();
11801190
} else {
11811191
HashTable *array = Z_ARRVAL(args[0]);
1182-
if (php_array_minmax_long(array, return_value, false)) {
1192+
if (php_array_minmax(array, return_value, false)) {
11831193
return;
11841194
}
11851195

@@ -1312,7 +1322,7 @@ PHP_FUNCTION(max)
13121322
RETURN_THROWS();
13131323
} else {
13141324
HashTable *array = Z_ARRVAL(args[0]);
1315-
if (php_array_minmax_long(array, return_value, true)) {
1325+
if (php_array_minmax(array, return_value, true)) {
13161326
return;
13171327
}
13181328

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
--TEST--
2+
min() and max() array long/double fast path preserves comparison behavior
3+
--FILE--
4+
<?php
5+
6+
$cases = [];
7+
8+
$cases["packed long"] = [4, 1, 7, -3, 2];
9+
10+
$long_sparse = [4, 1, 7];
11+
unset($long_sparse[1]);
12+
$long_sparse[5] = -2;
13+
$cases["sparse long"] = $long_sparse;
14+
15+
$la = 4;
16+
$lb = 9;
17+
$cases["long refs"] = [&$la, &$lb, 3];
18+
19+
$cases["packed double"] = [4.5, 1.5, 7.5, -3.5, 2.5];
20+
21+
$dbl_sparse = [4.5, 1.5, 7.5];
22+
unset($dbl_sparse[1]);
23+
$dbl_sparse[5] = -2.5;
24+
$cases["sparse double"] = $dbl_sparse;
25+
26+
$da = 4.5;
27+
$db = 9.5;
28+
$cases["double refs"] = [&$da, &$db, 3.5];
29+
30+
$cases["double equal"] = [2.5, 2.5, 2.5];
31+
32+
$cases["nan first"] = [NAN, 1.0, 2.0];
33+
$cases["nan middle"] = [3.0, NAN, 1.0];
34+
$cases["nan last"] = [3.0, 1.0, NAN];
35+
$cases["inf"] = [INF, -INF, 0.0, 5.0];
36+
37+
$cases["single long"] = [7];
38+
$cases["single double"] = [7.5];
39+
40+
$cases["first non-long"] = ["5", 3, 4];
41+
$cases["fallback after longs"] = [5, 4, "3", 2];
42+
$cases["fallback after doubles"] = [5.5, 4.5, "3", 2.5];
43+
$cases["long then double"] = [5, 4, 2.5, 9];
44+
$cases["double then long"] = [5.5, 4.5, 2, 9];
45+
46+
foreach ($cases as $name => $values) {
47+
echo "-- $name --\n";
48+
var_dump(min($values));
49+
var_dump(max($values));
50+
}
51+
52+
?>
53+
--EXPECT--
54+
-- packed long --
55+
int(-3)
56+
int(7)
57+
-- sparse long --
58+
int(-2)
59+
int(7)
60+
-- long refs --
61+
int(3)
62+
int(9)
63+
-- packed double --
64+
float(-3.5)
65+
float(7.5)
66+
-- sparse double --
67+
float(-2.5)
68+
float(7.5)
69+
-- double refs --
70+
float(3.5)
71+
float(9.5)
72+
-- double equal --
73+
float(2.5)
74+
float(2.5)
75+
-- nan first --
76+
float(1)
77+
float(NAN)
78+
-- nan middle --
79+
float(1)
80+
float(3)
81+
-- nan last --
82+
float(NAN)
83+
float(3)
84+
-- inf --
85+
float(-INF)
86+
float(INF)
87+
-- single long --
88+
int(7)
89+
int(7)
90+
-- single double --
91+
float(7.5)
92+
float(7.5)
93+
-- first non-long --
94+
int(3)
95+
string(1) "5"
96+
-- fallback after longs --
97+
int(2)
98+
int(5)
99+
-- fallback after doubles --
100+
float(2.5)
101+
float(5.5)
102+
-- long then double --
103+
float(2.5)
104+
int(9)
105+
-- double then long --
106+
int(2)
107+
int(9)

ext/standard/tests/array/min_max_array_long_fast_path.phpt

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)