-
Notifications
You must be signed in to change notification settings - Fork 8.1k
[ext/standard] Specialize min()/max() for numeric arrays #22127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mehmetcansahin
wants to merge
4
commits into
php:master
Choose a base branch
from
mehmetcansahin:minmax-long-array-fast-path
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
0f5c8a6
Specialize min and max for long arrays
mehmetcansahin bfb7e51
Fix min/max long fast path warning
mehmetcansahin c076dea
Extend min/max array fast path to doubles
mehmetcansahin 15babe3
Move min/max array fallback into array.c
mehmetcansahin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1089,9 +1089,151 @@ PHP_FUNCTION(key) | |
| } | ||
| /* }}} */ | ||
|
|
||
| static int php_data_compare(const void *f, const void *s) /* {{{ */ | ||
| static zval *php_array_data_minmax(const HashTable *array, bool max) /* {{{ */ | ||
| { | ||
| return zend_compare((zval*)f, (zval*)s); | ||
| uint32_t idx; | ||
| zval *res; | ||
|
|
||
| if (array->nNumOfElements == 0) { | ||
| return NULL; | ||
| } | ||
|
|
||
| if (HT_IS_PACKED(array)) { | ||
| zval *zv; | ||
|
|
||
| idx = 0; | ||
| while (1) { | ||
| if (idx == array->nNumUsed) { | ||
| return NULL; | ||
| } | ||
| if (Z_TYPE(array->arPacked[idx]) != IS_UNDEF) { | ||
| break; | ||
| } | ||
| idx++; | ||
| } | ||
| res = array->arPacked + idx; | ||
| for (; idx < array->nNumUsed; idx++) { | ||
| zv = array->arPacked + idx; | ||
| if (UNEXPECTED(Z_TYPE_P(zv) == IS_UNDEF)) { | ||
| continue; | ||
| } | ||
|
|
||
| if (max) { | ||
| if (zend_compare(res, zv) < 0) { | ||
| res = zv; | ||
| } | ||
| } else { | ||
| if (zend_compare(res, zv) > 0) { | ||
| res = zv; | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| Bucket *p; | ||
|
|
||
| idx = 0; | ||
| while (1) { | ||
| if (idx == array->nNumUsed) { | ||
| return NULL; | ||
| } | ||
| if (Z_TYPE(array->arData[idx].val) != IS_UNDEF) { | ||
| break; | ||
| } | ||
| idx++; | ||
| } | ||
| res = &array->arData[idx].val; | ||
| for (; idx < array->nNumUsed; idx++) { | ||
| p = array->arData + idx; | ||
| if (UNEXPECTED(Z_TYPE(p->val) == IS_UNDEF)) { | ||
| continue; | ||
| } | ||
|
|
||
| if (max) { | ||
| if (zend_compare(res, &p->val) < 0) { | ||
| res = &p->val; | ||
| } | ||
| } else { | ||
| if (zend_compare(res, &p->val) > 0) { | ||
| res = &p->val; | ||
| } | ||
|
Comment on lines
+1135
to
+1158
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto |
||
| } | ||
| } | ||
| } | ||
| return res; | ||
| } | ||
| /* }}} */ | ||
|
|
||
| static zend_always_inline bool php_array_minmax(HashTable *array, zval *return_value, bool max) /* {{{ */ | ||
| { | ||
| zval *entry, *result = NULL; | ||
| zend_long result_lval = 0; | ||
| double result_dval = 0.0; | ||
| /* IS_LONG or IS_DOUBLE while every value scanned so far has that exact type, | ||
| * IS_UNDEF once a value forces the generic zend_compare() fallback. */ | ||
| uint8_t fast_type = IS_UNDEF; | ||
|
|
||
| ZEND_HASH_FOREACH_VAL(array, entry) { | ||
| zval *value = entry; | ||
|
|
||
| ZVAL_DEREF(value); | ||
|
|
||
| if (fast_type == IS_LONG && EXPECTED(Z_TYPE_P(value) == IS_LONG)) { | ||
| zend_long value_lval = Z_LVAL_P(value); | ||
| if (max ? result_lval < value_lval : result_lval > value_lval) { | ||
| result = value; | ||
| result_lval = value_lval; | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| if (fast_type == IS_DOUBLE && EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) { | ||
| double value_dval = Z_DVAL_P(value); | ||
| /* NaN ordering differs from zend_compare(); hand it to the fallback. */ | ||
| if (EXPECTED(!zend_isnan(value_dval))) { | ||
| if (max ? result_dval < value_dval : result_dval > value_dval) { | ||
| result = value; | ||
| result_dval = value_dval; | ||
| } | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| if (!result) { | ||
| if (Z_TYPE_P(value) == IS_LONG) { | ||
| result = value; | ||
| result_lval = Z_LVAL_P(value); | ||
| fast_type = IS_LONG; | ||
| continue; | ||
| } | ||
| if (Z_TYPE_P(value) == IS_DOUBLE && !zend_isnan(Z_DVAL_P(value))) { | ||
| result = value; | ||
| result_dval = Z_DVAL_P(value); | ||
| fast_type = IS_DOUBLE; | ||
| continue; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| fast_type = IS_UNDEF; | ||
|
|
||
| int cmp = zend_compare(result, value); | ||
| if (max ? cmp < 0 : cmp > 0) { | ||
| result = value; | ||
| } | ||
| } ZEND_HASH_FOREACH_END(); | ||
|
|
||
| if (!result) { | ||
| return false; | ||
| } | ||
|
|
||
| if (fast_type == IS_LONG) { | ||
| ZVAL_LONG(return_value, result_lval); | ||
| } else if (fast_type == IS_DOUBLE) { | ||
| ZVAL_DOUBLE(return_value, result_dval); | ||
| } else { | ||
| ZVAL_COPY_DEREF(return_value, result); | ||
| } | ||
| return true; | ||
| } | ||
| /* }}} */ | ||
|
|
||
|
|
@@ -1114,7 +1256,12 @@ PHP_FUNCTION(min) | |
| zend_argument_type_error(1, "must be of type array, %s given", zend_zval_value_name(&args[0])); | ||
| RETURN_THROWS(); | ||
| } else { | ||
| zval *result = zend_hash_minmax(Z_ARRVAL(args[0]), php_data_compare, 0); | ||
| HashTable *array = Z_ARRVAL(args[0]); | ||
| if (php_array_minmax(array, return_value, false)) { | ||
| return; | ||
| } | ||
|
|
||
| zval *result = php_array_data_minmax(array, false); | ||
| if (result) { | ||
| RETURN_COPY_DEREF(result); | ||
| } else { | ||
|
|
@@ -1242,7 +1389,12 @@ PHP_FUNCTION(max) | |
| zend_argument_type_error(1, "must be of type array, %s given", zend_zval_value_name(&args[0])); | ||
| RETURN_THROWS(); | ||
| } else { | ||
| zval *result = zend_hash_minmax(Z_ARRVAL(args[0]), php_data_compare, 1); | ||
| HashTable *array = Z_ARRVAL(args[0]); | ||
| if (php_array_minmax(array, return_value, true)) { | ||
| return; | ||
| } | ||
|
|
||
| zval *result = php_array_data_minmax(array, true); | ||
| if (result) { | ||
| RETURN_COPY_DEREF(result); | ||
| } else { | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| --TEST-- | ||
| min() and max() array long/double fast path preserves comparison behavior | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| $cases = []; | ||
|
|
||
| $cases["packed long"] = [4, 1, 7, -3, 2]; | ||
|
|
||
| $long_sparse = [4, 1, 7]; | ||
| unset($long_sparse[1]); | ||
| $long_sparse[5] = -2; | ||
| $cases["sparse long"] = $long_sparse; | ||
|
|
||
| $la = 4; | ||
| $lb = 9; | ||
| $cases["long refs"] = [&$la, &$lb, 3]; | ||
|
|
||
| $cases["packed double"] = [4.5, 1.5, 7.5, -3.5, 2.5]; | ||
|
|
||
| $dbl_sparse = [4.5, 1.5, 7.5]; | ||
| unset($dbl_sparse[1]); | ||
| $dbl_sparse[5] = -2.5; | ||
| $cases["sparse double"] = $dbl_sparse; | ||
|
|
||
| $da = 4.5; | ||
| $db = 9.5; | ||
| $cases["double refs"] = [&$da, &$db, 3.5]; | ||
|
|
||
| $cases["double equal"] = [2.5, 2.5, 2.5]; | ||
|
|
||
| $cases["nan first"] = [NAN, 1.0, 2.0]; | ||
| $cases["nan middle"] = [3.0, NAN, 1.0]; | ||
| $cases["nan last"] = [3.0, 1.0, NAN]; | ||
| $cases["inf"] = [INF, -INF, 0.0, 5.0]; | ||
|
|
||
| $cases["single long"] = [7]; | ||
| $cases["single double"] = [7.5]; | ||
|
|
||
| $cases["first non-long"] = ["5", 3, 4]; | ||
| $cases["fallback after longs"] = [5, 4, "3", 2]; | ||
| $cases["fallback after doubles"] = [5.5, 4.5, "3", 2.5]; | ||
| $cases["long then double"] = [5, 4, 2.5, 9]; | ||
| $cases["double then long"] = [5.5, 4.5, 2, 9]; | ||
|
|
||
| foreach ($cases as $name => $values) { | ||
| echo "-- $name --\n"; | ||
| var_dump(min($values)); | ||
| var_dump(max($values)); | ||
| } | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| -- packed long -- | ||
| int(-3) | ||
| int(7) | ||
| -- sparse long -- | ||
| int(-2) | ||
| int(7) | ||
| -- long refs -- | ||
| int(3) | ||
| int(9) | ||
| -- packed double -- | ||
| float(-3.5) | ||
| float(7.5) | ||
| -- sparse double -- | ||
| float(-2.5) | ||
| float(7.5) | ||
| -- double refs -- | ||
| float(3.5) | ||
| float(9.5) | ||
| -- double equal -- | ||
| float(2.5) | ||
| float(2.5) | ||
| -- nan first -- | ||
| float(1) | ||
| float(NAN) | ||
| -- nan middle -- | ||
| float(1) | ||
| float(3) | ||
| -- nan last -- | ||
| float(NAN) | ||
| float(3) | ||
| -- inf -- | ||
| float(-INF) | ||
| float(INF) | ||
| -- single long -- | ||
| int(7) | ||
| int(7) | ||
| -- single double -- | ||
| float(7.5) | ||
| float(7.5) | ||
| -- first non-long -- | ||
| int(3) | ||
| string(1) "5" | ||
| -- fallback after longs -- | ||
| int(2) | ||
| int(5) | ||
| -- fallback after doubles -- | ||
| float(2.5) | ||
| float(5.5) | ||
| -- long then double -- | ||
| float(2.5) | ||
| int(9) | ||
| -- double then long -- | ||
| int(2) | ||
| int(9) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we not use the usual
ZEND_FOREACHAPI?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #22147 by using
ZEND_HASH_FOREACH_VAL()for the generic fallback scan.I also measured the trade-off locally (O2 CLI build,
opcache.enable_cli=0,N=10000, 2000min()+max()iterations, 7 samples, median ms). Compared with a hand-written packed/mixed loop:ZEND_HASH_FOREACH_VAL()So the standard foreach API keeps #22147 as the cleaner behavior-preserving baseline, while there is still a measurable performance difference compared to specialized packed/mixed traversal. I plan to keep #22147 on the standard API and use specialized traversal only for the optimized path in this PR.