Skip to content

Commit 0bf872b

Browse files
authored
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.
1 parent cd23607 commit 0bf872b

4 files changed

Lines changed: 75 additions & 4 deletions

File tree

UPGRADING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,7 @@ PHP 8.6 UPGRADE NOTES
10171017
. Reduced temporary allocations when iterating Phar directories.
10181018

10191019
- Standard:
1020+
. Improved performance of sorting single-element arrays.
10201021
. Improved performance of array_fill_keys().
10211022
. Improved performance of array_intersect().
10221023
. Improved performance of array_map() with multiple arrays passed.

Zend/zend_hash.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2997,9 +2997,17 @@ static void zend_hash_sort_internal(HashTable *ht, sort_func_t sort, bucket_comp
29972997

29982998
IS_CONSISTENT(ht);
29992999

3000-
if (!(ht->nNumOfElements>1) && !(renumber && ht->nNumOfElements>0)) {
3001-
/* Doesn't require sorting */
3002-
return;
3000+
if (ht->nNumOfElements <= 1) {
3001+
if (!renumber || ht->nNumOfElements == 0) {
3002+
/* Doesn't require sorting */
3003+
return;
3004+
}
3005+
if (sort == zend_sort && HT_IS_PACKED(ht) && HT_IS_WITHOUT_HOLES(ht)) {
3006+
/* The single element already has the expected index. */
3007+
ht->nInternalPointer = 0;
3008+
ht->nNextFreeElement = 1;
3009+
return;
3010+
}
30033011
}
30043012

30053013
if (HT_IS_PACKED(ht)) {

Zend/zend_hash.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,12 @@ static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucke
312312
* trigger user code. It will ensure the user code cannot free the array during
313313
* sorting. */
314314
static zend_always_inline void zend_array_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) {
315-
zend_array_sort_ex(ht, zend_sort, compare_func, renumber);
315+
/* zend_sort() cannot invoke the comparator for at most one element. */
316+
if (ht->nNumOfElements <= 1) {
317+
zend_hash_sort_ex(ht, zend_sort, compare_func, renumber);
318+
} else {
319+
zend_array_sort_ex(ht, zend_sort, compare_func, renumber);
320+
}
316321
}
317322

318323
static zend_always_inline uint32_t zend_hash_num_elements(const HashTable *ht) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
Sorting single-element arrays does not invoke the comparison function
3+
--FILE--
4+
<?php
5+
6+
$calls = 0;
7+
$compare = static function ($a, $b) use (&$calls) {
8+
$calls++;
9+
return $a <=> $b;
10+
};
11+
12+
$array = [42];
13+
// Keep the array packed and without holes, but leave the next free index at 11.
14+
$array[10] = 99;
15+
unset($array[10]);
16+
next($array);
17+
var_dump(usort($array, $compare));
18+
var_dump($array, key($array));
19+
$array[] = 43;
20+
var_dump($array);
21+
22+
$array = ['answer' => 42];
23+
next($array);
24+
var_dump(usort($array, $compare));
25+
var_dump($array, key($array));
26+
$array[] = 43;
27+
var_dump($array);
28+
29+
var_dump($calls);
30+
31+
?>
32+
--EXPECT--
33+
bool(true)
34+
array(1) {
35+
[0]=>
36+
int(42)
37+
}
38+
int(0)
39+
array(2) {
40+
[0]=>
41+
int(42)
42+
[1]=>
43+
int(43)
44+
}
45+
bool(true)
46+
array(1) {
47+
[0]=>
48+
int(42)
49+
}
50+
int(0)
51+
array(2) {
52+
[0]=>
53+
int(42)
54+
[1]=>
55+
int(43)
56+
}
57+
int(0)

0 commit comments

Comments
 (0)