Skip to content

Commit 6dcbecd

Browse files
committed
Optimize packed array sorting with zval comparators
Sort packed inputs to sort(), rsort(), and usort() directly as zvals, without a size cutoff. Share value comparison implementations and flag dispatch with Bucket wrappers, retaining stable ties and callback lifetime protection. Keep the metadata-free integer specialization private. Skip the lifetime pin and possible GC root registration for built-in sorts of direct scalars and strings. Continue scanning after any integer prefix; exclude NAN because string coercion warnings can invoke error handlers. Compact holes in both packed APIs and relocate active iterators as in rehash, including past-the-end cursors. Initialize original positions in both public APIs and preserve empty-array metadata. Share the array sort release logic with the existing Bucket path. Document numeric-index visibility during implicit comparison callbacks. Cover comparison modes, pivot boundaries, stable ties, references, holes, iterator relocation, append metadata, exceptions, and reentrancy through object conversions and NAN coercion warnings.
1 parent f142b81 commit 6dcbecd

17 files changed

Lines changed: 1074 additions & 150 deletions

UPGRADING

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,13 @@ PHP 8.6 UPGRADE NOTES
234234
and DirectoryIterator::current() returns string|SplFileInfo|static.
235235

236236
- Standard:
237+
. During sort() and rsort() on packed arrays, implicit comparison callbacks
238+
(such as __toString() and error handlers) can now read numeric indices of
239+
the array and observe its partially sorted contents. Previously, these
240+
reads reported an undefined array key warning and returned null because
241+
the hash index was cleared during sorting. This applies to all array sizes
242+
and comparison flags that invoke such callbacks. Arrays stored in mixed
243+
form continue to use the previous sorting path.
237244
. array_intersect() with at least two arrays now converts values to strings
238245
while scanning its inputs instead of during sort comparisons. This can
239246
change the number and order of conversion warnings and __toString() calls,
@@ -983,6 +990,8 @@ PHP 8.6 UPGRADE NOTES
983990
. Improved performance of array_walk().
984991
. Improved performance of intval('+0b...', 2) and intval('0b...', 2).
985992
. Improved performance of str_split().
993+
. sort(), rsort(), and usort() now avoid converting packed arrays to mixed
994+
storage while sorting. usort() continues to sort a copy of the input array.
986995

987996
- URI:
988997
. Improved performance of Uri\WhatWg\Url::parse() when collecting

UPGRADING.INTERNALS

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,19 @@ PHP 8.6 INTERNALS UPGRADE NOTES
187187
. New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were
188188
added, given the primary flags were running out of bits.
189189
. Added zend_hash_str_lookup().
190+
. Added zend_hash_sort_packed() to sort and renumber packed arrays in place
191+
without converting them to mixed storage. The array must have a reference
192+
count of 1. Holes are compacted and iterator positions are relocated as
193+
in zend_hash_sort_ex(). The comparator receives zval pointers and must
194+
use the original positions initialized in Z_EXTRA_P() to break ties
195+
stably. It must not execute user code or modify the array. Empty arrays
196+
retain their internal pointer and next free element.
197+
. Added zend_array_sort_packed() to sort and renumber packed user arrays
198+
without converting them to mixed storage. The array must have a reference
199+
count of 1. Holes are compacted, and the array is kept alive while the
200+
comparator runs, including calls to user code. The comparator receives
201+
zval pointers and must use the original positions initialized in
202+
Z_EXTRA_P() to break ties stably.
190203
. Added zend_ast_call_get_args() to fetch the argument node from any call
191204
node.
192205
. Added Z_PARAM_ENUM().

Zend/zend_hash.c

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2990,6 +2990,89 @@ ZEND_API void zend_hash_bucket_packed_swap(Bucket *p, Bucket *q)
29902990
q->h = h;
29912991
}
29922992

