Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,8 @@ PHP 8.6 UPGRADE NOTES
. Improved performance of array_walk().
. Improved performance of intval('+0b...', 2) and intval('0b...', 2).
. Improved performance of str_split().
. Improved performance and reduced memory usage of sort() and rsort()
when using SORT_REGULAR on packed arrays containing only integers.

- URI:
. Improved performance of Uri\WhatWg\Url::parse() when collecting
Expand Down
6 changes: 6 additions & 0 deletions UPGRADING.INTERNALS
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ PHP 8.6 INTERNALS UPGRADE NOTES
. New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were
added, given the primary flags were running out of bits.
. Added zend_hash_str_lookup().
. Added zend_hash_sort_packed() to sort and renumber packed arrays in place
without converting them to mixed storage. The array must have a reference
count of 1 and contain no holes. The comparator
receives pointers to zval elements rather than Bucket entries and must
not execute user code or modify the array. Callers requiring stable sorting
must initialize any tie-breaking metadata and compare it themselves.
. Added zend_ast_call_get_args() to fetch the argument node from any call
node.
. Added Z_PARAM_ENUM().
Expand Down
18 changes: 18 additions & 0 deletions Zend/zend_hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -2990,6 +2990,24 @@ ZEND_API void zend_hash_bucket_packed_swap(Bucket *p, Bucket *q)
q->h = h;
}

static void zend_hash_packed_zval_swap(void *a, void *b)
{
zval tmp = *(zval *) a;
*(zval *) a = *(zval *) b;
*(zval *) b = tmp;
}

ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compar)
{
IS_CONSISTENT(ht);
HT_ASSERT_RC1(ht);
ZEND_ASSERT(HT_IS_PACKED(ht) && HT_IS_WITHOUT_HOLES(ht));

zend_sort(ht->arPacked, ht->nNumUsed, sizeof(zval), compar, zend_hash_packed_zval_swap);
ht->nInternalPointer = 0;
ht->nNextFreeElement = ht->nNumUsed;
}

static void zend_hash_sort_internal(HashTable *ht, sort_func_t sort, bucket_compare_func_t compar, bool renumber)
{
Bucket *p;
Expand Down
5 changes: 5 additions & 0 deletions Zend/zend_hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ ZEND_API int zend_hash_compare(HashTable *ht1, const HashTable *ht2, compare_f
ZEND_API void ZEND_FASTCALL zend_hash_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber);
ZEND_API void ZEND_FASTCALL zend_array_sort_ex(HashTable *ht, sort_func_t sort_func, bucket_compare_func_t compare_func, bool renumber);

/* Sort and renumber an exclusively owned packed array without holes. The
* comparator must not execute user code or modify the array. Callers requiring
* stable sorting must initialize any tie-breaking metadata and compare it. */
ZEND_API void ZEND_FASTCALL zend_hash_sort_packed(HashTable *ht, compare_func_t compare_func);

static zend_always_inline void ZEND_FASTCALL zend_hash_sort(HashTable *ht, bucket_compare_func_t compare_func, bool renumber) {
zend_hash_sort_ex(ht, zend_sort, compare_func, renumber);
}
Expand Down
54 changes: 47 additions & 7 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,38 @@ PHP_FUNCTION(natcasesort)

typedef bucket_compare_func_t(*get_compare_function)(zend_long);

static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compare_function get_cmp, bool renumber) {
static int php_array_packed_long_compare(const void *a, const void *b)
{
const zval *lhs = a, *rhs = b;
return ZEND_THREEWAY_COMPARE(Z_LVAL_P(lhs), Z_LVAL_P(rhs));
}

static int php_array_packed_long_reverse_compare(const void *a, const void *b)
{
const zval *lhs = a, *rhs = b;
return ZEND_THREEWAY_COMPARE(Z_LVAL_P(rhs), Z_LVAL_P(lhs));
}

static bool php_array_try_packed_long_sort(HashTable *array, compare_func_t cmp)
{
if (!HT_IS_PACKED(array) || !HT_IS_WITHOUT_HOLES(array)) {
return false;
}

/* Reject references and other types before changing any element. Integer
* comparisons cannot invoke user code, so the array stays exclusively owned. */
for (uint32_t i = 0; i < array->nNumUsed; i++) {
if (Z_TYPE(array->arPacked[i]) != IS_LONG) {
return false;
}
}

zend_hash_sort_packed(array, cmp);
return true;
}

static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS,
get_compare_function get_cmp, bool renumber, compare_func_t packed_cmp) {
HashTable *array;
zend_long sort_type = PHP_SORT_REGULAR;
bucket_compare_func_t cmp;
Expand All @@ -708,6 +739,13 @@ static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compar

cmp = get_cmp(sort_type);

/* Keep arrays that do not need sorting on the existing path. */
if (renumber && packed_cmp && array->nNumOfElements > 1
&& (cmp == php_array_data_compare || cmp == php_array_reverse_data_compare)
&& php_array_try_packed_long_sort(array, packed_cmp)) {
RETURN_TRUE;
}

zend_array_sort(array, cmp, renumber);

RETURN_TRUE;
Expand All @@ -716,42 +754,44 @@ static zend_always_inline void php_sort(INTERNAL_FUNCTION_PARAMETERS, get_compar
/* {{{ Sort an array and maintain index association */
PHP_FUNCTION(asort)
{
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, false);
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, false, NULL);
}
/* }}} */

