Skip to content

Commit a4db8ce

Browse files
ReubenBondCopilot
andcommitted
Reduce callback tracking contention with striped callback dictionary
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 70dc490 commit a4db8ce

5 files changed

Lines changed: 368 additions & 223 deletions

File tree

src/Orleans.Core/Messaging/CorrelationId.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ namespace Orleans.Runtime
1616

1717
public static CorrelationId GetNext() => new(System.Threading.Interlocked.Increment(ref lastUsed));
1818

19-
public override int GetHashCode() => id.GetHashCode();
19+
public override int GetHashCode() => HashCode.Combine(id);
2020

2121
public override bool Equals(object? obj) => obj is CorrelationId correlationId && Equals(correlationId);
2222

src/Orleans.Core/Messaging/MessageFactory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public Message CreateMessage(object? body, InvokeMethodOptions options)
4949
private CorrelationId GetNextCorrelationId()
5050
{
5151
var id = _seed ^ Interlocked.Increment(ref _nextId);
52-
return new CorrelationId(unchecked((long)id));
52+
var stripeIndex = StripedCallbackDictionary<object>.GetCurrentThreadStripeIndex();
53+
return StripedCallbackDictionary<object>.CreateCorrelationId(unchecked((long)id), stripeIndex);
5354
}
5455

