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..c9443b2bc1f5 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 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. 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 99406f9e4192..1e091a140bc6 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -2990,6 +2990,24 @@ 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_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..60e5b4c311eb 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -304,6 +304,11 @@ 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 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) { zend_hash_sort_ex(ht, zend_sort, compare_func, renumber); } diff --git a/ext/standard/array.c b/ext/standard/array.c index acf65c07bd67..d923bf168a4e 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -695,7 +695,38 @@ PHP_FUNCTION(natcasesort) typedef bucket_compare_func_t(*get_compare_function)(zend_long); -static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compare_function get_cmp, bool renumber) { +static int php_array_packed_long_compare(const void *a, const void *b) +{ + const zval *lhs = a, *rhs = b; + 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; + 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)) { + 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, compare_func_t packed_cmp) { HashTable *array; zend_long sort_type = PHP_SORT_REGULAR; bucket_compare_func_t cmp; @@ -708,6 +739,13 @@ 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 && 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; + } + zend_array_sort(array, cmp, renumber); RETURN_TRUE; @@ -716,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