/* {{{ Sort an array in reverse order and maintain index association */
PHP_FUNCTION(arsort)
{
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, false);
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, false, NULL);
}
/* }}} */

/* {{{ Sort an array */
PHP_FUNCTION(sort)
{
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, true);
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_compare_func, true,
php_array_packed_long_compare);
}
/* }}} */

/* {{{ Sort an array in reverse order */
PHP_FUNCTION(rsort)
{
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, true);
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_data_reverse_compare_func, true,
php_array_packed_long_reverse_compare);
}
/* }}} */

/* {{{ Sort an array by key value in reverse order */
PHP_FUNCTION(krsort)
{
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_reverse_compare_func, false);
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_reverse_compare_func, false, NULL);
}
/* }}} */

/* {{{ Sort an array by key */
PHP_FUNCTION(ksort)
{
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_compare_func, false);
php_sort(INTERNAL_FUNCTION_PARAM_PASSTHRU, php_get_key_compare_func, false, NULL);
}
/* }}} */

Expand Down
59 changes: 59 additions & 0 deletions ext/standard/tests/array/sort/packed_integer_sort.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--TEST--
Packed integer sort and rsort at the insertion sort boundary
--FILE--
<?php
function check($actual, $expected) {
if ($actual !== $expected) {
throw new Exception(var_export([$actual, $expected], true));
}
}

foreach ([2, 16, 17] as $size) {
$ascending = range(1, $size);
$descending = array_reverse($ascending);
$permuted = array_merge(array_slice($ascending, 1), [1]);
$duplicates = array_fill(0, $size, 7);
$limits = array_fill(0, $size - 2, 0);
$limits[] = PHP_INT_MAX;
$limits[] = PHP_INT_MIN;
$sortedLimits = array_merge([PHP_INT_MIN], array_fill(0, $size - 2, 0), [PHP_INT_MAX]);
foreach ([[$ascending, $ascending], [$descending, $ascending],
[$permuted, $ascending], [$duplicates, $duplicates],
[$limits, $sortedLimits]] as [$input, $expected]) {
// Unknown flags also resolve to regular comparison.
foreach ([SORT_REGULAR, SORT_REGULAR | SORT_FLAG_CASE, 12345] as $flags) {
$values = $input;
check(sort($values, $flags), true);
check($values, $expected);
$values = $input;
check(rsort($values, $flags), true);
check($values, array_reverse($expected));
}
}
echo "size $size: OK\n";
}

// Other modes and sorts that preserve keys must keep their own comparators.
$values = [10, 2, 1];
sort($values, SORT_STRING);
check($values, [1, 10, 2]);
rsort($values, SORT_STRING);
check($values, [2, 10, 1]);
sort($values, SORT_NUMERIC);
check($values, [1, 2, 10]);
$values = [3, 1, 2];
asort($values);
check($values, [1 => 1, 2 => 2, 0 => 3]);
arsort($values);
check($values, [0 => 3, 2 => 2, 1 => 1]);
ksort($values);
check($values, [3, 1, 2]);
krsort($values);
check($values, [2 => 2, 1 => 1, 0 => 3]);
echo "other comparators: OK\n";
?>
--EXPECT--
size 2: OK
size 16: OK
size 17: OK
other comparators: OK
41 changes: 41 additions & 0 deletions ext/standard/tests/array/sort/packed_integer_sort_fallback.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
--TEST--
Packed sorting preserves stable comparisons for references and non-integers
--FILE--
<?php
function check($actual, $expected) {
if ($actual !== $expected) {
throw new Exception(var_export([$actual, $expected], true));
}
}

