From a667208a4da8fd74f25e2e87437749fa989e7d6c Mon Sep 17 00:00:00 2001 From: mehmetcansahin Date: Wed, 21 Oct 2026 11:13:47 +0300 Subject: [PATCH] Optimize array_unshift() for packed arrays Packed arrays without holes and without iterators are shifted in place when rebuilding would allocate a table of the same size. Otherwise the array is still rebuilt, so unused slots are released. --- NEWS | 2 + UPGRADING | 1 + ext/standard/array.c | 63 +++++++---- .../tests/array/array_unshift_packed.phpt | 102 ++++++++++++++++++ 4 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 ext/standard/tests/array/array_unshift_packed.phpt diff --git a/NEWS b/NEWS index 67c44a4420e9..00ec0ae9e775 100644 --- a/NEWS +++ b/NEWS @@ -145,6 +145,8 @@ PHP NEWS especially UTF-8. (Nicolas Grekas) . Improved performance of htmlspecialchars() when nothing needs encoding. (Nicolas Grekas) + . Improved performance of array_unshift() on packed arrays without holes. + (mehmetcansahin) - URI: . Fix casing of enum cases in UriHostType and UrlHostType to match the RFC diff --git a/UPGRADING b/UPGRADING index fd3efda9df7a..d7dc3ca86a98 100644 --- a/UPGRADING +++ b/UPGRADING @@ -123,6 +123,7 @@ PHP 8.7 UPGRADE NOTES . Improved performance of array_chunk() when not preserving keys. . Improved performance of array_splice() when inserting without removing elements. + . Improved performance of array_unshift() on packed arrays without holes. . Improved performance of str_rot13(). . Improved performance of pack() for large strings using the a, A and Z formats. diff --git a/ext/standard/array.c b/ext/standard/array.c index 0b184efc3998..8f3afe2248a9 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -3696,9 +3696,30 @@ PHP_FUNCTION(array_unshift) Z_PARAM_VARIADIC('+', args, argc) ZEND_PARSE_PARAMETERS_END(); - zend_hash_init(&new_hash, zend_hash_num_elements(Z_ARRVAL_P(stack)) + argc, NULL, ZVAL_PTR_DTOR, 0); + HashTable *ht = Z_ARRVAL_P(stack); + uint32_t new_count = zend_hash_num_elements(ht) + argc; - if (HT_IS_PACKED(Z_ARRVAL_P(stack))) { + /* The rebuild below allocates the smallest power of two that fits new_count, + * but at least HT_MIN_SIZE slots. If the current table has that size, a + * packed array without holes can be shifted in place instead. */ + bool same_table_size = new_count <= ht->nTableSize + && (new_count > ht->nTableSize / 2 || ht->nTableSize == HT_MIN_SIZE); + + if (HT_IS_PACKED(ht) && HT_IS_WITHOUT_HOLES(ht) && !HT_HAS_ITERATORS(ht) && same_table_size) { + memmove(ht->arPacked + argc, ht->arPacked, ht->nNumUsed * sizeof(zval)); + for (uint32_t i = 0; i < argc; i++) { + ZVAL_COPY(&ht->arPacked[i], &args[i]); + } + ht->nNumUsed = new_count; + ht->nNumOfElements = new_count; + ht->nNextFreeElement = new_count; + ht->nInternalPointer = 0; + RETURN_LONG(new_count); + } + + zend_hash_init(&new_hash, new_count, NULL, ZVAL_PTR_DTOR, 0); + + if (HT_IS_PACKED(ht)) { zend_hash_real_init_packed(&new_hash); ZEND_HASH_FILL_PACKED(&new_hash) { @@ -3707,7 +3728,7 @@ PHP_FUNCTION(array_unshift) ZEND_HASH_FILL_ADD(&args[i]); } - ZEND_HASH_PACKED_FOREACH_VAL(Z_ARRVAL_P(stack), value) { + ZEND_HASH_PACKED_FOREACH_VAL(ht, value) { ZEND_HASH_FILL_ADD(value); } ZEND_HASH_FOREACH_END(); } ZEND_HASH_FILL_END(); @@ -3717,7 +3738,7 @@ PHP_FUNCTION(array_unshift) zend_hash_next_index_insert_new(&new_hash, &args[i]); } - ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(stack), key, value) { + ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(ht, key, value) { if (key) { zend_hash_add_new(&new_hash, key, value); } else { @@ -3726,29 +3747,29 @@ PHP_FUNCTION(array_unshift) } ZEND_HASH_FOREACH_END(); } - if (UNEXPECTED(HT_HAS_ITERATORS(Z_ARRVAL_P(stack)))) { - zend_hash_iterators_advance(Z_ARRVAL_P(stack), argc); - HT_SET_ITERATORS_COUNT(&new_hash, HT_ITERATORS_COUNT(Z_ARRVAL_P(stack))); - HT_SET_ITERATORS_COUNT(Z_ARRVAL_P(stack), 0); + if (UNEXPECTED(HT_HAS_ITERATORS(ht))) { + zend_hash_iterators_advance(ht, argc); + HT_SET_ITERATORS_COUNT(&new_hash, HT_ITERATORS_COUNT(ht)); + HT_SET_ITERATORS_COUNT(ht, 0); } /* replace HashTable data */ - Z_ARRVAL_P(stack)->pDestructor = NULL; - zend_hash_destroy(Z_ARRVAL_P(stack)); - - HT_FLAGS(Z_ARRVAL_P(stack)) = HT_FLAGS(&new_hash); - Z_ARRVAL_P(stack)->nTableSize = new_hash.nTableSize; - Z_ARRVAL_P(stack)->nTableMask = new_hash.nTableMask; - Z_ARRVAL_P(stack)->nNumUsed = new_hash.nNumUsed; - Z_ARRVAL_P(stack)->nNumOfElements = new_hash.nNumOfElements; - Z_ARRVAL_P(stack)->nNextFreeElement = new_hash.nNextFreeElement; - Z_ARRVAL_P(stack)->arData = new_hash.arData; - Z_ARRVAL_P(stack)->pDestructor = new_hash.pDestructor; + ht->pDestructor = NULL; + zend_hash_destroy(ht); - zend_hash_internal_pointer_reset(Z_ARRVAL_P(stack)); + HT_FLAGS(ht) = HT_FLAGS(&new_hash); + ht->nTableSize = new_hash.nTableSize; + ht->nTableMask = new_hash.nTableMask; + ht->nNumUsed = new_hash.nNumUsed; + ht->nNumOfElements = new_hash.nNumOfElements; + ht->nNextFreeElement = new_hash.nNextFreeElement; + ht->arData = new_hash.arData; + ht->pDestructor = new_hash.pDestructor; + + zend_hash_internal_pointer_reset(ht); /* Clean up and return the number of elements in the stack */ - RETVAL_LONG(zend_hash_num_elements(Z_ARRVAL_P(stack))); + RETVAL_LONG(zend_hash_num_elements(ht)); } /* }}} */ diff --git a/ext/standard/tests/array/array_unshift_packed.phpt b/ext/standard/tests/array/array_unshift_packed.phpt new file mode 100644 index 000000000000..ca57a822bf5e --- /dev/null +++ b/ext/standard/tests/array/array_unshift_packed.phpt @@ -0,0 +1,102 @@ +--TEST-- +array_unshift() on packed arrays +--FILE-- + +--EXPECT-- +int(16) +[-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14] key: 0 +int(17) +[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] key: 0 +int(4) +[-1,0,2,3] key: 0 +int(4) +[-1,0,1,2,"next"] key: 0 +int(2) +[0,1,"next"] key: 0 +int(2) +[1,2,"next"] key: 0 +1 +2 +3 +[0,1,2,3] key: 0 +[0,10,2] key: 0 +[10,2] key: 0 +unshifted +destroyed