-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Proposal] use Timer in MemoryCache #45842
Conversation
…performed, use Timer instead
…two tests would need to wait for a minute to get result (CI would not like it)
…e expired (TechEmpower case)
# Conflicts: # src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs
Tagging subscribers to this area: @eerhardt, @maryamariyan Issue DetailsThis is my last proposal for improving After #45281, #45280, #45410, #45563, aspnet/Benchmarks#1603 and aspnet/Benchmarks#1607 we got to the place where the RPS for TE Caching benchmark has increased by 15% (from ~230k to ~270k). Now, most of the time related to caching (most of the time in general is spent in JSON serialization) and not related to usage of Concurrent Dictionary is spent in getting current time (to tell if given entry has expired or not): In case of TE Caching benchmark all cache entries have no expiration date. So this is kind of redundant and could be detected before getting current time. The problem is that We could store the information about the fact that all items in given cache can't expire, but it would rather be a hack.. Instead of this, we can introduce a I assume that most of our users have a single instance of the cache and they use the default expiration scan frequency (1 minute). This change gives us a 10% boost for the TE caching benchmark (from 274 to 307k RPS). After it, the Concurrent Dictionary becomes a bottleneck. But even if we replace the cache with an array, the RPS is around 330k so it put's us very close to the maximum limit. FWIW the microbenchmark results:
@eerhardt @davidfowl @maryamariyan @Tratcher
|
_timer = new System.Timers.Timer(Math.Max(1, _options.ExpirationScanFrequency.TotalMilliseconds)); | ||
_timer.Elapsed += OnTimerElapsed; | ||
_timer.Start(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is bad for testability. Any timing mechanism needs to be abstracted for the unit tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, but it's still doable
Have you tried making the current time check more efficient? |
I did: #45281 Now it's just a syscall and I can't see a way to optimize it further |
...because these should always be absolute stamps, in theory you could do something like get the current process/system time on cache initialization, via something like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Embedding a timer in the implementation makes the component impossible to test reliably. Tests must be in full control of any timing. This is a blocking design point to resolve.
@adamsitnik From having a quick look at MemoryCache there a few (small) quick wins left:
Don't hesitate to correct me! |
@@ -24,10 +22,10 @@ public class MemoryCache : IMemoryCache | |||
|
|||
private readonly MemoryCacheOptions _options; | |||
private readonly ConcurrentDictionary<object, CacheEntry> _entries; | |||
private readonly System.Timers.Timer _timer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Besides the abstraction thing, why not use System.Threading.Timer
? It should have less overhead, as System.Timers.Timer
also relies on Threading.Timer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right, nothing should be using System.Timers.Timer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used this particular timer because it was the first one VS has suggested ;)
I wanted to have a proof of concept to get some numbers and get feedback before I invest more in the implementation.
@drieseng you are right, but the places that you have pointed are not on the hot path that I am optimizing. So even if I optimize them it won't have any effects |
It's harder, but not impossible to mock. The question is whether the testing of |
I've already removed the redundant verification in #45281 Whatever |
It should affect the AddThenRemove and SetOverride benchmark results you've mentioned in the description... BUT... I'm saying this without having actually seen the benchmark code and without having tested the proposed changes :p If you can guarantee that I can have the dotnet runtime built on my Windows box in 30 minutes, I definitely want to try to prove you wrong (or admit my defeat). |
I may have missed some crucial implementation details, but could this work instead to achieve the objective?
|
Not necessarily, Windows and some versions of Linux have a special memory page read-only mapped into all processes and this value is directly readable from it without a syscall. It is also monolithically increasing unlike the real clock (which may be an attack vector for cache performance degradation). |
... and, at least on windows, there's a whole bunch of stuff that happens when calling |
I've optimized everything except leap seconds in the past there: dotnet/coreclr#26046 And now @GrabYourPitchforks is working on leap second handling perf: #44771 |
@adamsitnik |
@edevoogd big thanks for your proposal! I really like the idea described in https://github.com/edevoogd/ClockQuantization! |
For reasons described by @DamianEdwards on Twitter, I’ve decided to not pursue this idea. If we ever decide that we want to be in the Top 5 for this benchmark, we should reconsider it. |
Yes we do want to do this. |
I think you should still re-evaluate using |
FWIW, local experimentation with changes in edevoogd/ClockQuantization#1 shows the additional benefit of using |
This is my last proposal for improving
MemoryCache
, at least for a while (I've run out of ideas).After #45281, #45280, #45410, #45563, aspnet/Benchmarks#1603, and aspnet/Benchmarks#1607
we got to the place where the RPS for TE Caching benchmark has increased by 15% (from ~230k to ~270k).
Now, most of the time related to caching (most of the time, in general, is spent in JSON serialization) and not related to the usage of Concurrent Dictionary is spent in getting current time (to tell if the given entry has expired or not):
In the case of the TE Caching benchmark, all cache entries have no expiration date. So this is kind of redundant and could be detected before getting the current time.
The problem is that
MemoryCache
uses every public method to check if it has to scan existing cache items for expired ones. To be able to do that, it needs the current time.We could store the information about the fact that all items in the given cache can't expire, but it would rather be a hack..
Instead of this, we can introduce a
Timer
toMemoryCache
and ask it to perform the scan in the background at configured time interval. And this is what this proposal is all about.I assume that most of our users have a single instance of the cache and they use the default expiration scan frequency (1 minute).
This change gives us a 10% boost for the TE caching benchmark (from 274 to 307k RPS). After it, the Concurrent Dictionary becomes a bottleneck. But even if we replace the cache with an array, the RPS is around 330k so it puts us very close to the maximum limit.
FWIW the microbenchmark results:
@eerhardt @davidfowl @maryamariyan @Tratcher