foreach (['sort', 'rsort'] as $sort) {
$left = $right = 1;
$values = [&$left, 2, &$right];
$sort($values);
$left = 11;
$right = 12;
check($values, $sort === 'sort' ? [11, 12, 2] : [2, 11, 12]);

$values = [1.0, 1, 0.5];
$sort($values);
check($values, $sort === 'sort' ? [0.5, 1.0, 1] : [1.0, 1, 0.5]);

$values = ['1', '01', '2', '02'];
$sort($values);
check($values, $sort === 'sort' ? ['1', '01', '2', '02'] : ['2', '02', '1', '01']);

$values = range(16, 1);
$values[] = 0.5;
$sort($values);
check($values, $sort === 'sort' ? [0.5, ...range(1, 16)] : [...range(16, 1), 0.5]);

$values = [3, 9, 1, 2];
unset($values[1]);
$sort($values);
check($values, $sort === 'sort' ? [1, 2, 3] : [3, 2, 1]);
echo "$sort: OK\n";
}
?>
--EXPECT--
sort: OK
rsort: OK
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
--TEST--
Sorting packed integer arrays during by-reference foreach preserves iterator positions
--FILE--
<?php
function check($actual, $expected) {
if ($actual !== $expected) {
throw new Exception(var_export([$actual, $expected], true));
}
}

foreach (['sort', 'rsort'] as $sort) {
foreach ([false, true] as $removeCurrent) {
$values = [9, 3, 1, 2];
$visited = [];
$first = true;
foreach ($values as $key => &$value) {
$visited[] = [$key, $value];
if ($first) {
$first = false;
if ($removeCurrent) {
// Remove the referenced element while keeping the iterator active.
array_shift($values);
foreach (array_keys($values) as $index) {
check(ReflectionReference::fromArrayElement($values, $index), null);
}
}
$sort($values);
}
}
unset($value);
if ($removeCurrent) {
$expected = $sort === 'sort' ? [1, 2, 3] : [3, 2, 1];
check($visited, [[0, 9], [0, $expected[0]], [1, 2], [2, $expected[2]]]);
} else {
$expected = $sort === 'sort' ? [1, 2, 3, 9] : [9, 3, 2, 1];
check($visited, [[0, 9], [1, $expected[1]], [2, $expected[2]], [3, $expected[3]]]);
}
check($values, $expected);
}
echo "$sort: OK\n";
}
?>
--EXPECT--
sort: OK
rsort: OK
54 changes: 54 additions & 0 deletions ext/standard/tests/array/sort/packed_integer_sort_metadata.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
--TEST--
Packed integer sorting resets array metadata and preserves copy on write
--FILE--
<?php
function check($actual, $expected) {
if ($actual !== $expected) {
throw new Exception(var_export([$actual, $expected], true));
}
}

foreach (['sort', 'rsort'] as $sort) {
$values = [3, 1, 2, 9];
unset($values[3]);
end($values);
$sort($values);
$expected = $sort === 'sort' ? [1, 2, 3] : [3, 2, 1];
check($values, $expected);
check(key($values), 0);
check(current($values), $expected[0]);
$values[] = 99;
check($values, [...$expected, 99]);

$values = [3, 1, 2];
unset($values[2]);
$values[] = 2;
$sort($values);
$values[] = 99;
check($values, [...$expected, 99]);

$original = [3, 1, 2];
$values = $original;
$alias = &$values;
$sort($alias);
check($values, $expected);
check($original, [3, 1, 2]);
unset($alias);

$values = [11, 22];
unset($values[1], $values[0]);
$sort($values);
$values[] = 123;
check($values, [2 => 123]);

$values = [11, 22];
unset($values[0]);
$sort($values);
$values[] = 123;
check($values, [22, 123]);
echo "$sort: OK\n";
}
?>
--EXPECT--
sort: OK
rsort: OK
Loading