From 80c0b2d12ca6178e3d69b5decd7dd752a89fd75b Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Thu, 3 Sep 2026 12:33:40 +0800 Subject: [PATCH 1/4] Zend: Optimize sorting single-element arrays --- UPGRADING | 1 + Zend/zend_hash.c | 13 ++++++++++--- Zend/zend_hash.h | 7 ++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/UPGRADING b/UPGRADING index 42ee8f4228d4..def406b1d1be 100644 --- a/UPGRADING +++ b/UPGRADING @@ -1001,6 +1001,7 @@ PHP 8.6 UPGRADE NOTES . Reduced temporary allocations when iterating Phar directories. - Standard: + . Improved performance of sorting single-element arrays. . Improved performance of array_fill_keys(). . Improved performance of array_intersect(). . Improved performance of array_map() with multiple arrays passed. diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 99406f9e4192..75abfb7e255f 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -2997,9 +2997,16 @@ static void zend_hash_sort_internal(HashTable *ht, sort_func_t sort, bucket_comp IS_CONSISTENT(ht); - if (!(ht->nNumOfElements>1) && !(renumber && ht->nNumOfElements>0)) { - /* Doesn't require sorting */ - return; + if (ht->nNumOfElements <= 1) { + if (!renumber || ht->nNumOfElements == 0) { + /* Doesn't require sorting */ + return; + } + if (sort == zend_sort && HT_IS_PACKED(ht) && HT_IS_WITHOUT_HOLES(ht)) { + /* The single element already has the expected index. */ + ht->nInternalPointer = 0; + return; + } } if (HT_IS_PACKED(ht)) { diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h index 1181bee29fae..be8b4fd1af0d 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -312,7 +312,12 @@ static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucke * trigger user code. It will ensure the user code cannot free the array during * sorting. */ static zend_always_inline void zend_array_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) { - zend_array_sort_ex(ht, zend_sort, compare_func, renumber); + /* zend_sort() cannot invoke the comparator for at most one element. */ + if (UNEXPECTED(ht->nNumOfElements <= 1)) { + zend_hash_sort_ex(ht, zend_sort, compare_func, renumber); + } else { + zend_array_sort_ex(ht, zend_sort, compare_func, renumber); + } } static zend_always_inline uint32_t zend_hash_num_elements(const HashTable *ht) { From bf3fa03c07f76c016286d14db94ccb88720a658c Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Thu, 3 Sep 2026 17:33:35 +0800 Subject: [PATCH 2/4] set nNextFreeElement to 1 --- Zend/zend_hash.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 75abfb7e255f..84751a02e7c0 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -3005,6 +3005,7 @@ static void zend_hash_sort_internal(HashTable *ht, sort_func_t sort, bucket_comp if (sort == zend_sort && HT_IS_PACKED(ht) && HT_IS_WITHOUT_HOLES(ht)) { /* The single element already has the expected index. */ ht->nInternalPointer = 0; + ht->nNextFreeElement = 1; return; } } From 7586d0daad0d356774f6ead57f06306839123fe4 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Thu, 3 Sep 2026 17:40:20 +0800 Subject: [PATCH 3/4] Remove unnecessary UNEXPECTED macro --- Zend/zend_hash.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h index be8b4fd1af0d..c7cdbe95866e 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -313,7 +313,7 @@ static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucke * sorting. */ static zend_always_inline void zend_array_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) { /* zend_sort() cannot invoke the comparator for at most one element. */ - if (UNEXPECTED(ht->nNumOfElements <= 1)) { + if (ht->nNumOfElements <= 1) { zend_hash_sort_ex(ht, zend_sort, compare_func, renumber); } else { zend_array_sort_ex(ht, zend_sort, compare_func, renumber); From da873aaf28ff91267079e1b7360ea921dd1ae009 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Thu, 3 Sep 2026 18:02:11 +0800 Subject: [PATCH 4/4] add some behavioral tests --- .../tests/array/sort/sort_single_element.phpt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ext/standard/tests/array/sort/sort_single_element.phpt diff --git a/ext/standard/tests/array/sort/sort_single_element.phpt b/ext/standard/tests/array/sort/sort_single_element.phpt new file mode 100644 index 000000000000..d7d63e98c733 --- /dev/null +++ b/ext/standard/tests/array/sort/sort_single_element.phpt @@ -0,0 +1,57 @@ +--TEST-- +Sorting single-element arrays does not invoke the comparison function +--FILE-- + $b; +}; + +$array = [42]; +// Keep the array packed and without holes, but leave the next free index at 11. +$array[10] = 99; +unset($array[10]); +next($array); +var_dump(usort($array, $compare)); +var_dump($array, key($array)); +$array[] = 43; +var_dump($array); + +$array = ['answer' => 42]; +next($array); +var_dump(usort($array, $compare)); +var_dump($array, key($array)); +$array[] = 43; +var_dump($array); + +var_dump($calls); + +?> +--EXPECT-- +bool(true) +array(1) { + [0]=> + int(42) +} +int(0) +array(2) { + [0]=> + int(42) + [1]=> + int(43) +} +bool(true) +array(1) { + [0]=> + int(42) +} +int(0) +array(2) { + [0]=> + int(42) + [1]=> + int(43) +} +int(0)