Skip to content

Commit 1fccfdc

Browse files
committed
Implement a demo
1 parent 8133066 commit 1fccfdc

12 files changed

Lines changed: 430 additions & 54 deletions

UPGRADING

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -983,8 +983,9 @@ PHP 8.6 UPGRADE NOTES
983983
. Improved performance of array_walk().
984984
. Improved performance of intval('+0b...', 2) and intval('0b...', 2).
985985
. Improved performance of str_split().
986-
. Improved performance and reduced memory usage of sort() and rsort()
987-
when using SORT_REGULAR on packed arrays containing only integers.
986+
. sort() and rsort() now sort packed arrays containing at least 64 elements
987+
in place, avoiding conversion to mixed storage. Numeric indices remain
988+
accessible to callbacks invoked during comparison.
988989

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

UPGRADING.INTERNALS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ PHP 8.6 INTERNALS UPGRADE NOTES
193193
receives pointers to zval elements rather than Bucket entries and must
194194
not execute user code or modify the array. Callers requiring stable sorting
195195
must initialize any tie-breaking metadata and compare it themselves.
196+
. Added zend_array_sort_packed() to sort and renumber packed user arrays
197+
without converting them to mixed storage. The array must have a reference
198+
count of 1. Holes are compacted, and the array is kept alive while the
199+
comparator runs, including calls to user code. The comparator receives
200+
zval pointers and must use the original positions initialized in
201+
Z_EXTRA_P() to break ties stably.
196202
. Added zend_ast_call_get_args() to fetch the argument node from any call
197203
node.
198204
. Added Z_PARAM_ENUM().

Zend/zend_hash.c

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2997,17 +2997,56 @@ static void zend_hash_packed_zval_swap(void *a, void *b)
29972997
*(zval *) b = tmp;
29982998
}
29992999

3000-
ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compar)
3000+
static void zend_hash_sort_packed_internal(HashTable *ht, compare_func_t compar)
30013001
{
30023002
IS_CONSISTENT(ht);
3003-
HT_ASSERT_RC1(ht);
30043003
ZEND_ASSERT(HT_IS_PACKED(ht) && HT_IS_WITHOUT_HOLES(ht));
30053004

30063005
zend_sort(ht->arPacked, ht->nNumUsed, sizeof(zval), compar, zend_hash_packed_zval_swap);
30073006
ht->nInternalPointer = 0;
30083007
ht->nNextFreeElement = ht->nNumUsed;
30093008
}
30103009

3010+
ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compar)
3011+
{
3012+
HT_ASSERT_RC1(ht);
3013+
zend_hash_sort_packed_internal(ht, compar);
3014+
}
3015+
3016+
ZEND_API void ZEND_FASTCALL zend_array_sort_packed(HashTable *ht, compare_func_t compar)
3017+
{
3018+
HT_ASSERT_RC1(ht);
3019+
ZEND_ASSERT(HT_IS_PACKED(ht));
3020+
3021+
if (ht->nNumOfElements == 0) {
3022+
return;
3023+
}
3024+
3025+
/* Compact holes and record the original order for stable comparisons. */
3026+
uint32_t count = 0;
3027+
for (uint32_t i = 0; i < ht->nNumUsed; i++) {
3028+
zval *value = &ht->arPacked[i];
3029+
if (UNEXPECTED(Z_TYPE_P(value) == IS_UNDEF)) {
3030+
continue;
3031+
}
3032+
if (count != i) {
3033+
ht->arPacked[count] = *value;
3034+
}
3035+
Z_EXTRA(ht->arPacked[count]) = count;
3036+
count++;
3037+
}
3038+
ht->nNumUsed = count;
3039+
3040+
/* Keep the buffer alive and force PHP writes during comparison to separate. */
3041+
GC_ADDREF(ht);
3042+
zend_hash_sort_packed_internal(ht, compar);
3043+
if (UNEXPECTED(GC_DELREF(ht) == 0)) {
3044+
zend_array_destroy(ht);
3045+
} else {
3046+
gc_check_possible_root((zend_refcounted *) ht);
3047+
}
3048+
}
3049+
30113050
static void zend_hash_sort_internal(HashTable *ht, sort_func_t sort, bucket_compare_func_t compar, bool renumber)
30123051
{
30133052
Bucket *p;

Zend/zend_hash.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,11 @@ ZEND_API void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort_
309309
* stable sorting must initialize any tie-breaking metadata and compare it. */
310310
ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compare_func);
311311

