Commit 0bf872bebf5 for php.net
commit 0bf872bebf511f0c69f15bf5d9b86eb7176fd0eb
Author: Weilin Du <weilindu@php.net>
Date: Fri Sep 4 00:19:24 2026 +0800
Zend: Optimize sorting single-element arrays (#23550)
Use zend_hash_sort_ex() directly for single-element arrays to avoid
unpacking packed arrays and temporarily increasing the array refcount
when the comparator cannot be invoked anyway.
Keep the existing renumbering behavior for sort()-style operations and
add behavioral tests covering single-element sorting.
diff --git a/UPGRADING b/UPGRADING
index 5838bd60e65..feaec16c482 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -1017,6 +1017,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 99406f9e419..84751a02e7c 100644
--- a/Zend/zend_hash.c
+++ b/Zend/zend_hash.c
@@ -2997,9 +2997,17 @@ 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;
+ ht->nNextFreeElement = 1;
+ return;
+ }
}
if (HT_IS_PACKED(ht)) {
diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h
index 1181bee29fa..c7cdbe95866 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 (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) {
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 00000000000..d7d63e98c73
--- /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--
+<?php
+
+$calls = 0;
+$compare = static function ($a, $b) use (&$calls) {
+ $calls++;
+ return $a <=> $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)