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
2 changes: 2 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,8 @@ PHP 8.6 UPGRADE NOTES
. Reduced temporary allocations when iterating Phar directories.

- Standard:
. Improved performance of array_diff() for integer-only arrays with densely
distributed values.
. Improved performance of array_fill_keys().
. Improved performance of array_map() with multiple arrays passed.
. Improved performance of array_sum() and array_product() for
Expand Down
90 changes: 90 additions & 0 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -5753,6 +5753,92 @@ PHP_FUNCTION(array_diff_ukey)
}
/* }}} */

static zend_always_inline bool php_array_values_are_all_long(HashTable *ht)
{
zval *value;

ZEND_HASH_FOREACH_VAL(ht, value) {
ZVAL_DEREF(value);
if (UNEXPECTED(Z_TYPE_P(value) != IS_LONG)) {
return false;
}
} ZEND_HASH_FOREACH_END();

return true;
}

static zend_never_inline bool php_array_diff_long(zval *args, uint32_t argc, uint32_t exclude_count, zval *return_value)
{
zend_bitset exclude;
zval *value, *entry;
zend_string *key;
zend_long idx, min = ZEND_LONG_MAX, max = ZEND_LONG_MIN;
zend_ulong span = 0, offset;
const uint64_t max_range = (uint64_t) exclude_count * 8;
uint32_t range, bitset_len;
uint32_t i;

/* The copy loop below cannot cheaply undo a partially built result, so
* the first array must be checked upfront. */
if (!php_array_values_are_all_long(Z_ARRVAL(args[0]))) {
return false;
}

/* Check the value types and determine whether the values are dense enough
* for a bitset. Limiting the range to eight bits per exclude element keeps
* bitset memory proportional to the input, apart from word alignment. */
for (i = 1; i < argc; i++) {
ZEND_HASH_FOREACH_VAL(Z_ARRVAL(args[i]), value) {
ZVAL_DEREF(value);
if (UNEXPECTED(Z_TYPE_P(value) != IS_LONG)) {
return false;
}
if (Z_LVAL_P(value) < min) {
min = Z_LVAL_P(value);
}
if (Z_LVAL_P(value) > max) {
max = Z_LVAL_P(value);
}
span = (zend_ulong) max - (zend_ulong) min;
if (UNEXPECTED(span >= HT_MAX_SIZE || (uint64_t) span >= max_range)) {
return false;
}
} ZEND_HASH_FOREACH_END();
}

range = (uint32_t) span + 1;
bitset_len = zend_bitset_len(range);
exclude = safe_emalloc(bitset_len, ZEND_BITSET_ELM_SIZE, 0);
zend_bitset_clear(exclude, bitset_len);

for (i = 1; i < argc; i++) {
ZEND_HASH_FOREACH_VAL(Z_ARRVAL(args[i]), value) {
ZVAL_DEREF(value);
offset = (zend_ulong) Z_LVAL_P(value) - (zend_ulong) min;
ZEND_ASSERT(offset < range);
zend_bitset_incl(exclude, (uint32_t) offset);
} ZEND_HASH_FOREACH_END();
}

array_init_size(return_value, zend_hash_num_elements(Z_ARRVAL(args[0])));
ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL(args[0]), idx, key, value) {
entry = value;
ZVAL_DEREF(entry);
offset = (zend_ulong) Z_LVAL_P(entry) - (zend_ulong) min;
if (offset >= range || !zend_bitset_in(exclude, (uint32_t) offset)) {
if (key) {
value = zend_hash_add_new(Z_ARRVAL_P(return_value), key, value);
} else {
value = zend_hash_index_add_new(Z_ARRVAL_P(return_value), idx, value);
}
zval_add_ref(value);
}
} ZEND_HASH_FOREACH_END();

efree(exclude);
return true;
}

/* {{{ Returns the entries of arr1 that have values which are not present in any of the others arguments. */
PHP_FUNCTION(array_diff)
{
Expand Down Expand Up @@ -5853,6 +5939,10 @@ PHP_FUNCTION(array_diff)
RETURN_THROWS();
}

if (php_array_diff_long(args, argc, (uint32_t)num, return_value)) {
return;
}

ZVAL_NULL(&dummy);
/* create exclude map */
zend_hash_init(&exclude, num, NULL, NULL, 0);
Expand Down
63 changes: 63 additions & 0 deletions ext/standard/tests/array/array_diff_long_values.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
--TEST--
array_diff() with long values
--FILE--
<?php
function dump_array(array $value): void {
echo json_encode($value), "\n";
}

dump_array(array_diff([1, 2, 3], [2]));
dump_array(array_diff([-2, -1, 0, 1], [-1, 1]));
dump_array(array_diff([PHP_INT_MIN, -1, PHP_INT_MAX], [PHP_INT_MIN, PHP_INT_MAX]));
var_dump(array_values(array_diff(
[PHP_INT_MIN, PHP_INT_MIN + 1, PHP_INT_MIN + 2, PHP_INT_MIN + 3],
[PHP_INT_MIN, PHP_INT_MIN + 2, PHP_INT_MIN + 2],
)) === [PHP_INT_MIN + 1, PHP_INT_MIN + 3]);
var_dump(array_values(array_diff(
[PHP_INT_MAX - 3, PHP_INT_MAX - 2, PHP_INT_MAX - 1, PHP_INT_MAX],
[PHP_INT_MAX - 2, PHP_INT_MAX, PHP_INT_MAX],
)) === [PHP_INT_MAX - 3, PHP_INT_MAX - 1]);
var_dump(array_diff(
[PHP_INT_MIN, 10, 11, 12, PHP_INT_MAX],
[11],
) === [PHP_INT_MIN, 10, 3 => 12, PHP_INT_MAX]);
dump_array(array_diff([1, 2, 2, 3], [2]));
dump_array(array_diff(['a' => 1, 'b' => 2, 'c' => 3], [2]));
dump_array(array_diff([1, 2, 3], ['2']));
dump_array(array_diff([1, '2', 3], [2]));
dump_array(array_diff([1, 2, 3, 4], [2], [4, 5]));
dump_array(array_diff([1, 2, 3, 4], [2], ['3']));
dump_array(array_diff([1, 2], [1, 2]));

$y = 2;
dump_array(array_diff([1, 2, 3], [&$y]));

$x = 1;
$array = [&$x, 2];
$result = array_diff($array, [2]);
$x = 9;
var_dump($result[0]);

$sparseValues = [];
for ($i = 1; $i <= 256; $i++) {
$sparseValues[] = $i * 65536;
}
var_dump(array_diff([1, 2], $sparseValues) === [1, 2]);
?>
--EXPECT--
{"0":1,"2":3}
{"0":-2,"2":0}
{"1":-1}
bool(true)
bool(true)
bool(true)
{"0":1,"3":3}
{"a":1,"c":3}
{"0":1,"2":3}
{"0":1,"2":3}
{"0":1,"2":3}
{"0":1,"3":4}
[]
{"0":1,"2":3}
int(9)
bool(true)
Loading