Skip to content

Avoid concatenated cache keys in CompilingMatcher - #186

Merged
Seldaek merged 7 commits into
composer:mainfrom
Toflar:feature/optimize-compiling-matcher-cache
Sep 24, 2026
Merged

Seldaek merged 7 commits into
composer:mainfrom
Toflar:feature/optimize-compiling-matcher-cache

Conversation

@Toflar

@Toflar Toflar commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

CompilingMatcher caches match results but currently builds a combined string key on every call, even when the result is already cached.

For example, checking whether 2.1.0 satisfies >=2.0.0:

  1. convert the constraint to a string.
  2. concatenate the operator, constraint and version into one cache key.
  3. look up the result using that key.

Checking the same constraint against 2.2.0 etc. repeats this work, including concatenating the unchanged operator and constraint.

This PR stores results in nested arrays instead:

$cache[$operator]['>= 2.0.0']['2.1.0'] = true;
$cache[$operator]['>= 2.0.0']['2.2.0'] = true;

Each call accesses the operator/constraint bucket by reference, then looks up the version within it. This avoids constructing the combined string key over and over again.
The reference bit might look strange at first, that's why I've added a comment explaining it. It basically lets the lookup and any subsequent write reuse the same bucket and it has a measurable positive impact on performance.

On a real project's composer update, pool optimization took 678 ms instead of 723 ms (~6% faster) so really this complexity is all about optimizing the hot path 😊

Comment thread src/CompilingMatcher.php Outdated
Comment thread src/CompilingMatcher.php Outdated
Comment thread src/CompilingMatcher.php Outdated
Comment on lines 88 to 93
if (!isset(self::$compiledCheckerCache[$operator][$constraintString])) {
$code = $constraint->compile($operator);
self::$compiledCheckerCache[$cacheKey] = $function = eval('return function($v, $b){return '.$code.';};');
self::$compiledCheckerCache[$operator][$constraintString] = $function = eval('return function($v, $b){return '.$code.';};');
} else {
$function = self::$compiledCheckerCache[$cacheKey];
$function = self::$compiledCheckerCache[$operator][$constraintString];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really a point in splitting the cacheKey here into sub-arrays? That just creates more arrays without benefit AFAICT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a tradeoff: nested arrays avoid constructing concatenated string keys on every call but introduce more arrays and extra lookups. But accessing by reference then improved bucket lookup time again. My measurements showed that nested arrays alone saved roughly 30 ms in pool optimization.
So there was a small measured benefit on my project but I mean, it's minimal and we can drop it. Just figured CompilingMatcher is built for the hot path so it's probably justified to go the extra mile to squeeze out even more perf 😇

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense for $resultCache IMO, but self::$compiledCheckerCache is not using the reference trick, and you access once in isset() then once on read at line 92, so you do two extra hash lookups and gain zero benefits except the lack of a string concat to build $cacheKey? Not a huge deal either way tho..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I did the change I meant.. I can't prove it's any better because this is way too jittery on my machine, none of the changes are meaningfully faster enough to really tell.

Toflar and others added 2 commits September 24, 2026 14:01
Co-authored-by: Jordi Boggiano <j.boggiano@seld.be>
Co-authored-by: Jordi Boggiano <j.boggiano@seld.be>
Comment thread src/CompilingMatcher.php Outdated
Comment thread src/CompilingMatcher.php Outdated
@Seldaek
Seldaek merged commit 346ebc8 into composer:main Sep 24, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants