[php-src] master: ext/standard: Optimize array_chunk() by filling packed chunks directly

From: Date: Thu, 08 Oct 2026 05:36:20 +0000
Subject: [php-src] master: ext/standard: Optimize array_chunk() by filling packed chunks directly
Groups: php.cvs 
Request: Send a blank email to php-cvs+get-142076@lists.php.net to get a copy of this message
Author: mehmetcansahin (mehmetcansahin) Committer: David Carlier (devnexen) Date: 2026-10-08T06:30:43+01:00 Commit: https://github.com/php/php-src/commit/6fc4017a9406ecc71890bb85830a23057963a562 Raw diff: https://github.com/php/php-src/commit/6fc4017a9406ecc71890bb85830a23057963a562.diff ext/standard: 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. Close GH-23556 Changed paths: A ext/standard/tests/array/array_chunk_holes_and_refs.phpt M NEWS M UPGRADING M ext/standard/array.c Diff: diff --git a/NEWS b/NEWS index cc86611e0a3f..9c5f7bede786 100644 --- a/NEWS +++ b/NEWS @@ -118,6 +118,8 @@ PHP NEWS elements. (mehmetcansahin) . Enforce max_filter_count: limit the number of filters that can be chained in a php://filter URL. (Sjoerd Langkemper) + . Improved performance of array_chunk() when not preserving keys. + (mehmetcansahin) - URI: . Fix casing of enum cases in UriHostType and UrlHostType to match the RFC diff --git a/UPGRADING b/UPGRADING index 05ae834590c6..d8d763986d0f 100644 --- a/UPGRADING +++ b/UPGRADING @@ -98,6 +98,7 @@ PHP 8.7 UPGRADE NOTES - Standard: . Improved performance of strcspn() and strpbrk() when searching longer strings with a single-byte character mask. + . Improved performance of array_chunk() when not preserving keys. . Improved performance of array_splice() when inserting without removing elements. . Improved performance of str_rot13(). diff --git a/ext/standard/array.c b/ext/standard/array.c index f276996eda74..0b184efc3998 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -7328,6 +7328,42 @@ 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) { @@ -7335,15 +7371,11 @@ PHP_FUNCTION(array_chunk) 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); diff --git a/ext/standard/tests/array/array_chunk_holes_and_refs.phpt b/ext/standard/tests/array/array_chunk_holes_and_refs.phpt new file mode 100644 index 000000000000..81fbcb68ceca --- /dev/null +++ b/ext/standard/tests/array/array_chunk_holes_and_refs.phpt @@ -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) + } +}

« previous php.cvs (#142076) next »