Skip to content

Commit e104be4

Browse files
Reduce array_diff() long fast path overhead
Check the first value's type inline before calling php_array_diff_long(), and scan the exclude arrays before the first array so sparse or non-int exclusions fall back without a full scan of the first array. Allocate the bitset with ZEND_BITSET_ALLOCA like the other bitsets in this file. Add a test building a bitset of more than one word, a NEWS entry, and clarify that only the values to exclude need to be dense. Suggested by @ArtUkrainskiy.
1 parent 0d9e757 commit e104be4

4 files changed

Lines changed: 39 additions & 11 deletions

File tree

‎NEWS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ PHP NEWS
125125
in a php://filter URL. (Sjoerd Langkemper)
126126
. Improved performance of array_chunk() when not preserving keys.
127127
(mehmetcansahin)
128+
. Improved performance of array_diff() for integer-only arrays when the
129+
values to exclude are densely distributed. (mehmetcansahin)
128130

129131
- URI:
130132
. Fix casing of enum cases in UriHostType and UrlHostType to match the RFC

‎UPGRADING‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ PHP 8.7 UPGRADE NOTES
107107
. Improved performance of strcspn() and strpbrk() when searching longer strings
108108
with a single-byte character mask.
109109
. Improved performance of array_chunk() when not preserving keys.
110-
. Improved performance of array_diff() for integer-only arrays with densely
111-
distributed values.
110+
. Improved performance of array_diff() for integer-only arrays when the
111+
values to exclude are densely distributed.
112112
. Improved performance of array_splice() when inserting without removing
113113
elements.
114114
. Improved performance of str_rot13().

‎ext/standard/array.c‎

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6072,6 +6072,18 @@ static zend_always_inline bool php_array_values_are_all_long(HashTable *ht)
60726072
return true;
60736073
}
60746074

6075+
static zend_always_inline bool php_array_first_value_is_long(HashTable *ht)
6076+
{
6077+
zval *value;
6078+
6079+
ZEND_HASH_FOREACH_VAL(ht, value) {
6080+
ZVAL_DEREF(value);
6081+
return Z_TYPE_P(value) == IS_LONG;
6082+
} ZEND_HASH_FOREACH_END();
6083+
6084+
return false;
6085+
}
6086+
60756087
static zend_never_inline bool php_array_diff_long(zval *args, uint32_t argc, uint32_t exclude_count, zval *return_value)
60766088
{
60776089
zend_bitset exclude;
@@ -6082,12 +6094,7 @@ static zend_never_inline bool php_array_diff_long(zval *args, uint32_t argc, uin
60826094
const uint64_t max_range = (uint64_t) exclude_count * 8;
60836095
uint32_t range, bitset_len;
60846096
uint32_t i;
6085-
6086-
/* The copy loop below cannot cheaply undo a partially built result, so
6087-
* the first array must be checked upfront. */
6088-
if (!php_array_values_are_all_long(Z_ARRVAL(args[0]))) {
6089-
return false;
6090-
}
6097+
ALLOCA_FLAG(use_heap);
60916098

60926099
/* Check the value types and determine whether the values are dense enough
60936100
* for a bitset. Limiting the range to eight bits per exclude element keeps
@@ -6111,9 +6118,16 @@ static zend_never_inline bool php_array_diff_long(zval *args, uint32_t argc, uin
61116118
} ZEND_HASH_FOREACH_END();
61126119
}
61136120

6121+
/* Checked after the exclude arrays, whose type and range checks can rule
6122+
* the bitset out after a few values. The copy loop below cannot cheaply undo
6123+
* a partially built result, so the first array must be checked before it. */
6124+
if (!php_array_values_are_all_long(Z_ARRVAL(args[0]))) {
6125+
return false;
6126+
}
6127+
61146128
range = (uint32_t) span + 1;
61156129
bitset_len = zend_bitset_len(range);
6116-
exclude = safe_emalloc(bitset_len, ZEND_BITSET_ELM_SIZE, 0);
6130+
exclude = ZEND_BITSET_ALLOCA(bitset_len, use_heap);
61176131
zend_bitset_clear(exclude, bitset_len);
61186132

61196133
for (i = 1; i < argc; i++) {
@@ -6140,7 +6154,7 @@ static zend_never_inline bool php_array_diff_long(zval *args, uint32_t argc, uin
61406154
}
61416155
} ZEND_HASH_FOREACH_END();
61426156

6143-
efree(exclude);
6157+
free_alloca(exclude, use_heap);
61446158
return true;
61456159
}
61466160

@@ -6244,7 +6258,8 @@ PHP_FUNCTION(array_diff)
62446258
RETURN_THROWS();
62456259
}
62466260

6247-
if (php_array_diff_long(args, argc, (uint32_t)num, return_value)) {
6261+
if (php_array_first_value_is_long(Z_ARRVAL(args[0]))
6262+
&& php_array_diff_long(args, argc, (uint32_t)num, return_value)) {
62486263
return;
62496264
}
62506265

‎ext/standard/tests/array/array_diff_long_values.phpt‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ for ($i = 1; $i <= 256; $i++) {
4343
$sparseValues[] = $i * 65536;
4444
}
4545
var_dump(array_diff([1, 2], $sparseValues) === [1, 2]);
46+
47+
$wideExclude = [];
48+
for ($i = 0; $i < 100; $i++) {
49+
$wideExclude[] = $i * 7;
50+
}
51+
$wideInput = range(-1, 700);
52+
var_dump(array_diff($wideInput, $wideExclude) === array_filter(
53+
$wideInput,
54+
fn($v) => $v < 0 || $v > 693 || $v % 7 !== 0,
55+
));
4656
?>
4757
--EXPECT--
4858
{"0":1,"2":3}
@@ -61,3 +71,4 @@ bool(true)
6171
{"0":1,"2":3}
6272
int(9)
6373
bool(true)
74+
bool(true)

0 commit comments

Comments
 (0)