Uh oh!
There was an error while loading. Please reload this page.
Optimize array_intersect() using hash-based matching - #23019
Optimize array_intersect() using hash-based matching#23019mehmetcansahin wants to merge 4 commits into
Conversation
LamentXU123
commented
Aug 4, 2026
This looks sensible. But could you please provide the "Local benchmarks" you've run for us to verify. These days it's hard to tell a performance improvement without benchmarks. |
mehmetcansahin
commented
Aug 4, 2026
@LamentXU123 Thanks. I reran the benchmarks on an Apple M1, comparing base
Benchmark script: benchmark.php<?phpdeclare(strict_types=1);
constTARGET_SAMPLE_NS = 100_000_000;
constSAMPLE_COUNT = 11;
functionmakeStrings(int$start, int$size): array
{
$values = [];
for ($i = $start, $end = $start + $size; $i < $end; $i++) {
$values[] = "value_$i";
}
return$values;
}
functionmakeMixed(int$start, int$size): array
{
$values = [];
for ($i = $start, $end = $start + $size; $i < $end; $i++) {
$values[] = ($i & 1) === 0 ? $i : (string) $i;
}
return$values;
}
functionscenarios(): array
{
$fallbackFirst = range(0, 9_999);
array_unshift($fallbackFirst, 0.5);
$fallbackLast = range(0, 9_999);
$fallbackLast[] = 0.5;
return [
'int-10' => [range(0, 9), range(5, 14)],
'int-1000' => [range(0, 999), range(500, 1_499)],
'int-100000' => [range(0, 99_999), range(50_000, 149_999)],
'string-10000' => [makeStrings(0, 10_000), makeStrings(5_000, 10_000)],
'mixed-int-string-10000' => [makeMixed(0, 10_000), makeMixed(5_000, 10_000)],
'int-10000-3-arrays' => [
range(0, 9_999),
range(2_500, 12_499),
range(5_000, 14_999),
],
'fallback-float-first-10000' => [$fallbackFirst, range(5_000, 14_999)],
'fallback-float-last-10000' => [$fallbackLast, range(5_000, 14_999)],
];
}
functionmeasure(array$arrays, int$iterations): array
{
$checksum = 0;
$start = hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$checksum += count(array_intersect(...$arrays));
}
return [hrtime(true) - $start, $checksum];
}
$allScenarios = scenarios();
$selected = $argv[1] ?? null;
if ($selected === null || !isset($allScenarios[$selected])) {
fwrite(STDERR, "Usage: php benchmark.php <scenario>\n\nScenarios:\n");
foreach (array_keys($allScenarios) as$name) {
fwrite(STDERR, "$name\n");
}
exit(1);
}
$arrays = $allScenarios[$selected];
$iterations = 1;
do {
[$elapsed] = measure($arrays, $iterations);
if ($elapsed >= TARGET_SAMPLE_NS || $iterations >= 1_048_576) {
break;
}
$iterations *= 2;
} while (true);
measure($arrays, $iterations);
$samples = [];
$checksum = 0;
for ($sample = 0; $sample < SAMPLE_COUNT; $sample++) {
[$elapsed, $sampleChecksum] = measure($arrays, $iterations);
$samples[] = $elapsed / $iterations;
$checksum ^= $sampleChecksum;
}
sort($samples);
$median = $samples[intdiv(count($samples), 2)];
printf(
"%s iterations=%d samples=%d median_us=%.3f min_us=%.3f max_us=%.3f checksum=%d\n",
$selected,
$iterations,
SAMPLE_COUNT,
$median / 1_000,
$samples[0] / 1_000,
$samples[array_key_last($samples)] / 1_000,
$checksum,
); |
LamentXU123
commented
Aug 4, 2026
I don't love the additional code complexity, but the benchmark result seems worth it :/ |
arnaud-lb
commented
Aug 4, 2026
Current algo:
New algo:
New algo is clearly superior. Could the same algorithm be used in all cases, not only |
2e270dc to
8fb2b5eComparemehmetcansahin
commented
Aug 5, 2026
I tried the universal hash approach and updated the implementation to use it for all value types. The type-based fallback is now gone. The former fallback cases are about 109–111x faster, float-only arrays are about 24x faster, and the existing integer/string cases remained neutral or improved by up to 22%. I also added empty-input short-circuiting, while preserving the first array's key/bucket metadata, and documented the observable conversion-order differences in |
8fb2b5e to
367bbffComparemehmetcansahin
commented
Aug 5, 2026
I pushed the latest fixes. Updated benchmark results:
The safety fix adds approximately 2–20% overhead compared with the previous hash implementation, while remaining 7.44–114.68× faster than the base implementation. |
arnaud-lb
left a comment
There was a problem hiding this comment.
This looks good to me apart from a few nits. I will merge once those are resolved.
Feel free to add a NEWS entry.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| ZEND_HASH_FOREACH_KEY(result, num_key, key) { | ||
| if (zend_bitset_in(delete_bitset, result_pos)) { |
There was a problem hiding this comment.
Did you consider using ZEND_BITSET_FOREACH() here?
There was a problem hiding this comment.
The bitset tracks live-entry positions, not bucket indexes. Since zend_array_dup() can compact holes after conversion, using ZEND_BITSET_FOREACH() would still require mapping each position back to the corresponding bucket.
Replaces
array_intersect()'s sort-based matching with a hash-based implementation for calls with at least two arrays. Integer and string values use normalized hash keys directly; other values are converted according to the existing string-comparison semantics. Single-array calls retain the generic path.This removes the type-based fallback and makes matching expected-linear. Empty operands short-circuit after argument validation while preserving the first array's key and bucket metadata.
String conversions now occur while scanning instead of during sort comparisons. This can change warning and
__toString()invocation counts and order, which conversion exception is reached, and results for stateful__toString()implementations. This is documented inUPGRADING.Compared with the previous PR head on an Apple M1, existing integer/string cases are neutral or up to 22% faster, the former fallback cases are about 109–111x faster, and float-only arrays are about 24x faster.
Verification includes the configured full test suite with 0 failures, all relevant
array_intersect*tests, the resource-heavy hard-timeout test, and 4,000 differential cases matching the previous implementation for stable conversions and array metadata.