diff --git a/NEWS b/NEWS index 9c5f7bede786..adf07f087f52 100644 --- a/NEWS +++ b/NEWS @@ -118,6 +118,7 @@ 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_push() with several values. (mehmetcansahin) . Improved performance of array_chunk() when not preserving keys. (mehmetcansahin) diff --git a/UPGRADING b/UPGRADING index d8d763986d0f..14cc382a890b 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_push() with several values. . Improved performance of array_chunk() when not preserving keys. . Improved performance of array_splice() when inserting without removing elements. diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index efcf9c00544e..b4f554dd2728 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -45,6 +45,10 @@ PHP 8.7 INTERNALS UPGRADE NOTES - The INIT_CLASS_ENTRY_INIT_METHODS() macro, which was previously used as part of handling disabled classes, was removed following the removal of the `disable_classes` INI setting in PHP 8.5. +- Added zend_hash_next_index_insert_copies(), which inserts several values like + repeated zend_hash_next_index_insert() calls and adds a reference to each + inserted value. Values appended right after the last used slot of a packed + array are stored in one go. ======================== 2. Build system changes diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 4640846ecc76..222cfc484d11 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -1246,6 +1246,49 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_insert_new(HashTable *ht, zval return _zend_hash_index_add_or_update_i(ht, ht->nNextFreeElement, pData, HASH_ADD | HASH_ADD_NEW | HASH_ADD_NEXT); } +ZEND_API uint32_t ZEND_FASTCALL zend_hash_next_index_insert_copies(HashTable *ht, zval *values, uint32_t count) +{ + IS_CONSISTENT(ht); + HT_ASSERT_RC1(ht); + + /* Several values can be stored directly after the last used slot of a + * packed or uninitialized array. */ + if (count > 1 && (HT_IS_PACKED(ht) || (HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED))) { + zend_long next = ht->nNextFreeElement == ZEND_LONG_MIN ? 0 : ht->nNextFreeElement; + bool appends_after_last = next == (zend_long) ht->nNumUsed; + uint32_t room = ht->nTableSize - ht->nNumUsed; + /* Whether a packed array with holes stays packed when it grows + * depends on how full it is (see _zend_hash_index_add_or_update_i()), + * so those go through the one-by-one path below. */ + bool can_grow = HT_IS_WITHOUT_HOLES(ht) && count <= HT_MAX_SIZE - ht->nNumUsed; + + if (appends_after_last && (count <= room || can_grow)) { + if (count > room) { + zend_hash_extend(ht, ht->nNumUsed + count, true); + } else if (HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED) { + zend_hash_real_init_packed_ex(ht); + } + + zval *zv = ht->arPacked + ht->nNumUsed; + for (uint32_t i = 0; i < count; i++) { + ZVAL_COPY(&zv[i], &values[i]); + } + ht->nNumUsed += count; + ht->nNumOfElements += count; + ht->nNextFreeElement = ht->nNumUsed; + return count; + } + } + + for (uint32_t i = 0; i < count; i++) { + if (!_zend_hash_index_add_or_update_i(ht, ht->nNextFreeElement, &values[i], HASH_ADD | HASH_ADD_NEXT)) { + return i; + } + Z_TRY_ADDREF(values[i]); + } + return count; +} + ZEND_API zval* ZEND_FASTCALL zend_hash_index_lookup(HashTable *ht, zend_ulong h) { return _zend_hash_index_add_or_update_i(ht, h, NULL, HASH_LOOKUP); diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h index c7cdbe95866e..33cc1510f380 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -138,6 +138,11 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_index_add_new(HashTable *ht, zend_ulong h ZEND_API zval* ZEND_FASTCALL zend_hash_index_update(HashTable *ht, zend_ulong h, zval *pData); ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_insert(HashTable *ht, zval *pData); ZEND_API zval* ZEND_FASTCALL zend_hash_next_index_insert_new(HashTable *ht, zval *pData); +/* Inserts the count values in order like zend_hash_next_index_insert(), and + * adds a reference to each inserted value (like ZVAL_COPY()). Returns the + * number of inserted values, which is less than count only if an insertion + * failed; the values after it are then not inserted. */ +ZEND_API uint32_t ZEND_FASTCALL zend_hash_next_index_insert_copies(HashTable *ht, zval *values, uint32_t count); ZEND_API zval* ZEND_FASTCALL zend_hash_index_add_empty_element(HashTable *ht, zend_ulong h); ZEND_API zval* ZEND_FASTCALL zend_hash_add_empty_element(HashTable *ht, zend_string *key); diff --git a/ext/standard/array.c b/ext/standard/array.c index 0b184efc3998..8730b929d35b 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -3476,15 +3476,17 @@ PHP_FUNCTION(array_push) Z_PARAM_VARIADIC('+', args, argc) ZEND_PARSE_PARAMETERS_END(); - /* For each subsequent argument, make it a reference, increase refcount, and add it to the end of the array */ - for (uint32_t i = 0; i < argc; i++) { - Z_TRY_ADDREF(args[i]); - - if (zend_hash_next_index_insert(Z_ARRVAL_P(stack), &args[i]) == NULL) { - Z_TRY_DELREF(args[i]); + /* A single value is added faster by zend_hash_next_index_insert(). */ + if (argc == 1) { + Z_TRY_ADDREF(args[0]); + if (zend_hash_next_index_insert(Z_ARRVAL_P(stack), &args[0]) == NULL) { + Z_TRY_DELREF(args[0]); zend_throw_error(NULL, "Cannot add element to the array as the next element is already occupied"); RETURN_THROWS(); } + } else if (zend_hash_next_index_insert_copies(Z_ARRVAL_P(stack), args, argc) < argc) { + zend_throw_error(NULL, "Cannot add element to the array as the next element is already occupied"); + RETURN_THROWS(); } /* Clean up and return the number of values in the stack */ diff --git a/ext/standard/tests/array/array_push_multiple_values.phpt b/ext/standard/tests/array/array_push_multiple_values.phpt new file mode 100644 index 000000000000..07e2a7218d7d --- /dev/null +++ b/ext/standard/tests/array/array_push_multiple_values.phpt @@ -0,0 +1,163 @@ +--TEST-- +array_push() with several values +--FILE-- + 0]; +unset($a[-3]); +$copy = $a; +var_dump(array_push($a, 1, 2), array_keys($a), $copy); + +echo "Internal pointer\n"; +$a = [1, 2, 3]; +next($a); +array_push($a, 4, 5); +var_dump(key($a)); + +echo "Refcounted values\n"; +$o = new stdClass; +$a = [1]; +array_push($a, $o, $o); +debug_zval_dump($o); +unset($a); +debug_zval_dump($o); + +echo "Next index already used\n"; +$a = [PHP_INT_MAX - 1 => 0]; +try { + array_push($a, $o, $o, $o); +} catch (Error $e) { + echo $e->getMessage(), "\n"; + unset($e); +} +var_dump(array_keys($a) === [PHP_INT_MAX - 1, PHP_INT_MAX]); +debug_zval_dump($o); +?> +--EXPECT-- +Room for all values +int(8) +bool(true) +One value more than the room +int(9) +bool(true) +More values than twice the table size +int(25) +bool(true) +Empty array +int(2) +bool(true) +int(9) +bool(true) +No values keep the next index of an empty array +int(0) +array(2) { + [0]=> + int(-5) + [1]=> + int(-4) +} +Hole with room +int(5) +array(5) { + [0]=> + int(0) + [1]=> + int(2) + [2]=> + int(3) + [3]=> + int(4) + [4]=> + int(5) +} +Holes without room +int(4) +NULL +array(4) { + [0]=> + int(14) + [1]=> + int(15) + [2]=> + int(16) + [3]=> + int(17) +} +Next index after the last used slot +int(4) +array(4) { + [0]=> + int(0) + [1]=> + int(1) + [2]=> + int(3) + [3]=> + int(4) +} +Negative next index of an empty copy +int(2) +array(2) { + [0]=> + int(-2) + [1]=> + int(-1) +} +array(0) { +} +Internal pointer +int(1) +Refcounted values +object(stdClass)#1 (0) refcount(4){ +} +object(stdClass)#1 (0) refcount(2){ +} +Next index already used +Cannot add element to the array as the next element is already occupied +bool(true) +object(stdClass)#1 (0) refcount(3){ +}