diff --git a/NEWS b/NEWS index 653bcff9e05a..27ca71031f78 100644 --- a/NEWS +++ b/NEWS @@ -54,6 +54,8 @@ PHP NEWS . Fixed read buffer compaction in php_stream_filter_flush(). (crystarm) . Io\Poll\Context::wait() now rejects a $maxEvents value greater than INT_MAX instead of truncating it. (marc-mabe) + . Improved performance of array_shift() on packed arrays without holes. + (mehmetcansahin) - SimpleXML: . Fixed writing to a dimension of the object returned by attributes() not diff --git a/UPGRADING b/UPGRADING index 5a5cafc0234f..e0874e76e09e 100644 --- a/UPGRADING +++ b/UPGRADING @@ -977,6 +977,7 @@ PHP 8.6 UPGRADE NOTES . Improved performance of array_fill_keys(). . Improved performance of array_intersect(). . Improved performance of array_map() with multiple arrays passed. + . Improved performance of array_shift() on packed arrays without holes. . Improved performance of array_sum() and array_product() for integer-only arrays. . Improved performance of array_unshift(). diff --git a/ext/standard/array.c b/ext/standard/array.c index acf65c07bd67..cfb9e6fca1fa 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -3576,6 +3576,8 @@ PHP_FUNCTION(array_shift) /* re-index like it did before */ if (HT_IS_PACKED(Z_ARRVAL_P(stack))) { uint32_t k = 0; + /* Must be read before the deletion below opens a hole. */ + bool without_holes = HT_IS_WITHOUT_HOLES(Z_ARRVAL_P(stack)); /* Get the first value and copy it into the return value */ idx = 0; @@ -3596,15 +3598,32 @@ PHP_FUNCTION(array_shift) zend_hash_packed_del_val(Z_ARRVAL_P(stack), val); if (EXPECTED(!HT_HAS_ITERATORS(Z_ARRVAL_P(stack)))) { - for (idx = 0; idx < Z_ARRVAL_P(stack)->nNumUsed; idx++) { - val = Z_ARRVAL_P(stack)->arPacked + idx; - if (Z_TYPE_P(val) == IS_UNDEF) continue; - if (idx != k) { - zval *q = Z_ARRVAL_P(stack)->arPacked + k; - ZVAL_COPY_VALUE(q, val); - ZVAL_UNDEF(val); + if (without_holes) { + /* The slot just vacated is the only hole, so the rest moves down by one + * without having to test every slot. */ + HashTable *ht = Z_ARRVAL_P(stack); + + if (ht->nNumUsed > 0) { + k = ht->nNumUsed - 1; + if (k == 1) { + /* Avoid the memmove() call overhead for the common tiny-array case. */ + ZVAL_COPY_VALUE(ht->arPacked, ht->arPacked + 1); + } else { + memmove(ht->arPacked, ht->arPacked + 1, sizeof(zval) * k); + } + ZVAL_UNDEF(ht->arPacked + k); + } + } else { + for (idx = 0; idx < Z_ARRVAL_P(stack)->nNumUsed; idx++) { + val = Z_ARRVAL_P(stack)->arPacked + idx; + if (Z_TYPE_P(val) == IS_UNDEF) continue; + if (idx != k) { + zval *q = Z_ARRVAL_P(stack)->arPacked + k; + ZVAL_COPY_VALUE(q, val); + ZVAL_UNDEF(val); + } + k++; } - k++; } } else { uint32_t iter_pos = zend_hash_iterators_lower_pos(Z_ARRVAL_P(stack), 0); diff --git a/ext/standard/tests/array/array_shift_packed.phpt b/ext/standard/tests/array/array_shift_packed.phpt new file mode 100644 index 000000000000..438f75783359 --- /dev/null +++ b/ext/standard/tests/array/array_shift_packed.phpt @@ -0,0 +1,82 @@ +--TEST-- +array_shift() on packed arrays: hole-free fast path and holed fallback +--FILE-- + +--EXPECT-- +int(1) +array(4) { + [0]=> + string(3) "two" + [1]=> + int(3) + [2]=> + int(4) + [3]=> + int(5) +} +array(5) { + [0]=> + string(3) "two" + [1]=> + int(3) + [2]=> + int(4) + [3]=> + int(5) + [4]=> + int(6) +} +int(10) +array(1) { + [0]=> + int(20) +} +int(42) +array(0) { +} +array(1) { + [0]=> + int(7) +} +int(2) +array(2) { + [0]=> + int(4) + [1]=> + int(5) +} +int(20) +array(0) { +} +array(1) { + [0]=> + int(30) +}