From 9e1e3c17f8db260f40d7cc61450d69050e3415b5 Mon Sep 17 00:00:00 2001 From: mehmetcansahin Date: Thu, 9 Jul 2026 11:03:00 +0300 Subject: [PATCH] Optimize array_diff() for long values --- UPGRADING | 2 + ext/standard/array.c | 90 +++++++++++++++++++ .../tests/array/array_diff_long_values.phpt | 63 +++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 ext/standard/tests/array/array_diff_long_values.phpt diff --git a/UPGRADING b/UPGRADING index b9c659ccc405..e5b9dff03e6f 100644 --- a/UPGRADING +++ b/UPGRADING @@ -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 diff --git a/ext/standard/array.c b/ext/standard/array.c index a233e6c4dcb5..477956076661 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -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) { @@ -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); diff --git a/ext/standard/tests/array/array_diff_long_values.phpt b/ext/standard/tests/array/array_diff_long_values.phpt new file mode 100644 index 000000000000..442cba52233a --- /dev/null +++ b/ext/standard/tests/array/array_diff_long_values.phpt @@ -0,0 +1,63 @@ +--TEST-- +array_diff() with long values +--FILE-- + 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)