5556
public Message CreateResponseMessage(Message request)
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using System.Runtime.CompilerServices;
6+
7+
namespace Orleans.Runtime;
8+
9+
/// <summary>
10+
/// A striped dictionary that distributes entries across multiple internal dictionaries
11+
/// to reduce lock contention. The stripe is determined by bits embedded in the CorrelationId.
12+
/// </summary>
13+
/// <typeparam name="TValue">The type of values stored in the dictionary.</typeparam>
14+
internal sealed class StripedCallbackDictionary<TValue> : IEnumerable<KeyValuePair<CorrelationId, TValue>>
15+
{
16+
/// <summary>
17+
/// The number of bits used to identify the stripe (stored in the upper bits of the CorrelationId).
18+
/// </summary>
19+
public const int StripeBits = 7;
20+
21+
/// <summary>
22+
/// The number of stripes (must be a power of 2).
23+
/// </summary>
24+
public const int StripeCount = 1 << StripeBits; // 128 stripes
25+
26+
/// <summary>
27+
/// Mask to extract the stripe index from the upper bits.
28+
/// </summary>
29+
private const long StripeMask = (long)(StripeCount - 1) << (64 - StripeBits);
30+
31+
/// <summary>
32+
/// The shift amount to move the stripe bits to the lowest position.
33+
/// </summary>
34+
private const int StripeShift = 64 - StripeBits;
35+
36+
private readonly Stripe[] _stripes;
37+
38+
public StripedCallbackDictionary()
39+
{
40+
_stripes = new Stripe[StripeCount];
41+
for (int i = 0; i < StripeCount; i++)
42+
{
43+
_stripes[i] = new Stripe();
44+
}
45+
}
46+
47+
/// <summary>
48+
/// Encodes a stripe index into the upper bits of a base value to create a CorrelationId.
49+
/// </summary>
50+
/// <param name="baseValue">The base value (e.g., from an incrementing counter XORed with a seed).</param>
51+
/// <param name="stripeIndex">The stripe index (typically derived from thread id).</param>
52+
/// <returns>A CorrelationId with the stripe encoded in the upper bits.</returns>
53+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
54+
public static CorrelationId CreateCorrelationId(long baseValue, int stripeIndex)
55+
{
56+
// Clear the upper StripeBits of the base value and set the stripe index there
57+
long maskedBase = baseValue & ~StripeMask;
58+
long stripeValue = (long)(stripeIndex & (StripeCount - 1)) << StripeShift;
59+
return new CorrelationId(maskedBase | stripeValue);
60+
}
61+
62+
/// <summary>
63+
/// Extracts the stripe index from a CorrelationId.
64+
/// </summary>
65+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
66+
public static int GetStripeIndex(CorrelationId correlationId)
67+
{
68+
return (int)((correlationId.ToInt64() & StripeMask) >>> StripeShift);
69+
}
70+
71+
/// <summary>
72+
/// Gets the stripe index for the current thread. Use this when creating new CorrelationIds.
73+
/// </summary>
74+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
75+
public static int GetCurrentThreadStripeIndex()
76+
{
77+
return Environment.CurrentManagedThreadId & (StripeCount - 1);
78+
}
79+
80+
/// <summary>
81+
/// Gets the stripe for the given correlation id.
82+
/// </summary>
83+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
84+
private Stripe GetStripe(CorrelationId correlationId)
85+
{
86+
return _stripes[GetStripeIndex(correlationId)];
87+
}
88+
89+
/// <summary>
90+
/// Attempts to add the specified key and value to the dictionary.
91+
/// </summary>
92+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
93+
public bool TryAdd(CorrelationId key, TValue value)
94+
{
95+
var stripe = GetStripe(key);
96+
lock (stripe.Lock)
97+
{
98+
return stripe.Dictionary.TryAdd(key, value);
99+
}
100+
}
101+
102+
/// <summary>
103+
/// Attempts to get the value associated with the specified key.
104+
/// </summary>
105+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
106+
public bool TryGetValue(CorrelationId key, out TValue? value)
107+
{
108+
var stripe = GetStripe(key);
109+
lock (stripe.Lock)
110+
{
111+
return stripe.Dictionary.TryGetValue(key, out value);
112+
}
113+
}
114+
115+
/// <summary>
116+
/// Attempts to remove the value with the specified key.
117+
/// </summary>
118+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
119+
public bool TryRemove(CorrelationId key, out TValue? value)
120+
{
121+
var stripe = GetStripe(key);
122+
lock (stripe.Lock)
123+
{
124+
return stripe.Dictionary.Remove(key, out value);
125+
}
126+
}
127+
128+
/// <summary>
129+
/// Gets the approximate total count of items across all stripes.
130+
/// </summary>
131+
public int Count
132+
{
133+
get
134+
{
135+
int count = 0;
136+
foreach (var stripe in _stripes)
137+
{
138+
lock (stripe.Lock)
139+
{
140+
count += stripe.Dictionary.Count;
141+
}
142+
}
143+
return count;
144+
}
145+
}
146+
147+
/// <summary>
148+
/// Counts items matching a predicate across all stripes.
149+
/// </summary>
150+
public int CountWhere(Func<KeyValuePair<CorrelationId, TValue>, bool> predicate)
151+
{
152+
int count = 0;
153+
foreach (var stripe in _stripes)
154+
{
155+
lock (stripe.Lock)
156+
{
157+
foreach (var kvp in stripe.Dictionary)
158+
{
159+
if (predicate(kvp))
160+
{
161+
count++;
162+
}
163+
}
164+
}
165+
}
166+
return count;
167+
}
168+
169+
/// <summary>
170+
/// Returns an enumerator that iterates through all items in all stripes.
171+
/// Note: This takes a snapshot of each stripe under its lock.
172+
/// </summary>
173+
public Enumerator GetEnumerator() => new(this);
174+
175+
IEnumerator<KeyValuePair<CorrelationId, TValue>> IEnumerable<KeyValuePair<CorrelationId, TValue>>.GetEnumerator() => GetEnumerator();
176+
177+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
178+
179+
private sealed class Stripe
180+
{
181+
public readonly object Lock = new();
182+
public readonly Dictionary<CorrelationId, TValue> Dictionary = new();
183+
}
184+
185+
public struct Enumerator : IEnumerator<KeyValuePair<CorrelationId, TValue>>
186+
{
187+
private readonly StripedCallbackDictionary<TValue> _dictionary;
188+
private int _stripeIndex;
189+
private List<KeyValuePair<CorrelationId, TValue>>? _currentSnapshot;
190+
private int _snapshotIndex;
191+
192+
internal Enumerator(StripedCallbackDictionary<TValue> dictionary)
193+
{
194+
_dictionary = dictionary;
195+
_stripeIndex = -1;
196+
_currentSnapshot = null;
197+
_snapshotIndex = -1;
198+
}
199+
200+
public KeyValuePair<CorrelationId, TValue> Current => _currentSnapshot![_snapshotIndex];
201+
202+
object IEnumerator.Current => Current;
203+
204+
public bool MoveNext()
205+
{
206+
while (true)
207+
{
208+
// Try to advance within current snapshot
209+
if (_currentSnapshot != null)
210+
{
211+
_snapshotIndex++;
212+
if (_snapshotIndex < _currentSnapshot.Count)
213+
{
214+
return true;
215+
}
216+
}
217+
218+
// Move to next stripe
219+
_stripeIndex++;
220+
if (_stripeIndex >= _dictionary._stripes.Length)
221+
{
222+
_currentSnapshot = null;
223+
return false;
224+
}
225+
226+
// Take a snapshot of the next stripe
227+
var stripe = _dictionary._stripes[_stripeIndex];
228+
lock (stripe.Lock)
229+
{
230+
if (stripe.Dictionary.Count > 0)
231+
{
232+
_currentSnapshot = new List<KeyValuePair<CorrelationId, TValue>>(stripe.Dictionary);
233+
_snapshotIndex = -1;
234+
}
235+
else
236+
{
237+
_currentSnapshot = null;
238+
}
239+
}
240+
}
241+
}
242+
243+
public void Reset()
244+
{
245+
_stripeIndex = -1;
246+
_currentSnapshot = null;
247+
_snapshotIndex = -1;
248+
}
249+
250+
public void Dispose()
251+
{
252+
_currentSnapshot = null;
253+
}
254+
}
255+
}

0 commit comments

Comments
 (0)