312+
/* Sort and renumber a packed user array, compacting holes and keeping it alive
313+
* across calls to user code. The comparator receives zvals and must use the
314+
* original positions initialized in Z_EXTRA_P() to break ties stably. */
315+
ZEND_API void ZEND_FASTCALL zend_array_sort_packed(HashTable *ht, compare_func_t compare_func);
316+
312317
static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) {
313318
zend_hash_sort_ex(ht, zend_sort, compare_func, renumber);
314319
}

ext/standard/array.c

Lines changed: 100 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -282,17 +282,17 @@ static zend_always_inline int php_array_key_compare_string_locale_unstable_i(Buc
282282
}
283283
/* }}} */
284284

285-
static zend_always_inline int php_array_data_compare_unstable_i(Bucket *f, Bucket *s) /* {{{ */
285+
static zend_always_inline int php_array_data_compare_zval_unstable_i(zval *f, zval *s) /* {{{ */
286286
{
287-
int result = zend_compare(&f->val, &s->val);
287+
int result = zend_compare(f, s);
288288
/* Special enums handling for array_unique. We don't want to add this logic to zend_compare as
289289
* that would be observable via comparison operators. */
290-
zval *rhs = &s->val;
290+
zval *rhs = s;
291291
ZVAL_DEREF(rhs);
292292
if (UNEXPECTED(Z_TYPE_P(rhs) == IS_OBJECT)
293293
&& result == ZEND_UNCOMPARABLE
294294
&& (Z_OBJCE_P(rhs)->ce_flags & ZEND_ACC_ENUM)) {
295-
zval *lhs = &f->val;
295+
zval *lhs = f;
296296
ZVAL_DEREF(lhs);
297297
if (Z_TYPE_P(lhs) == IS_OBJECT && (Z_OBJCE_P(lhs)->ce_flags & ZEND_ACC_ENUM)) {
298298
// Order doesn't matter, we just need to group the same enum values
@@ -308,29 +308,29 @@ static zend_always_inline int php_array_data_compare_unstable_i(Bucket *f, Bucke
308308
}
309309
/* }}} */
310310

311-
static zend_always_inline int php_array_data_compare_numeric_unstable_i(Bucket *f, Bucket *s) /* {{{ */
311+
static zend_always_inline int php_array_data_compare_numeric_zval_unstable_i(zval *f, zval *s) /* {{{ */
312312
{
313-
return numeric_compare_function(&f->val, &s->val);
313+
return numeric_compare_function(f, s);
314314
}
315315
/* }}} */
316316

317-
static zend_always_inline int php_array_data_compare_string_case_unstable_i(Bucket *f, Bucket *s) /* {{{ */
317+
static zend_always_inline int php_array_data_compare_string_case_zval_unstable_i(zval *f, zval *s) /* {{{ */
318318
{
319-
return string_case_compare_function(&f->val, &s->val);
319+
return string_case_compare_function(f, s);
320320
}
321321
/* }}} */
322322

323-
static zend_always_inline int php_array_data_compare_string_unstable_i(Bucket *f, Bucket *s) /* {{{ */
323+
static zend_always_inline int php_array_data_compare_string_zval_unstable_i(zval *f, zval *s) /* {{{ */
324324
{
325-
return string_compare_function(&f->val, &s->val);
325+
return string_compare_function(f, s);
326326
}
327327
/* }}} */
328328

329-
static int php_array_natural_general_compare(Bucket *f, Bucket *s, bool fold_case) /* {{{ */
329+
static int php_array_natural_general_compare(zval *f, zval *s, bool fold_case) /* {{{ */
330330
{
331331
zend_string *tmp_str1, *tmp_str2;
332-
zend_string *str1 = zval_get_tmp_string(&f->val, &tmp_str1);
333-
zend_string *str2 = zval_get_tmp_string(&s->val, &tmp_str2);
332+
zend_string *str1 = zval_get_tmp_string(f, &tmp_str1);
333+
zend_string *str2 = zval_get_tmp_string(s, &tmp_str2);
334334

335335
int result = strnatcmp_ex(ZSTR_VAL(str1), ZSTR_LEN(str1), ZSTR_VAL(str2), ZSTR_LEN(str2), fold_case);
336336

@@ -340,36 +340,62 @@ static int php_array_natural_general_compare(Bucket *f, Bucket *s, bool fold_cas
340340
}
341341
/* }}} */
342342

343-
static zend_always_inline int php_array_natural_compare_unstable_i(Bucket *a, Bucket *b) /* {{{ */
343+
static zend_always_inline int php_array_natural_compare_zval_unstable_i(zval *a, zval *b) /* {{{ */
344344
{
345345
return php_array_natural_general_compare(a, b, false);
346346
}
347347
/* }}} */
348348

349-
static zend_always_inline int php_array_natural_case_compare_unstable_i(Bucket *a, Bucket *b) /* {{{ */
349+
static zend_always_inline int php_array_natural_case_compare_zval_unstable_i(zval *a, zval *b) /* {{{ */
350350
{
351351
return php_array_natural_general_compare(a, b, true);
352352
}
353353
/* }}} */
354354

355-
static int php_array_data_compare_string_locale_unstable_i(Bucket *f, Bucket *s) /* {{{ */
355+
static int php_array_data_compare_string_locale_zval_unstable_i(zval *f, zval *s) /* {{{ */
356356
{
357-
return string_locale_compare_function(&f->val, &s->val);
357+
return string_locale_compare_function(f, s);
358358
}
359359
/* }}} */
360360

361+
static zend_never_inline ZEND_COLD int stable_zval_sort_fallback(const zval *a, const zval *b)
362+
{
363+
return ZEND_THREEWAY_COMPARE(Z_EXTRA_P(a), Z_EXTRA_P(b));
364+
}
365+
366+
/* Share value comparisons between Bucket sorting and packed zval sorting. */
367+
#define DEFINE_DATA_SORT_VARIANTS(name) \
368+
static zend_always_inline int php_array_##name##_unstable_i(Bucket *a, Bucket *b) { \
369+
return php_array_##name##_zval_unstable_i(&a->val, &b->val); \
370+
} \
371+
DEFINE_SORT_VARIANTS(name) \
372+
static zend_never_inline int php_array_packed_##name(const void *a, const void *b) { \
373+
int result = php_array_##name##_zval_unstable_i((zval *) a, (zval *) b); \
374+
if (EXPECTED(result)) { \
375+
return result; \
376+
} \
377+
return stable_zval_sort_fallback(a, b); \
378+
} \
379+
static zend_never_inline int php_array_packed_reverse_##name(const void *a, const void *b) { \
380+
int result = php_array_##name##_zval_unstable_i((zval *) a, (zval *) b) * -1; \
381+
if (EXPECTED(result)) { \
382+
return result; \
383+
} \
384+
return stable_zval_sort_fallback(a, b); \
385+
}
386+
361387
DEFINE_SORT_VARIANTS(key_compare);
362388
DEFINE_SORT_VARIANTS(key_compare_numeric);
363389
DEFINE_SORT_VARIANTS(key_compare_string_case);
364390
DEFINE_SORT_VARIANTS(key_compare_string);
365391
DEFINE_SORT_VARIANTS(key_compare_string_locale);
366-
DEFINE_SORT_VARIANTS(data_compare);
367-
DEFINE_SORT_VARIANTS(data_compare_numeric);
368-
DEFINE_SORT_VARIANTS(data_compare_string_case);
369-
DEFINE_SORT_VARIANTS(data_compare_string);
370-
DEFINE_SORT_VARIANTS(data_compare_string_locale);
371-
DEFINE_SORT_VARIANTS(natural_compare);
372-
DEFINE_SORT_VARIANTS(natural_case_compare);
392+
DEFINE_DATA_SORT_VARIANTS(data_compare);
393+
DEFINE_DATA_SORT_VARIANTS(data_compare_numeric);
394+
DEFINE_DATA_SORT_VARIANTS(data_compare_string_case);
395+
DEFINE_DATA_SORT_VARIANTS(data_compare_string);
396+
DEFINE_DATA_SORT_VARIANTS(data_compare_string_locale);
397+
DEFINE_DATA_SORT_VARIANTS(natural_compare);
398+
DEFINE_DATA_SORT_VARIANTS(natural_case_compare);
373399

374400
static bucket_compare_func_t php_get_key_compare_func(zend_long sort_type)
375401
{
@@ -491,6 +517,39 @@ static bucket_compare_func_t php_get_data_reverse_compare_func(zend_long sort_ty
491517
return NULL;
492518
}
493519

520+
static zend_always_inline compare_func_t php_get_packed_data_compare_func_ex(zend_long sort_type, bool reverse)
521+
{
522+
switch (sort_type & ~PHP_SORT_FLAG_CASE) {
523+
case PHP_SORT_NUMERIC:
524+
return reverse ? php_array_packed_reverse_data_compare_numeric : php_array_packed_data_compare_numeric;
525+
case PHP_SORT_STRING:
526+
if (sort_type & PHP_SORT_FLAG_CASE) {
527+
return reverse ? php_array_packed_reverse_data_compare_string_case : php_array_packed_data_compare_string_case;
528+
}
529+
return reverse ? php_array_packed_reverse_data_compare_string : php_array_packed_data_compare_string;
530+
case PHP_SORT_NATURAL:
531+
if (sort_type & PHP_SORT_FLAG_CASE) {
532+
return reverse ? php_array_packed_reverse_natural_case_compare : php_array_packed_natural_case_compare;
533+
}
534+
return reverse ? php_array_packed_reverse_natural_compare : php_array_packed_natural_compare;
535+
case PHP_SORT_LOCALE_STRING:
536+
return reverse ? php_array_packed_reverse_data_compare_string_locale : php_array_packed_data_compare_string_locale;
537+
case PHP_SORT_REGULAR:
538+
default:
539+
return reverse ? php_array_packed_reverse_data_compare : php_array_packed_data_compare;
540+
}
541+
}
542+
543+
static compare_func_t php_get_packed_data_compare_func(zend_long sort_type)
544+
{
545+
return php_get_packed_data_compare_func_ex(sort_type, false);
546+
}
547+
548+
static compare_func_t php_get_packed_data_reverse_compare_func(zend_long sort_type)
549+
{
550+
return php_get_packed_data_compare_func_ex(sort_type, true);
551+
}
552+
494553
static bucket_compare_func_t php_get_data_compare_func_unstable(zend_long sort_type, bool reverse) /* {{{ */
495554
{
496555
switch (sort_type & ~PHP_SORT_FLAG_CASE) {
@@ -694,6 +753,7 @@ PHP_FUNCTION(natcasesort)
694753
/* }}} */
695754

696755
typedef bucket_compare_func_t(*get_compare_function)(zend_long);
756+
typedef compare_func_t (*get_packed_compare_function)(zend_long);
697757

698758
static int php_array_packed_long_compare(const void *a, const void *b)
699759
{
@@ -726,7 +786,7 @@ static bool php_array_try_packed_long_sort(HashTable *array, compare_func_t cmp)
726786
}
727787

728788
static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS,
729-
get_compare_function get_cmp, bool renumber, compare_func_t packed_cmp) {
789+
get_compare_function get_cmp, bool renumber, get_packed_compare_function get_packed_cmp) {
730790
HashTable *array;
731791
zend_long sort_type = PHP_SORT_REGULAR;
732792
bucket_compare_func_t cmp;
@@ -737,15 +797,22 @@ static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS,
737797
Z_PARAM_LONG(sort_type)
738798
ZEND_PARSE_PARAMETERS_END();
739799

740-
cmp = get_cmp(sort_type);
741-
742-
/* Keep arrays that do not need sorting on the existing path. */
743-
if (renumber && packed_cmp && array->nNumOfElements > 1
744-
&& (cmp == php_array_data_compare || cmp == php_array_reverse_data_compare)
745-
&& php_array_try_packed_long_sort(array, packed_cmp)) {
800+
/* Keep small arrays on the existing path. */
801+
if (renumber && get_packed_cmp && array->nNumOfElements >= 64 && HT_IS_PACKED(array)) {
802+
compare_func_t packed_cmp = get_packed_cmp(sort_type);
803+
compare_func_t long_cmp = NULL;
804+
if (packed_cmp == php_array_packed_data_compare) {
805+
long_cmp = php_array_packed_long_compare;
806+
} else if (packed_cmp == php_array_packed_reverse_data_compare) {
807+
long_cmp = php_array_packed_long_reverse_compare;
808+
}
809+
if (!long_cmp || !php_array_try_packed_long_sort(array, long_cmp)) {
810+
zend_array_sort_packed(array, packed_cmp);
811+
}
746812
RETURN_TRUE;
747813
}
748814

815+
cmp = get_cmp(sort_type);
749816
zend_array_sort(array, cmp, renumber);
750817

751818
RETURN_TRUE;
@@ -769,15 +836,15 @@ PHP_FUNCTION(arsort)
769836
PHP_FUNCTION(sort)
770837
{
771838
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, true,
772-
php_array_packed_long_compare);
839+
php_get_packed_data_compare_func);
773840
}
774841
/* }}} */
775842

776843
/* {{{ Sort an array in reverse order */
777844
PHP_FUNCTION(rsort)
778845
{
779846
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, true,
780-
php_array_packed_long_reverse_compare);
847+
php_get_packed_data_reverse_compare_func);
781848
}
782849
/* }}} */
783850

ext/standard/tests/array/sort/packed_integer_sort.phpt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
Packed integer sort and rsort at the insertion sort boundary
2+
Packed integer sort and rsort at the insertion sort and fast path boundaries
33
--FILE--
44
<?php
55
function check($actual, $expected) {
@@ -8,7 +8,7 @@ function check($actual, $expected) {
88
}
99
}
1010

11-
foreach ([2, 16, 17] as $size) {
11+
foreach ([2, 16, 17, 63, 64, 65] as $size) {
1212
$ascending = range(1, $size);
1313
$descending = array_reverse($ascending);
1414
$permuted = array_merge(array_slice($ascending, 1), [1]);
@@ -56,4 +56,7 @@ echo "other comparators: OK\n";
5656
size 2: OK
5757
size 16: OK
5858
size 17: OK
59+
size 63: OK
60+
size 64: OK
61+
size 65: OK
5962
other comparators: OK

ext/standard/tests/array/sort/packed_integer_sort_fallback.phpt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ function check($actual, $expected) {
1010

1111
foreach (['sort', 'rsort'] as $sort) {
1212
$left = $right = 1;
13-
$values = [&$left, 2, &$right];
13+
$zeros = array_fill(0, 61, 0);
14+
$values = [...$zeros, &$left, 2, &$right];
1415
$sort($values);
1516
$left = 11;
1617
$right = 12;
17-
check($values, $sort === 'sort' ? [11, 12, 2] : [2, 11, 12]);
18+
check($values, $sort === 'sort' ? [...$zeros, 11, 12, 2] : [2, 11, 12, ...$zeros]);
1819

1920
$values = [1.0, 1, 0.5];
2021
$sort($values);
@@ -24,10 +25,10 @@ foreach (['sort', 'rsort'] as $sort) {
2425
$sort($values);
2526
check($values, $sort === 'sort' ? ['1', '01', '2', '02'] : ['2', '02', '1', '01']);
2627

27-
$values = range(16, 1);
28+
$values = range(64, 1);
2829
$values[] = 0.5;
2930
$sort($values);
30-
check($values, $sort === 'sort' ? [0.5, ...range(1, 16)] : [...range(16, 1), 0.5]);
31+
check($values, $sort === 'sort' ? [0.5, ...range(1, 64)] : [...range(64, 1), 0.5]);
3132

3233
$values = [3, 9, 1, 2];
3334
unset($values[1]);

0 commit comments

Comments
 (0)