diff --git a/NEWS b/NEWS index e507963f2059..d2ecea263f60 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/UPGRADING b/UPGRADING index feaec16c4826..b2f3b0839cf1 100644 --- a/UPGRADING +++ b/UPGRADING @@ -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(). diff --git a/ext/standard/array.c b/ext/standard/array.c index acf65c07bd67..4922145363e1 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -7293,6 +7293,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) { @@ -7300,15 +7336,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-- + 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) + } +}