2993+
static void zend_hash_packed_zval_swap(void *a, void *b)
2994+
{
2995+
zval tmp = *(zval *) a;
2996+
*(zval *) a = *(zval *) b;
2997+
*(zval *) b = tmp;
2998+
}
2999+
3000+
static void zend_hash_sort_packed_prepare(HashTable *ht)
3001+
{
3002+
uint32_t old_num_used = ht->nNumUsed;
3003+
uint32_t iter_pos = HT_INVALID_IDX;
3004+
uint32_t count = 0;
3005+
for (uint32_t i = 0; i < ht->nNumUsed; i++) {
3006+
zval *value = &ht->arPacked[i];
3007+
if (UNEXPECTED(Z_TYPE_P(value) == IS_UNDEF)) {
3008+
if (count == i && UNEXPECTED(HT_HAS_ITERATORS(ht))) {
3009+
/* As in rehash, cursors at the first hole already have the right
3010+
* destination. Relocate subsequent cursors as values move. */
3011+
iter_pos = zend_hash_iterators_lower_pos(ht, i + 1);
3012+
}
3013+
continue;
3014+
}
3015+
if (count != i) {
3016+
ht->arPacked[count] = *value;
3017+
if (UNEXPECTED(ht->nInternalPointer > count && ht->nInternalPointer <= i)) {
3018+
ht->nInternalPointer = count;
3019+
}
3020+
if (UNEXPECTED(i >= iter_pos)) {
3021+
do {
3022+
zend_hash_iterators_update(ht, iter_pos, count);
3023+
iter_pos = zend_hash_iterators_lower_pos(ht, iter_pos + 1);
3024+
} while (iter_pos < i);
3025+
}
3026+
}
3027+
Z_EXTRA(ht->arPacked[count]) = count;
3028+
count++;
3029+
}
3030+
ht->nNumUsed = count;
3031+
if (count != old_num_used && UNEXPECTED(HT_HAS_ITERATORS(ht))) {
3032+
/* A cursor at the old end must still see elements appended after sorting. */
3033+
_zend_hash_iterators_update(ht, old_num_used, count);
3034+
}
3035+
}
3036+
3037+
static void zend_hash_sort_packed_internal(HashTable *ht, compare_func_t compar)
3038+
{
3039+
IS_CONSISTENT(ht);
3040+
ZEND_ASSERT(HT_IS_PACKED(ht));
3041+
if (ht->nNumOfElements == 0) {
3042+
return;
3043+
}
3044+
3045+
/* Compact holes and record the original order for stable comparisons. */
3046+
zend_hash_sort_packed_prepare(ht);
3047+
zend_sort(ht->arPacked, ht->nNumUsed, sizeof(zval), compar, zend_hash_packed_zval_swap);
3048+
ht->nInternalPointer = 0;
3049+
ht->nNextFreeElement = ht->nNumUsed;
3050+
}
3051+
3052+
ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compar)
3053+
{
3054+
HT_ASSERT_RC1(ht);
3055+
zend_hash_sort_packed_internal(ht, compar);
3056+
}
3057+
3058+
static zend_always_inline void zend_array_sort_release(HashTable *ht)
3059+
{
3060+
if (UNEXPECTED(GC_DELREF(ht) == 0)) {
3061+
zend_array_destroy(ht);
3062+
} else {
3063+
gc_check_possible_root((zend_refcounted *) ht);
3064+
}
3065+
}
3066+
3067+
ZEND_API void ZEND_FASTCALL zend_array_sort_packed(HashTable *ht, compare_func_t compar)
3068+
{
3069+
HT_ASSERT_RC1(ht);
3070+
/* Keep the buffer alive and force PHP writes during comparison to separate. */
3071+
GC_ADDREF(ht);
3072+
zend_hash_sort_packed_internal(ht, compar);
3073+
zend_array_sort_release(ht);
3074+
}
3075+
29933076
static void zend_hash_sort_internal(HashTable *ht, sort_func_t sort, bucket_compare_func_t compar, bool renumber)
29943077
{
29953078
Bucket *p;
@@ -3102,11 +3185,7 @@ ZEND_API void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort,
31023185

31033186
zend_hash_sort_internal(ht, sort, compar, renumber);
31043187

3105-
if (UNEXPECTED(GC_DELREF(ht) == 0)) {
3106-
zend_array_destroy(ht);
3107-
} else {
3108-
gc_check_possible_root((zend_refcounted *)ht);
3109-
}
3188+
zend_array_sort_release(ht);
31103189
}
31113190

31123191
static zend_always_inline int zend_hash_compare_impl(const HashTable *ht1, const HashTable *ht2, compare_func_t compar, bool ordered) {

Zend/zend_hash.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,18 @@ ZEND_API int zend_hash_compare(HashTable *ht1, const HashTable *ht2, compare_f
304304
ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber);
305305
ZEND_API void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber);
306306

307+
/* Sort and renumber an exclusively owned packed array, compacting holes. The
308+
* comparator receives zvals and must use the original positions initialized
309+
* in Z_EXTRA_P() to break ties stably. It must not execute user code or modify
310+
* the array. Compaction relocates iterators as in zend_hash_sort_ex().
311+
* Empty arrays retain their internal pointer and next free element. */
312+
ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compare_func);
313+
314+
/* Sort and renumber a packed user array, compacting holes and keeping it alive
315+
* across calls to user code. The comparator receives zvals and must use the
316+
* original positions initialized in Z_EXTRA_P() to break ties stably. */
317+
ZEND_API void ZEND_FASTCALL zend_array_sort_packed(HashTable *ht, compare_func_t compare_func);
318+
307319
static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) {
308320
zend_hash_sort_ex(ht, zend_sort, compare_func, renumber);
309321
}

0 commit comments

Comments
 (0)