From e43223d3f1733b7590513ce107d43cf21b27a587 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Sun, 6 Sep 2026 01:41:09 +0800 Subject: [PATCH 1/2] Optimize sort() and rsort() for packed integer arrays --- UPGRADING | 2 ++ UPGRADING.INTERNALS | 6 ++++++ Zend/zend_hash.c | 23 ++++++++++++++++++++++ Zend/zend_hash.h | 4 ++++ ext/standard/array.c | 45 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/UPGRADING b/UPGRADING index 5a5cafc0234f..8b23e6a87751 100644 --- a/UPGRADING +++ b/UPGRADING @@ -983,6 +983,8 @@ PHP 8.6 UPGRADE NOTES . Improved performance of array_walk(). . Improved performance of intval('+0b...', 2) and intval('0b...', 2). . Improved performance of str_split(). + . Improved performance and reduced memory usage of sort() and rsort() + when using SORT_REGULAR on packed arrays containing only integers. - URI: . Improved performance of Uri\WhatWg\Url::parse() when collecting diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index bd671018e038..e922d0090a09 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -187,6 +187,12 @@ PHP 8.6 INTERNALS UPGRADE NOTES . New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were added, given the primary flags were running out of bits. . Added zend_hash_str_lookup(). + . Added zend_hash_sort_packed() to sort and renumber packed arrays in place + without converting them to mixed storage. The array must have a reference + count of 1, contain no holes, and have no active iterators. The comparator + receives pointers to zval elements rather than Bucket entries and must + not execute user code or modify the array. Original positions are stored + in Z_EXTRA_P() so the comparator can use them for stable tie-breaking. . Added zend_ast_call_get_args() to fetch the argument node from any call node. . Added Z_PARAM_ENUM(). diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 99406f9e4192..9394c7b2d9e8 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -2990,6 +2990,29 @@ ZEND_API void zend_hash_bucket_packed_swap(Bucket *p, Bucket *q) q->h = h; } +static void zend_hash_packed_zval_swap(void *a, void *b) +{ + zval tmp = *(zval *) a; + *(zval *) a = *(zval *) b; + *(zval *) b = tmp; +} + +ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compar) +{ + IS_CONSISTENT(ht); + HT_ASSERT_RC1(ht); + ZEND_ASSERT(HT_IS_PACKED(ht) && HT_IS_WITHOUT_HOLES(ht)); + ZEND_ASSERT(!HT_HAS_ITERATORS(ht)); + + /* Preserve original order for stable comparisons. Swaps must include u2. */ + for (uint32_t i = 0; i < ht->nNumUsed; i++) { + Z_EXTRA(ht->arPacked[i]) = i; + } + zend_sort(ht->arPacked, ht->nNumUsed, sizeof(zval), compar, zend_hash_packed_zval_swap); + ht->nInternalPointer = 0; + ht->nNextFreeElement = ht->nNumUsed; +} + static void zend_hash_sort_internal(HashTable *ht, sort_func_t sort, bucket_compare_func_t compar, bool renumber) { Bucket *p; diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h index 1181bee29fae..82bf91794a4b 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -304,6 +304,10 @@ ZEND_API int zend_hash_compare(HashTable *ht1, const HashTable *ht2, compare_f ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber); ZEND_API void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber); +/* Sort and renumber a packed array without holes. The comparator must not + * execute user code or modify the array. */ +ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compare_func); + static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) { zend_hash_sort_ex(ht, zend_sort, compare_func, renumber); } diff --git a/ext/standard/array.c b/ext/standard/array.c index acf65c07bd67..c5c605309e7c 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -695,6 +695,43 @@ PHP_FUNCTION(natcasesort) typedef bucket_compare_func_t(*get_compare_function)(zend_long); +static int php_array_packed_long_compare(const void *a, const void *b) +{ + const zval *lhs = a, *rhs = b; + if (Z_LVAL_P(lhs) != Z_LVAL_P(rhs)) { + return Z_LVAL_P(lhs) > Z_LVAL_P(rhs) ? 1 : -1; + } + return (Z_EXTRA_P(lhs) > Z_EXTRA_P(rhs)) - (Z_EXTRA_P(lhs) < Z_EXTRA_P(rhs)); +} + +static int php_array_packed_long_reverse_compare(const void *a, const void *b) +{ + const zval *lhs = a, *rhs = b; + if (Z_LVAL_P(lhs) != Z_LVAL_P(rhs)) { + return Z_LVAL_P(lhs) < Z_LVAL_P(rhs) ? 1 : -1; + } + return (Z_EXTRA_P(lhs) > Z_EXTRA_P(rhs)) - (Z_EXTRA_P(lhs) < Z_EXTRA_P(rhs)); +} + +static bool php_array_try_packed_long_sort(HashTable *array, compare_func_t cmp) +{ + if (!HT_IS_PACKED(array) + || !HT_IS_WITHOUT_HOLES(array) || HT_HAS_ITERATORS(array)) { + return false; + } + + /* Reject references and other types before changing any element. Integer + * comparisons cannot invoke user code, so the array stays exclusively owned. */ + for (uint32_t i = 0; i < array->nNumUsed; i++) { + if (Z_TYPE(array->arPacked[i]) != IS_LONG) { + return false; + } + } + + zend_hash_sort_packed(array, cmp); + return true; +} + static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compare_function get_cmp, bool renumber) { HashTable *array; zend_long sort_type = PHP_SORT_REGULAR; @@ -708,6 +745,14 @@ static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compar cmp = get_cmp(sort_type); + /* Keep arrays that do not need sorting on the existing path. */ + if (renumber && sort_type == PHP_SORT_REGULAR && array->nNumOfElements > 1 + && php_array_try_packed_long_sort(array, + get_cmp == php_get_data_compare_func + ? php_array_packed_long_compare : php_array_packed_long_reverse_compare)) { + RETURN_TRUE; + } + zend_array_sort(array, cmp, renumber); RETURN_TRUE; From 81330666eb6f329351e868d4c2d535f900192b1a Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Mon, 7 Sep 2026 00:13:09 +0800 Subject: [PATCH 2/2] version 2. I review the logic and find something to improve --- UPGRADING.INTERNALS | 6 +- Zend/zend_hash.c | 5 -- Zend/zend_hash.h | 5 +- ext/standard/array.c | 37 +++++------- .../tests/array/sort/packed_integer_sort.phpt | 59 +++++++++++++++++++ .../sort/packed_integer_sort_fallback.phpt | 41 +++++++++++++ .../sort/packed_integer_sort_iterators.phpt | 45 ++++++++++++++ .../sort/packed_integer_sort_metadata.phpt | 54 +++++++++++++++++ 8 files changed, 221 insertions(+), 31 deletions(-) create mode 100644 ext/standard/tests/array/sort/packed_integer_sort.phpt create mode 100644 ext/standard/tests/array/sort/packed_integer_sort_fallback.phpt create mode 100644 ext/standard/tests/array/sort/packed_integer_sort_iterators.phpt create mode 100644 ext/standard/tests/array/sort/packed_integer_sort_metadata.phpt diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index e922d0090a09..c9443b2bc1f5 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -189,10 +189,10 @@ PHP 8.6 INTERNALS UPGRADE NOTES . Added zend_hash_str_lookup(). . Added zend_hash_sort_packed() to sort and renumber packed arrays in place without converting them to mixed storage. The array must have a reference - count of 1, contain no holes, and have no active iterators. The comparator + count of 1 and contain no holes. The comparator receives pointers to zval elements rather than Bucket entries and must - not execute user code or modify the array. Original positions are stored - in Z_EXTRA_P() so the comparator can use them for stable tie-breaking. + not execute user code or modify the array. Callers requiring stable sorting + must initialize any tie-breaking metadata and compare it themselves. . Added zend_ast_call_get_args() to fetch the argument node from any call node. . Added Z_PARAM_ENUM(). diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 9394c7b2d9e8..1e091a140bc6 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -3002,12 +3002,7 @@ ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t IS_CONSISTENT(ht); HT_ASSERT_RC1(ht); ZEND_ASSERT(HT_IS_PACKED(ht) && HT_IS_WITHOUT_HOLES(ht)); - ZEND_ASSERT(!HT_HAS_ITERATORS(ht)); - /* Preserve original order for stable comparisons. Swaps must include u2. */ - for (uint32_t i = 0; i < ht->nNumUsed; i++) { - Z_EXTRA(ht->arPacked[i]) = i; - } zend_sort(ht->arPacked, ht->nNumUsed, sizeof(zval), compar, zend_hash_packed_zval_swap); ht->nInternalPointer = 0; ht->nNextFreeElement = ht->nNumUsed; diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h index 82bf91794a4b..60e5b4c311eb 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -304,8 +304,9 @@ ZEND_API int zend_hash_compare(HashTable *ht1, const HashTable *ht2, compare_f ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber); ZEND_API void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber); -/* Sort and renumber a packed array without holes. The comparator must not - * execute user code or modify the array. */ +/* Sort and renumber an exclusively owned packed array without holes. The + * comparator must not execute user code or modify the array. Callers requiring + * stable sorting must initialize any tie-breaking metadata and compare it. */ ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compare_func); static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) { diff --git a/ext/standard/array.c b/ext/standard/array.c index c5c605309e7c..d923bf168a4e 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -698,25 +698,18 @@ typedef bucket_compare_func_t(*get_compare_function)(zend_long); static int php_array_packed_long_compare(const void *a, const void *b) { const zval *lhs = a, *rhs = b; - if (Z_LVAL_P(lhs) != Z_LVAL_P(rhs)) { - return Z_LVAL_P(lhs) > Z_LVAL_P(rhs) ? 1 : -1; - } - return (Z_EXTRA_P(lhs) > Z_EXTRA_P(rhs)) - (Z_EXTRA_P(lhs) < Z_EXTRA_P(rhs)); + return ZEND_THREEWAY_COMPARE(Z_LVAL_P(lhs), Z_LVAL_P(rhs)); } static int php_array_packed_long_reverse_compare(const void *a, const void *b) { const zval *lhs = a, *rhs = b; - if (Z_LVAL_P(lhs) != Z_LVAL_P(rhs)) { - return Z_LVAL_P(lhs) < Z_LVAL_P(rhs) ? 1 : -1; - } - return (Z_EXTRA_P(lhs) > Z_EXTRA_P(rhs)) - (Z_EXTRA_P(lhs) < Z_EXTRA_P(rhs)); + return ZEND_THREEWAY_COMPARE(Z_LVAL_P(rhs), Z_LVAL_P(lhs)); } static bool php_array_try_packed_long_sort(HashTable *array, compare_func_t cmp) { - if (!HT_IS_PACKED(array) - || !HT_IS_WITHOUT_HOLES(array) || HT_HAS_ITERATORS(array)) { + if (!HT_IS_PACKED(array) || !HT_IS_WITHOUT_HOLES(array)) { return false; } @@ -732,7 +725,8 @@ static bool php_array_try_packed_long_sort(HashTable *array, compare_func_t cmp) return true; } -static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compare_function get_cmp, bool renumber) { +static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, + get_compare_function get_cmp, bool renumber, compare_func_t packed_cmp) { HashTable *array; zend_long sort_type = PHP_SORT_REGULAR; bucket_compare_func_t cmp; @@ -746,10 +740,9 @@ static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compar cmp = get_cmp(sort_type); /* Keep arrays that do not need sorting on the existing path. */ - if (renumber && sort_type == PHP_SORT_REGULAR && array->nNumOfElements > 1 - && php_array_try_packed_long_sort(array, - get_cmp == php_get_data_compare_func - ? php_array_packed_long_compare : php_array_packed_long_reverse_compare)) { + if (renumber && packed_cmp && array->nNumOfElements > 1 + && (cmp == php_array_data_compare || cmp == php_array_reverse_data_compare) + && php_array_try_packed_long_sort(array, packed_cmp)) { RETURN_TRUE; } @@ -761,42 +754,44 @@ static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compar /* {{{ Sort an array and maintain index association */ PHP_FUNCTION(asort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, false); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, false, NULL); } /* }}} */ /* {{{ Sort an array in reverse order and maintain index association */ PHP_FUNCTION(arsort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, false); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, false, NULL); } /* }}} */ /* {{{ Sort an array */ PHP_FUNCTION(sort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, true); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, true, + php_array_packed_long_compare); } /* }}} */ /* {{{ Sort an array in reverse order */ PHP_FUNCTION(rsort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, true); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, true, + php_array_packed_long_reverse_compare); } /* }}} */ /* {{{ Sort an array by key value in reverse order */ PHP_FUNCTION(krsort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_reverse_compare_func, false); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_reverse_compare_func, false, NULL); } /* }}} */ /* {{{ Sort an array by key */ PHP_FUNCTION(ksort) { - php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_compare_func, false); + php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_compare_func, false, NULL); } /* }}} */ diff --git a/ext/standard/tests/array/sort/packed_integer_sort.phpt b/ext/standard/tests/array/sort/packed_integer_sort.phpt new file mode 100644 index 000000000000..b48d54a87cb6 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_integer_sort.phpt @@ -0,0 +1,59 @@ +--TEST-- +Packed integer sort and rsort at the insertion sort boundary +--FILE-- + 1, 2 => 2, 0 => 3]); +arsort($values); +check($values, [0 => 3, 2 => 2, 1 => 1]); +ksort($values); +check($values, [3, 1, 2]); +krsort($values); +check($values, [2 => 2, 1 => 1, 0 => 3]); +echo "other comparators: OK\n"; +?> +--EXPECT-- +size 2: OK +size 16: OK +size 17: OK +other comparators: OK diff --git a/ext/standard/tests/array/sort/packed_integer_sort_fallback.phpt b/ext/standard/tests/array/sort/packed_integer_sort_fallback.phpt new file mode 100644 index 000000000000..dd1dfb263fc5 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_integer_sort_fallback.phpt @@ -0,0 +1,41 @@ +--TEST-- +Packed sorting preserves stable comparisons for references and non-integers +--FILE-- + +--EXPECT-- +sort: OK +rsort: OK diff --git a/ext/standard/tests/array/sort/packed_integer_sort_iterators.phpt b/ext/standard/tests/array/sort/packed_integer_sort_iterators.phpt new file mode 100644 index 000000000000..59842ce0c208 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_integer_sort_iterators.phpt @@ -0,0 +1,45 @@ +--TEST-- +Sorting packed integer arrays during by-reference foreach preserves iterator positions +--FILE-- + &$value) { + $visited[] = [$key, $value]; + if ($first) { + $first = false; + if ($removeCurrent) { + // Remove the referenced element while keeping the iterator active. + array_shift($values); + foreach (array_keys($values) as $index) { + check(ReflectionReference::fromArrayElement($values, $index), null); + } + } + $sort($values); + } + } + unset($value); + if ($removeCurrent) { + $expected = $sort === 'sort' ? [1, 2, 3] : [3, 2, 1]; + check($visited, [[0, 9], [0, $expected[0]], [1, 2], [2, $expected[2]]]); + } else { + $expected = $sort === 'sort' ? [1, 2, 3, 9] : [9, 3, 2, 1]; + check($visited, [[0, 9], [1, $expected[1]], [2, $expected[2]], [3, $expected[3]]]); + } + check($values, $expected); + } + echo "$sort: OK\n"; +} +?> +--EXPECT-- +sort: OK +rsort: OK diff --git a/ext/standard/tests/array/sort/packed_integer_sort_metadata.phpt b/ext/standard/tests/array/sort/packed_integer_sort_metadata.phpt new file mode 100644 index 000000000000..90413189fce6 --- /dev/null +++ b/ext/standard/tests/array/sort/packed_integer_sort_metadata.phpt @@ -0,0 +1,54 @@ +--TEST-- +Packed integer sorting resets array metadata and preserves copy on write +--FILE-- + 123]); + + $values = [11, 22]; + unset($values[0]); + $sort($values); + $values[] = 123; + check($values, [22, 123]); + echo "$sort: OK\n"; +} +?> +--EXPECT-- +sort: OK +rsort: OK