Problem/Motivation
If a large amount of cache tags are stored in \Drupal\Core\Cache\CacheTagsChecksumTrait::$tagCache, then the the array operations we run on them, such as array_keys() and array_diff() can get very expensive.
I doubt that this makes a measurable impact on regular operations as there aren't *that* many cache tags, but in special cases, such as the new redis report, it adds up *a lot*. The report loops over all caches and checks if they haven't been invalidated, to figure out a percentage of expired and invalidated items.
Turns out, the results in massive costs in side calculateChecksum(), which I managed to catch with the blackfire continuous monitoring report:

It's somewhat similar to the memory cache cache tag invalidation issues.
Note: the original performance measurements were taken on 11.3.x and mostly already fixed with #3580109: Optimise CacheTagsCheckSumTrait::calculateChecksum(), which I had forgotten about.
Steps to reproduce
Proposed resolution
Convert the remaining array_diff/array_merge/array_keys()/.. calls to loops over possibly much smaller $tags and $this->preloadTags.
Performance improvement is small but I think still worth it to avoid the extra array operations on filling the static cache with 0's, also avoids an extra combined variable.
Remaining tasks
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| Screenshot From 2026-08-06 11-43-53.png | 47.09 KB | berdir |
Issue fork drupal-3615175
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 3615175-slow-array-functions
compare
- 3615175-array-functions
changes, plain diff MR !16604
Comments
Comment #2
berdirComment #5
sjpagan commentedMR !16604, pipeline green.
The cost is in
registerCacheTagsForPreload().array_merge()copies the whole registered list on every call, andDatabaseBackend::getMultiple()calls it once per lookup.Bench: N batches of 10 tags, then one checksum read.
Linear after the change, quadratic before.
The
calculateChecksum()conversions alone changed nothing measurable.array_combine()with+=was slower than the current code:+=on a property duplicates the array.$preloadTagsis now keyed by tag. Nothing in core or contrib reads it, includingRedisCacheTagsChecksum.Comment #6
smustgrave commented@sjpagan thanks for contributing but just a few notes.
As previously commented some of the comments have been coming off as AI so please take a moment and read https://www.drupal.org/docs/develop/issues/issue-procedures-and-etiquett...
There's no rule against using AI at all but again please read the issue etiquette page.
Thanks.
Comment #7
sjpagan commentedOkay @smustgrave, I’ve read the article, i wasn’t aware of that, so I’ll be more careful when writing comments and code, following the guidelines.
I saw that you replied to my other questions as well; do I need to reply to those too regarding the same issue?
Comment #8
smustgrave commentedNo sir you are good :) thank you
Comment #9
sjpagan commentedAdded the missing test coverage. `CacheTagsChecksumTest` and `CacheTagsChecksumHelper` cover the preload path, which had no test in core. They pass both before and after the change.
Trimmed the comments the previous commits added to the trait, from 17 lines to 7. No code changes in that commit. Also updated the MR description.
Comment #10
smustgrave commentedGoing to let other reviews these as the bot seems to be putting a lot that don't have time to review all but first thing is to check if existing test coverage exists already vs new files. Being able to add a new assert to an existing test that's related is preferred.
If test coverage is missing it may not be in scope to add here but again will leave for others.
Comment #11
sjpagan commentedChecked first — nothing in core calls `registerCacheTagsForPreload()`, and there's no test class for the trait. The only separate helper file in core is `CacheCollectorHelper`, and that one is shared by three test classes, so it didn't apply here.
Merged it into `CacheTagsChecksumTest.php` as a second class instead. One new file now, not two.
Comment #12
berdirConfused on the timeline of the comments here, feels like some are replies to others but were written first, but maybe I just didn't read them properly.
I set this to a bug report, but in the sense of "a performance bug", not a functional one. I guess we usually treat those as tasks. This contains no behavior change, it's purely an internal optimization. As such, I don't think this needs any tests at all. Our existing tests *should* be sufficient to cover the changed code, as commented on the MR.
Comment #13
berdirThanks, I think this looks good now. Per the numbers in #5, not sure how common it is to have more than 1k cache tag lookups in the cache, but there's no downside to this I think and for long running processes like migrations, this could easily add up quite a bit.
Comment #14
sjpagan commentedWithdrawing the numbers in #5: that bench never reads a checksum between
registrations. registerCacheTagsForPreload() is back to main.
Locally the loops in calculateChecksum() give 8 to 14 percent, flat from
1,000 to 50,000 tagCache entries.
Comment #15
berdirI'm sorry, I made a mistake and didn't review and test this well enough.
The performance data I reported was based on 11.3.x, not main. This problem I saw there was already fixed in #3580109: Optimise CacheTagsCheckSumTrait::calculateChecksum(), I was part of that issue, I just forgot about it.
I only realized this when double-checking the performance tests and then was confused when I didn't see much of a difference, even with very large tag lists, not as I expected it at least.
I considered closing this issue as a duplicate, but I think the remaining changes are useful, more readable/simpler and a tiny bit faster, but only when preload tags are involved and actual lookups, specifically lookup misses.
Comment #16
catchComment #17
catchOK agreed, even if the performance gains are very marginal, the code is a bit easier to follow, so no reason not to do this.
Committed/pushed to main. Doesn't apply to 11.x but given there's not a noticeable performance improvement I think we could leave this in main. Thanks!