Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions UPGRADING.INTERNALS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions Zend/zend_hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions Zend/zend_hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 8 additions & 6 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
163 changes: 163 additions & 0 deletions ext/standard/tests/array/array_push_multiple_values.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
--TEST--
array_push() with several values
--FILE--
<?php
echo "Room for all values\n";
$a = range(1, 6);
var_dump(array_push($a, 7, 8), $a === range(1, 8));

echo "One value more than the room\n";
$a = range(1, 7);
var_dump(array_push($a, 8, 9), $a === range(1, 9));

echo "More values than twice the table size\n";
$a = range(1, 8);
var_dump(array_push($a, ...range(9, 25)), $a === range(1, 25));

echo "Empty array\n";
$a = [];
var_dump(array_push($a, 1, 2), $a === [1, 2]);
$a = [];
var_dump(array_push($a, ...range(0, 8)), $a === range(0, 8));

echo "No values keep the next index of an empty array\n";
$a = [];
var_dump(array_push($a));
$a[-5] = 1;
$a[] = 2;
var_dump(array_keys($a));

echo "Hole with room\n";
$a = [0, 1, 2, 3];
unset($a[1]);
var_dump(array_push($a, 4, 5), array_keys($a));

echo "Holes without room\n";
$a = range(0, 15);
for ($i = 0; $i < 14; $i++) {
unset($a[$i]);
}
end($a);
next($a);
var_dump(array_push($a, 16, 17), key($a), array_keys($a));

echo "Next index after the last used slot\n";
$a = [0, 1, 2];
unset($a[2]);
var_dump(array_push($a, 3, 4), array_keys($a));

echo "Negative next index of an empty copy\n";
$a = [-3 => 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){
}
Loading