Add weight-based eviction to ConcurrentLfu#814
Conversation
Design/phasing plan to add Caffeine-parity weight-based eviction to ConcurrentLfu and ConcurrentTLfu via the existing INodePolicy seam (no new generic), with weighted node subclasses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add IWeigher, builder WithWeigher + factory wiring (weighted no-events via FastConcurrentLfu), the INodePolicy sizing seam (IsWeighted/GetWeight/GetPolicyWeight/SetPolicyWeight) implemented trivially by the unweighted policies, weighted node subclasses, weighted access/expiry node policies, and LfuNodeList.AddFirst/MoveToFront. No change to the unweighted path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add policyWeight-authoritative weighted size accounting (weightedSize/window/protected), weighted OnWrite add/update/remove with oversize handling, weighted EvictFromWindow/EvictFromMain with frequency admission (incl. 1/128 anti-hashflood), weighted PromoteProbation/ReFitProtected, zero-weight survival, and weighted-aware OnAccess promotion. Maintains the invariant weightedSize == sum of policyWeight. Count path unchanged (JIT-elided via IsWeighted). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adapt window/main-protected maximums in weight units via Caffeine's hill-climbing (DetermineWeightedAdjustment + IncreaseWindow/DecreaseWindow moving whole nodes between queues within quota, capped at QUEUE_TRANSFER_THRESHOLD). Replaces the count-based OptimizePartitioning for weighted caches. Count path unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add INodePolicy.ExpiryCalculator so wrappers expose the time policy without depending on the concrete policy type; refactor FastConcurrentLfu to use it (now supports weighted+expiry no-events). Add WeightedConcurrentLfu<K,V,N,P> generic events wrapper for weighted+events and weighted+time+events. Factory now routes all four weighted combos. Tests cover all combos plus the shared cache suite against the weighted+events wrapper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a weighted concurrency soak test (4 threads x get/update/remove) that reuses the generic integrity checker and asserts the weighted-size invariant holds under load. Document weighted eviction (WithWeigher) in the README. Validated across all target frameworks including net48. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The iteration parameter was unused, so the dotnet format analyzer pass (IDE0060) removed it, causing xUnit to fail binding the [Repeat]-provided argument. Use the parameter so the test binds and runs the full workload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Matches the IExpiryCalculator naming convention. Pure rename of the public interface and its method (plus implementers and usages); no behavioral change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
286cb79 to
5c80835
Compare
|
fwiw, this paper argued for a different weighted admission scheme than Caffeine's (compares an accumulation). My concerns with the approach was that under concurrency this becomes harder to reason about because the cache's state is constantly changing. There is still a need for an evaluation loop to handle races, e.g. the entry's weight might change mid evaluation or it could be explicitly removed. Since weight-based caches are less common for on-heap application caches (hard to size objects, smaller, use less memory) it seemed like an optimization that could be deferred. The approach would make more sense for persisted or dedicated remote cache servers which might hold hundreds of millions of entries, but also have vastly different needs and design goals. You might find something interesting to borrow as I liked the idea conceptually but there was a marginal hit rate increase and no real world advocate. |
Summary
Adds opt-in weight-based eviction to
ConcurrentLfu, faithfully matching Caffeine's weighted W-TinyLFU. A user supplies anIWeigher<K, V>; the cache is then bounded by total weight instead of item count.WithWeighercomposes withWithEventsandWithExpireAfterWrite/WithExpireAfterAccess/WithExpireAfter.Design
INodePolicyslot via two new policy structs (WeightedAccessOrderPolicy,WeightedExpireAfterPolicy).ConcurrentLfuCore/TimerWheelarities are unchanged.static readonly bool IsWeighted = default(P).IsWeightedand JIT-elided in the count case. Weight is stored on weighted node subclasses, so unweighted node layout is untouched.policyWeight-authoritative accounting maintains the invariantweightedSize == Σ policyWeight, which is robust to BitFaster's buffered/duplicated writes.evictFromWindow/evictFromMain(zero-weight skips, oversize-candidate eviction, frequency admission with the>=6/1-in-128anti-hashflood jitter), oversize-on-add, conditional weight-change handling, zero-weight survival, and the absolute-weight hill-climb (increase/decreaseWindowmoving whole nodes within quota).IBoundedPolicy.Capacitystaysint(the weight budget); internal accounting islong. With a weigher, zero/low-weight entries meanCountcan exceedCapacity(by design).Scope
In:
ConcurrentLfu(with/without events) andConcurrentTLfu(weight composed with time expiry). Deferred:FastConcurrentLfustandalone exposure and the scoped/atomic/async builder wrappers.Testing
mainProtectedWeightedSize) — fixed and regression-tested — plus three weighted+expiry hazards (deschedule-on-remove, skipAfterWriteon evict, idempotent discount).Note
TimerWheelTests.WhenRescheduledLaterNodeIsMovedis a pre-existing, date/wall-clock-dependent test (uses realDuration.SinceEpoch()); it fails on a cleanmainindependent of this change.Draft
Opened as a draft for review of the approach and API shape before finalizing.