-
Notifications
You must be signed in to change notification settings - Fork 2.1k
perf(runtime): reduce callback tracking contention #10062
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
Open
ReubenBond
wants to merge
11
commits into
dotnet:main
Choose a base branch
from
ReubenBond:split/striped-callback-dictionary
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+375
−21
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
28464da
perf(runtime): reduce callback tracking contention
ReubenBond 3fa1619
fix(runtime): preserve callback shutdown semantics
ReubenBond 6c287f2
test(runtime): cover striped callback dictionary
ReubenBond cffc9bf
fix(runtime): preserve callback shutdown boundaries
ReubenBond a123c10
fix(runtime): retain nullable callback invariants
ReubenBond e2450e2
fix(runtime): prevent pooled callback snapshot reuse
ReubenBond f58d1db
fix(runtime): preserve callback ownership
ReubenBond bf1cb5a
perf(runtime): remove callback scan allocation
ReubenBond 7b435bd
perf(runtime): preserve client callback fast path
ReubenBond 1c7e0e1
perf(runtime): key silo callbacks by correlation id
ReubenBond f7c990e
perf(runtime): remove callback count allocation
ReubenBond File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
191 changes: 191 additions & 0 deletions
191
src/Orleans.Core/Messaging/StripedCallbackDictionary.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| #nullable enable | ||
| using System; | ||
| using System.Buffers; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Runtime.CompilerServices; | ||
|
|
||
| namespace Orleans.Runtime; | ||
|
|
||
| /// <summary> | ||
| /// A striped dictionary that distributes entries across multiple internal dictionaries | ||
| /// to reduce lock contention by hashing correlation ids across stripes. | ||
| /// </summary> | ||
| /// <typeparam name="TValue">The type of values stored in the dictionary.</typeparam> | ||
| internal sealed class StripedCallbackDictionary<TValue> | ||
| where TValue : notnull | ||
| { | ||
| private const int StripeBits = 7; | ||
| // Fibonacci hashing spreads sequential and strided ids using one multiply and shift. | ||
| private const ulong HashFactor = 11_400_714_819_323_198_485; | ||
|
|
||
| /// <summary> | ||
| /// The number of stripes. | ||
| /// </summary> | ||
| public const int StripeCount = 1 << StripeBits; | ||
|
|
||
| private readonly Stripe[] _stripes; | ||
|
|
||
| public StripedCallbackDictionary() | ||
| { | ||
| _stripes = new Stripe[StripeCount]; | ||
| for (int i = 0; i < StripeCount; i++) | ||
| { | ||
| _stripes[i] = new Stripe(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Computes the stripe index for a correlation id. | ||
| /// </summary> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static int GetStripeIndex(CorrelationId correlationId) | ||
| => (int)(unchecked((ulong)correlationId.ToInt64() * HashFactor) >> (64 - StripeBits)); | ||
|
|
||
| /// <summary> | ||
| /// Gets the stripe for the given callback id. | ||
| /// </summary> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private Stripe GetStripe(CorrelationId correlationId) | ||
| { | ||
| return _stripes[GetStripeIndex(correlationId)]; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Attempts to add the specified key and value to the dictionary. | ||
| /// </summary> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public bool TryAdd(CorrelationId id, TValue value) | ||
| { | ||
| var stripe = GetStripe(id); | ||
| lock (stripe.Lock) | ||
| { | ||
| return stripe.Dictionary.TryAdd(id, value); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Attempts to get the value associated with the specified key. | ||
| /// </summary> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public bool TryGetValue(CorrelationId id, [NotNullWhen(true)] out TValue? value) | ||
| { | ||
| var stripe = GetStripe(id); | ||
| lock (stripe.Lock) | ||
| { | ||
| return stripe.Dictionary.TryGetValue(id, out value); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Attempts to remove the value with the specified key. | ||
| /// </summary> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public bool TryRemove(CorrelationId id, [NotNullWhen(true)] out TValue? value) | ||
| { | ||
| var stripe = GetStripe(id); | ||
| lock (stripe.Lock) | ||
| { | ||
| return stripe.Dictionary.Remove(id, out value); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the approximate total count of items across all stripes. | ||
| /// </summary> | ||
| public int Count | ||
| { | ||
| get | ||
| { | ||
| int count = 0; | ||
| foreach (var stripe in _stripes) | ||
| { | ||
| lock (stripe.Lock) | ||
| { | ||
| count += stripe.Dictionary.Count; | ||
| } | ||
| } | ||
| return count; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Counts items matching a predicate across all stripes. | ||
| /// </summary> | ||
| public int CountWhere(Func<TValue, bool> predicate) | ||
| => CountWhere(predicate, static (value, predicate) => predicate(value)); | ||
|
|
||
| /// <summary> | ||
| /// Counts items matching a predicate across all stripes. | ||
| /// </summary> | ||
| public int CountWhere<TState>(TState state, Func<TValue, TState, bool> predicate) | ||
| { | ||
| int count = 0; | ||
| foreach (var stripe in _stripes) | ||
| { | ||
| lock (stripe.Lock) | ||
| { | ||
| foreach (var value in stripe.Dictionary.Values) | ||
| { | ||
| if (predicate(value, state)) | ||
| { | ||
| count++; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return count; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Visits a snapshot of the values in each stripe. | ||
| /// </summary> | ||
| public void ForEach<TState>(TState state, Action<TValue, TState> action) | ||
| { | ||
| foreach (var stripe in _stripes) | ||
| { | ||
| TValue[]? snapshot = null; | ||
| var snapshotCount = 0; | ||
| try | ||
| { | ||
| lock (stripe.Lock) | ||
| { | ||
| if (stripe.Dictionary.Count == 0) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| snapshot = ArrayPool<TValue>.Shared.Rent(stripe.Dictionary.Count); | ||
| foreach (var value in stripe.Dictionary.Values) | ||
| { | ||
| snapshot[snapshotCount++] = value; | ||
| } | ||
| } | ||
|
|
||
| for (var i = 0; i < snapshotCount; i++) | ||
| { | ||
| action(snapshot[i], state); | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| if (snapshot is not null) | ||
| { | ||
| ArrayPool<TValue>.Shared.Return( | ||
| snapshot, | ||
| clearArray: RuntimeHelpers.IsReferenceOrContainsReferences<TValue>()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private sealed class Stripe | ||
| { | ||
| #if NET9_0_OR_GREATER | ||
| public readonly System.Threading.Lock Lock = new(); | ||
| #else | ||
| public readonly object Lock = new(); | ||
| #endif | ||
| public readonly Dictionary<CorrelationId, TValue> Dictionary = new(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.