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)
+  }
+}