Commit 6fc4017a940 for php
commit 6fc4017a9406ecc71890bb85830a23057963a562
Author: mehmetcansahin <sahinmcan@gmail.com>
Date: Thu Sep 3 16:09:15 2026 +0300
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
diff --git a/NEWS b/NEWS
index cc86611e0a3..9c5f7bede78 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 05ae834590c..d8d763986d0 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 f276996eda7..0b184efc399 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 00000000000..81fbcb68cec
--- /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)
+ }
+}