Skip to content

Commit 0d2cf57

Browse files
PHP array_shift(): Optimize tail move on hole-free packed arrays
When a packed array has no holes and no active iterators, the slot vacated by the shifted element is the only hole, so the tail can be moved down with a single memmove instead of testing every slot for IS_UNDEF and comparing source and destination indexes. A tail of a single element is moved directly instead, avoiding the memmove call overhead on tiny arrays.
1 parent f142b81 commit 0d2cf57

4 files changed

Lines changed: 112 additions & 8 deletions

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ PHP NEWS
5454
. Fixed read buffer compaction in php_stream_filter_flush(). (crystarm)
5555
. Io\Poll\Context::wait() now rejects a $maxEvents value greater than
5656
INT_MAX instead of truncating it. (marc-mabe)
57+
. Improved performance of array_shift() on packed arrays without holes.
58+
(mehmetcansahin)
5759

5860
- SimpleXML:
5961
. Fixed writing to a dimension of the object returned by attributes() not

UPGRADING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,7 @@ PHP 8.6 UPGRADE NOTES
977977
. Improved performance of array_fill_keys().
978978
. Improved performance of array_intersect().
979979
. Improved performance of array_map() with multiple arrays passed.
980+
. Improved performance of array_shift() on packed arrays without holes.
980981
. Improved performance of array_sum() and array_product() for
981982
integer-only arrays.
982983
. Improved performance of array_unshift().

ext/standard/array.c

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3576,6 +3576,8 @@ PHP_FUNCTION(array_shift)
35763576
/* re-index like it did before */
35773577
if (HT_IS_PACKED(Z_ARRVAL_P(stack))) {
35783578
uint32_t k = 0;
3579+
/* Must be read before the deletion below opens a hole. */
3580+
bool without_holes = HT_IS_WITHOUT_HOLES(Z_ARRVAL_P(stack));
35793581

35803582
/* Get the first value and copy it into the return value */
35813583
idx = 0;
@@ -3596,15 +3598,32 @@ PHP_FUNCTION(array_shift)
35963598
zend_hash_packed_del_val(Z_ARRVAL_P(stack), val);
35973599

35983600
if (EXPECTED(!HT_HAS_ITERATORS(Z_ARRVAL_P(stack)))) {
3599-
for (idx = 0; idx < Z_ARRVAL_P(stack)->nNumUsed; idx++) {
3600-
val = Z_ARRVAL_P(stack)->arPacked + idx;
3601-
if (Z_TYPE_P(val) == IS_UNDEF) continue;
3602-
if (idx != k) {
3603-
zval *q = Z_ARRVAL_P(stack)->arPacked + k;
3604-
ZVAL_COPY_VALUE(q, val);
3605-
ZVAL_UNDEF(val);
3601+
if (without_holes) {
3602+
/* The slot just vacated is the only hole, so the rest moves down by one
3603+
* without having to test every slot. */
3604+
HashTable *ht = Z_ARRVAL_P(stack);
3605+
3606+
if (ht->nNumUsed > 0) {
3607+
k = ht->nNumUsed - 1;
3608+
if (k == 1) {
3609+
/* Avoid the memmove() call overhead for the common tiny-array case. */
3610+
ZVAL_COPY_VALUE(ht->arPacked, ht->arPacked + 1);
3611+
} else {
3612+
memmove(ht->arPacked, ht->arPacked + 1, sizeof(zval) * k);
3613+
}
3614+
ZVAL_UNDEF(ht->arPacked + k);
3615+
}
3616+
} else {
3617+
for (idx = 0; idx < Z_ARRVAL_P(stack)->nNumUsed; idx++) {
3618+
val = Z_ARRVAL_P(stack)->arPacked + idx;
3619+
if (Z_TYPE_P(val) == IS_UNDEF) continue;
3620+
if (idx != k) {
3621+
zval *q = Z_ARRVAL_P(stack)->arPacked + k;
3622+
ZVAL_COPY_VALUE(q, val);
3623+
ZVAL_UNDEF(val);
3624+
}
3625+
k++;
36063626
}
3607-
k++;
36083627
}
36093628
} else {
36103629
uint32_t iter_pos = zend_hash_iterators_lower_pos(Z_ARRVAL_P(stack), 0);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
--TEST--
2+
array_shift() on packed arrays: hole-free fast path and holed fallback
3+
--FILE--
4+
<?php
5+
// Hole-free tail move (tail longer than one element).
6+
$a = [1, "two", 3, 4, 5];
7+
var_dump(array_shift($a), $a);
8+
$a[] = 6; // The next appended key must follow the compacted tail.
9+
var_dump($a);
10+
11+
// Hole-free two-element array (single-element tail).
12+
$a = [10, 20];
13+
var_dump(array_shift($a), $a);
14+
15+
// Hole-free single element: shifts to empty, the next appended key is 0.
16+
$a = [42];
17+
var_dump(array_shift($a), $a);
18+
$a[] = 7;
19+
var_dump($a);
20+
21+
// Holes at the head and in the middle: the compaction loop must skip them.
22+
$a = [1, 2, 3, 4, 5];
23+
unset($a[0], $a[2]);
24+
var_dump(array_shift($a), $a);
25+
26+
// Shift a holed array down to empty: the next appended key must be 0.
27+
$a = [10, 20];
28+
unset($a[0]);
29+
var_dump(array_shift($a), $a);
30+
$a[] = 30;
31+
var_dump($a);
32+
?>
33+
--EXPECT--
34+
int(1)
35+
array(4) {
36+
[0]=>
37+
string(3) "two"
38+
[1]=>
39+
int(3)
40+
[2]=>
41+
int(4)
42+
[3]=>
43+
int(5)
44+
}
45+
array(5) {
46+
[0]=>
47+
string(3) "two"
48+
[1]=>
49+
int(3)
50+
[2]=>
51+
int(4)
52+
[3]=>
53+
int(5)
54+
[4]=>
55+
int(6)
56+
}
57+
int(10)
58+
array(1) {
59+
[0]=>
60+
int(20)
61+
}
62+
int(42)
63+
array(0) {
64+
}
65+
array(1) {
66+
[0]=>
67+
int(7)
68+
}
69+
int(2)
70+
array(2) {
71+
[0]=>
72+
int(4)
73+
[1]=>
74+
int(5)
75+
}
76+
int(20)
77+
array(0) {
78+
}
79+
array(1) {
80+
[0]=>
81+
int(30)
82+
}

0 commit comments

Comments
 (0)