Skip to content

Commit 5e2e4c4

Browse files
Optimize array_chunk() by filling packed chunks directly
When $preserve_keys is not passed, every chunk is a list of at most $size elements, so each chunk can be built with ZEND_HASH_FILL_PACKED instead of one zend_hash_next_index_insert() call per element.
1 parent ec93b0b commit 5e2e4c4

4 files changed

Lines changed: 243 additions & 8 deletions

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ PHP NEWS
3636
. Fixed read buffer compaction in php_stream_filter_flush(). (crystarm)
3737
. Io\Poll\Context::wait() now rejects a $maxEvents value greater than
3838
INT_MAX instead of truncating it. (marc-mabe)
39+
. Improved performance of array_chunk() when not preserving keys.
40+
(mehmetcansahin)
3941

4042

4143
27 Aug 2026, PHP 8.6.0beta2

UPGRADING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,7 @@ PHP 8.6 UPGRADE NOTES
959959
. Reduced temporary allocations when iterating Phar directories.
960960

961961
- Standard:
962+
. Improved performance of array_chunk() when not preserving keys.
962963
. Improved performance of array_fill_keys().
963964
. Improved performance of array_intersect().
964965
. Improved performance of array_map() with multiple arrays passed.

ext/standard/array.c

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7293,22 +7293,54 @@ PHP_FUNCTION(array_chunk)
72937293
array_init_size(return_value, (uint32_t)(((num_in - 1) / size) + 1));
72947294
zend_hash_real_init_packed(Z_ARRVAL_P(return_value));
72957295

