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)