Skip to content
Open
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 NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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_chunk() when not preserving keys.
(mehmetcansahin)

- SimpleXML:
. Fixed writing to a dimension of the object returned by attributes() not
Expand Down
1 change: 1 addition & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,7 @@ PHP 8.6 UPGRADE NOTES
. Reduced temporary allocations when iterating Phar directories.

- Standard:
. Improved performance of array_chunk() when not preserving keys.
. Improved performance of sorting single-element arrays.
. Improved performance of array_fill_keys().
. Improved performance of array_intersect().
Expand Down
48 changes: 40 additions & 8 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -7293,22 +7293,54 @@ PHP_FUNCTION(array_chunk)
array_init_size(return_value, (uint32_t)(((num_in - 1) / size) + 1));
zend_hash_real_init_packed(Z_ARRVAL_P(return_value));

if (!preserve_keys) {
/* Every chunk is a list of exactly `size` elements (the last one possibly
* shorter), so each one can be filled directly. The input is walked by
* element pointer, so the packed/hash stride is computed only once. */
HashTable *ht = Z_ARRVAL_P(input);
uint32_t elem_size = ZEND_HASH_ELEMENT_SIZE(ht);
zval *zv = ht->arPacked;
uint32_t remaining = (uint32_t)num_in;

while (remaining > 0) {
uint32_t chunk_size = MIN((uint32_t)size, remaining);

array_init_size(&chunk, chunk_size);
zend_hash_real_init_packed(Z_ARRVAL(chunk));
ZEND_HASH_FILL_PACKED(Z_ARRVAL(chunk)) {
uint32_t n = 0;
while (n < chunk_size) {
entry = zv;
zv = ZEND_HASH_NEXT_ELEMENT(zv, elem_size);
if (UNEXPECTED(Z_TYPE_P(entry) == IS_UNDEF)) {
continue;
}
if (UNEXPECTED(Z_ISREF_P(entry)) && Z_REFCOUNT_P(entry) == 1) {
entry = Z_REFVAL_P(entry);
}
Z_TRY_ADDREF_P(entry);
ZEND_HASH_FILL_ADD(entry);
n++;
}
} ZEND_HASH_FILL_END();
zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &chunk);
remaining -= chunk_size;
}
return;
}

ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(input), num_key, str_key, entry) {
/* If new chunk, create and initialize it. */
if (current == 0) {
array_init_size(&chunk, (uint32_t)size);
add_next_index_zval(return_value, &chunk);
}

/* Add entry to the chunk, preserving keys if necessary. */
if (preserve_keys) {
if (str_key) {
entry = zend_hash_add_new(Z_ARRVAL(chunk), str_key, entry);
} else {
entry = zend_hash_index_add_new(Z_ARRVAL(chunk), num_key, entry);
}
/* Add entry to the chunk, preserving keys. */
if (str_key) {
entry = zend_hash_add_new(Z_ARRVAL(chunk), str_key, entry);
} else {
entry = zend_hash_next_index_insert(Z_ARRVAL(chunk), entry);
entry = zend_hash_index_add_new(Z_ARRVAL(chunk), num_key, entry);
}
zval_add_ref(entry);

Expand Down
200 changes: 200 additions & 0 deletions ext/standard/tests/array/array_chunk_holes_and_refs.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
--TEST--
array_chunk() on arrays with holes and on referenced values
--FILE--
<?php
// Packed array that keeps IS_UNDEF holes after unset()
$a = [0, 1, 2, 3, 4, 5, 6, 7];
unset($a[0], $a[3], $a[7]);
var_dump(array_chunk($a, 2));
var_dump(array_chunk($a, 2, true));

// String-keyed hash with holes: chunks are compacted lists
$h = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5];
unset($h['b'], $h['e']);
var_dump(array_chunk($h, 2));
var_dump(array_chunk($h, 2, true));

// Last chunk shorter than size
var_dump(array_chunk([1, 2, 3, 4, 5], 3));

// Chunk size larger than the array
var_dump(array_chunk([1, 2], 10));

// A singly-referenced value is unwrapped like a plain copy:
// later writes to the input must not leak into the chunks
$b = [1, 2, 3];
$r = &$b[1];
unset($r);
$c = array_chunk($b, 2);
$b[1] = 99;
var_dump($c);

// Unwrapped refcounted payloads (object, array, string) must be retained by
// the chunk: the input is released first, and the destructor must run only
// once the chunk is released too
class Dtor {
public function __destruct() { echo "Dtor destroyed\n"; }
}
$e = [new Dtor, range(1, 2), str_repeat('s', 3)];
foreach ($e as &$v) {}
unset($v);
$c = array_chunk($e, 2);
unset($e);
echo "input released\n";
var_dump($c);
unset($c);
echo "chunks released\n";

// Live references are preserved and kept alive by the chunk
$d = [1, 2];
$live = &$d[0];
$c = array_chunk($d, 2);
unset($d);
$live = 42;
var_dump($c);
unset($live);
?>
--EXPECT--
array(3) {
[0]=>
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
[1]=>
array(2) {
[0]=>
int(4)
[1]=>
int(5)
}
[2]=>
array(1) {
[0]=>
int(6)
}
}
array(3) {
[0]=>
array(2) {
[1]=>
int(1)
[2]=>
int(2)
}
[1]=>
array(2) {
[4]=>
int(4)
[5]=>
int(5)
}
[2]=>
array(1) {
[6]=>
int(6)
}
}
array(2) {
[0]=>
array(2) {
[0]=>
int(1)
[1]=>
int(3)
}
[1]=>
array(1) {
[0]=>
int(4)
}
}
array(2) {
[0]=>
array(2) {
["a"]=>
int(1)
["c"]=>
int(3)
}
[1]=>
array(1) {
["d"]=>
int(4)
}
}
array(2) {
[0]=>
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
[1]=>
array(2) {
[0]=>
int(4)
[1]=>
int(5)
}
}
array(1) {
[0]=>
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
}
array(2) {
[0]=>
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
[1]=>
array(1) {
[0]=>
int(3)
}
}
input released
array(2) {
[0]=>
array(2) {
[0]=>
object(Dtor)#1 (0) {
}
[1]=>
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
}
[1]=>
array(1) {
[0]=>
string(3) "sss"
}
}
Dtor destroyed
chunks released
array(1) {
[0]=>
array(2) {
[0]=>
&int(42)
[1]=>
int(2)
}
}
Loading