7296+
if (!preserve_keys) {
7297+
/* Every chunk is a list of exactly `size` elements (the last one possibly
7298+
* shorter), so each one can be filled directly. The input is walked by
7299+
* element pointer, so the packed/hash stride is computed only once. */
7300+
HashTable *ht = Z_ARRVAL_P(input);
7301+
uint32_t elem_size = ZEND_HASH_ELEMENT_SIZE(ht);
7302+
zval *zv = ht->arPacked;
7303+
uint32_t remaining = (uint32_t)num_in;
7304+
7305+
while (remaining > 0) {
7306+
uint32_t chunk_size = MIN((uint32_t)size, remaining);
7307+
7308+
array_init_size(&chunk, chunk_size);
7309+
zend_hash_real_init_packed(Z_ARRVAL(chunk));
7310+
ZEND_HASH_FILL_PACKED(Z_ARRVAL(chunk)) {
7311+
uint32_t n = 0;
7312+
while (n < chunk_size) {
7313+
entry = zv;
7314+
zv = ZEND_HASH_NEXT_ELEMENT(zv, elem_size);
7315+
if (UNEXPECTED(Z_TYPE_P(entry) == IS_UNDEF)) {
7316+
continue;
7317+
}
7318+
if (UNEXPECTED(Z_ISREF_P(entry)) && Z_REFCOUNT_P(entry) == 1) {
7319+
entry = Z_REFVAL_P(entry);
7320+
}
7321+
Z_TRY_ADDREF_P(entry);
7322+
ZEND_HASH_FILL_ADD(entry);
7323+
n++;
7324+
}
7325+
} ZEND_HASH_FILL_END();
7326+
zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &chunk);
7327+
remaining -= chunk_size;
7328+
}
7329+
return;
7330+
}
7331+
72967332
ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(input), num_key, str_key, entry) {
72977333
/* If new chunk, create and initialize it. */
72987334
if (current == 0) {
72997335
array_init_size(&chunk, (uint32_t)size);
73007336
add_next_index_zval(return_value, &chunk);
73017337
}
73027338

7303-
/* Add entry to the chunk, preserving keys if necessary. */
7304-
if (preserve_keys) {
7305-
if (str_key) {
7306-
entry = zend_hash_add_new(Z_ARRVAL(chunk), str_key, entry);
7307-
} else {
7308-
entry = zend_hash_index_add_new(Z_ARRVAL(chunk), num_key, entry);
7309-
}
7339+
/* Add entry to the chunk, preserving keys. */
7340+
if (str_key) {
7341+
entry = zend_hash_add_new(Z_ARRVAL(chunk), str_key, entry);
73107342
} else {
7311-
entry = zend_hash_next_index_insert(Z_ARRVAL(chunk), entry);
7343+
entry = zend_hash_index_add_new(Z_ARRVAL(chunk), num_key, entry);
73127344
}
73137345
zval_add_ref(entry);
73147346

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
--TEST--
2+
array_chunk() on arrays with holes and on referenced values
3+
--FILE--
4+
<?php
5+
// Packed array that keeps IS_UNDEF holes after unset()
6+
$a = [0, 1, 2, 3, 4, 5, 6, 7];
7+
unset($a[0], $a[3], $a[7]);
8+
var_dump(array_chunk($a, 2));
9+
var_dump(array_chunk($a, 2, true));
10+
11+
// String-keyed hash with holes: chunks are compacted lists
12+
$h = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5];
13+
unset($h['b'], $h['e']);
14+
var_dump(array_chunk($h, 2));
15+
var_dump(array_chunk($h, 2, true));
16+
17+
// Last chunk shorter than size
18+
var_dump(array_chunk([1, 2, 3, 4, 5], 3));
19+
20+
// Chunk size larger than the array
21+
var_dump(array_chunk([1, 2], 10));
22+
23+
// A singly-referenced value is unwrapped like a plain copy:
24+
// later writes to the input must not leak into the chunks
25+
$b = [1, 2, 3];
26+
$r = &$b[1];
27+
unset($r);
28+
$c = array_chunk($b, 2);
29+
$b[1] = 99;
30+
var_dump($c);
31+
32+
// Unwrapped refcounted payloads (object, array, string) must be retained by
33+
// the chunk: the input is released first, and the destructor must run only
34+
// once the chunk is released too
35+
class Dtor {
36+
public function __destruct() { echo "Dtor destroyed\n"; }
37+
}
38+
$e = [new Dtor, range(1, 2), str_repeat('s', 3)];
39+
foreach ($e as &$v) {}
40+
unset($v);
41+
$c = array_chunk($e, 2);
42+
unset($e);
43+
echo "input released\n";
44+
var_dump($c);
45+
unset($c);
46+
echo "chunks released\n";
47+
48+
// Live references are preserved and kept alive by the chunk
49+
$d = [1, 2];
50+
$live = &$d[0];
51+
$c = array_chunk($d, 2);
52+
unset($d);
53+
$live = 42;
54+
var_dump($c);
55+
unset($live);
56+
?>
57+
--EXPECT--
58+
array(3) {
59+
[0]=>
60+
array(2) {
61+
[0]=>
62+
int(1)
63+
[1]=>
64+
int(2)
65+
}
66+
[1]=>
67+
array(2) {
68+
[0]=>
69+
int(4)
70+
[1]=>
71+
int(5)
72+
}
73+
[2]=>
74+
array(1) {
75+
[0]=>
76+
int(6)
77+
}
78+
}
79+
array(3) {
80+
[0]=>
81+
array(2) {
82+
[1]=>
83+
int(1)
84+
[2]=>
85+
int(2)
86+
}
87+
[1]=>
88+
array(2) {
89+
[4]=>
90+
int(4)
91+
[5]=>
92+
int(5)
93+
}
94+
[2]=>
95+
array(1) {
96+
[6]=>
97+
int(6)
98+
}
99+
}
100+
array(2) {
101+
[0]=>
102+
array(2) {
103+
[0]=>
104+
int(1)
105+
[1]=>
106+
int(3)
107+
}
108+
[1]=>
109+
array(1) {
110+
[0]=>
111+
int(4)
112+
}
113+
}
114+
array(2) {
115+
[0]=>
116+
array(2) {
117+
["a"]=>
118+
int(1)
119+
["c"]=>
120+
int(3)
121+
}
122+
[1]=>
123+
array(1) {
124+
["d"]=>
125+
int(4)
126+
}
127+
}
128+
array(2) {
129+
[0]=>
130+
array(3) {
131+
[0]=>
132+
int(1)
133+
[1]=>
134+
int(2)
135+
[2]=>
136+
int(3)
137+
}
138+
[1]=>
139+
array(2) {
140+
[0]=>
141+
int(4)
142+
[1]=>
143+
int(5)
144+
}
145+
}
146+
array(1) {
147+
[0]=>
148+
array(2) {
149+
[0]=>
150+
int(1)
151+
[1]=>
152+
int(2)
153+
}
154+
}
155+
array(2) {
156+
[0]=>
157+
array(2) {
158+
[0]=>
159+
int(1)
160+
[1]=>
161+
int(2)
162+
}
163+
[1]=>
164+
array(1) {
165+
[0]=>
166+
int(3)
167+
}
168+
}
169+
input released
170+
array(2) {
171+
[0]=>
172+
array(2) {
173+
[0]=>
174+
object(Dtor)#1 (0) {
175+
}
176+
[1]=>
177+
array(2) {
178+
[0]=>
179+
int(1)
180+
[1]=>
181+
int(2)
182+
}
183+
}
184+
[1]=>
185+
array(1) {
186+
[0]=>
187+
string(3) "sss"
188+
}
189+
}
190+
Dtor destroyed
191+
chunks released
192+
array(1) {
193+
[0]=>
194+
array(2) {
195+
[0]=>
196+
&int(42)
197+
[1]=>
198+
int(2)
199+
}
200+
}

0 commit comments

Comments
 (0)