Skip to content

Commit 3888fda

Browse files
authored
Avoid repeatedly encoding method name strings in protocol (#41644)
1 parent afa4860 commit 3888fda

File tree

1 file changed

+85
-37
lines changed

1 file changed

+85
-37
lines changed

src/SignalR/server/Core/src/Internal/Utf8HashLookup.cs

Lines changed: 85 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,78 @@ namespace Microsoft.AspNetCore.SignalR.Internal;
1212
/// </summary>
1313
internal sealed class Utf8HashLookup
1414
{
15-
private int[] buckets;
16-
private Slot[] slots;
17-
private int count;
15+
private int[] _buckets;
16+
private int[] _caseSensitiveBuckets;
17+
private Slot[] _slots;
18+
private int _count;
1819

1920
private const int HashCodeMask = 0x7fffffff;
2021

2122
internal Utf8HashLookup()
2223
{
23-
buckets = new int[7];
24-
slots = new Slot[7];
24+
_buckets = new int[7];
25+
_caseSensitiveBuckets = new int[7];
26+
_slots = new Slot[7];
2527
}
2628

2729
internal void Add(string value)
2830
{
29-
var hashCode = GetKeyHashCode(value.AsSpan());
30-
31-
if (count == slots.Length)
31+
if (_count == _slots.Length)
3232
{
3333
Resize();
3434
}
3535

36-
int index = count;
37-
count++;
36+
int slotIndex = _count;
37+
_count++;
38+
39+
var encodedValue = Encoding.UTF8.GetBytes(value);
40+
var hashCode = GetHashCode(value.AsSpan());
41+
var caseSensitiveHashCode = GetCaseSensitiveHashCode(encodedValue);
42+
int bucketIndex = hashCode % _buckets.Length;
43+
int caseSensitiveBucketIndex = caseSensitiveHashCode % _caseSensitiveBuckets.Length;
44+
45+
_slots[slotIndex].hashCode = hashCode;
46+
_slots[slotIndex].caseSensitiveHashCode = caseSensitiveHashCode;
47+
48+
_slots[slotIndex].value = value;
49+
_slots[slotIndex].encodedValue = encodedValue;
50+
51+
_slots[slotIndex].next = _buckets[bucketIndex] - 1;
52+
_slots[slotIndex].caseSensitiveNext = _caseSensitiveBuckets[caseSensitiveBucketIndex] - 1;
53+
54+
_buckets[bucketIndex] = slotIndex + 1;
55+
_caseSensitiveBuckets[caseSensitiveBucketIndex] = slotIndex + 1;
56+
}
57+
58+
internal bool TryGetValue(ReadOnlySpan<byte> encodedValue, [MaybeNullWhen(false), AllowNull] out string value)
59+
{
60+
var caseSensitiveHashCode = GetCaseSensitiveHashCode(encodedValue);
61+
62+
for (var i = _caseSensitiveBuckets[caseSensitiveHashCode % _caseSensitiveBuckets.Length] - 1; i >= 0; i = _slots[i].caseSensitiveNext)
63+
{
64+
if (_slots[i].caseSensitiveHashCode == caseSensitiveHashCode && encodedValue.SequenceEqual(_slots[i].encodedValue.AsSpan()))
65+
{
66+
value = _slots[i].value;
67+
return true;
68+
}
69+
}
3870

39-
int bucket = hashCode % buckets.Length;
40-
slots[index].hashCode = hashCode;
41-
slots[index].key = value;
42-
slots[index].value = value;
43-
slots[index].next = buckets[bucket] - 1;
44-
buckets[bucket] = index + 1;
71+
// If we cannot find a case-sensitive match, we transcode the encodedValue to a stackalloced UTF16 string
72+
// and do an OrdinalIgnoreCase comparison.
73+
return TryGetValueSlow(encodedValue, out value);
4574
}
4675

47-
internal bool TryGetValue(ReadOnlySpan<byte> utf8, [MaybeNullWhen(false), AllowNull] out string value)
76+
private bool TryGetValueSlow(ReadOnlySpan<byte> encodedValue, [MaybeNullWhen(false), AllowNull] out string value)
4877
{
4978
const int StackAllocThreshold = 128;
5079

51-
// Transcode to utf16 for comparison
5280
char[]? pooled = null;
53-
var count = Encoding.UTF8.GetCharCount(utf8);
81+
var count = Encoding.UTF8.GetCharCount(encodedValue);
5482
var chars = count <= StackAllocThreshold ?
5583
stackalloc char[StackAllocThreshold] :
5684
(pooled = ArrayPool<char>.Shared.Rent(count));
57-
var encoded = Encoding.UTF8.GetChars(utf8, chars);
58-
var hasValue = TryGetValue(chars[..encoded], out value);
85+
var encoded = Encoding.UTF8.GetChars(encodedValue, chars);
86+
var hasValue = TryGetValueFromChars(chars[..encoded], out value);
5987
if (pooled is not null)
6088
{
6189
ArrayPool<char>.Shared.Return(pooled);
@@ -64,15 +92,15 @@ internal bool TryGetValue(ReadOnlySpan<byte> utf8, [MaybeNullWhen(false), AllowN
6492
return hasValue;
6593
}
6694

67-
private bool TryGetValue(ReadOnlySpan<char> key, [MaybeNullWhen(false), AllowNull] out string value)
95+
private bool TryGetValueFromChars(ReadOnlySpan<char> key, [MaybeNullWhen(false), AllowNull] out string value)
6896
{
69-
var hashCode = GetKeyHashCode(key);
97+
var hashCode = GetHashCode(key);
7098

71-
for (var i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next)
99+
for (var i = _buckets[hashCode % _buckets.Length] - 1; i >= 0; i = _slots[i].next)
72100
{
73-
if (slots[i].hashCode == hashCode && key.Equals(slots[i].key, StringComparison.OrdinalIgnoreCase))
101+
if (_slots[i].hashCode == hashCode && key.Equals(_slots[i].value, StringComparison.OrdinalIgnoreCase))
74102
{
75-
value = slots[i].value;
103+
value = _slots[i].value;
76104
return true;
77105
}
78106
}
@@ -81,32 +109,52 @@ private bool TryGetValue(ReadOnlySpan<char> key, [MaybeNullWhen(false), AllowNul
81109
return false;
82110
}
83111

84-
private static int GetKeyHashCode(ReadOnlySpan<char> key)
112+
private static int GetHashCode(ReadOnlySpan<char> value) =>
113+
HashCodeMask & string.GetHashCode(value, StringComparison.OrdinalIgnoreCase);
114+
115+
private static int GetCaseSensitiveHashCode(ReadOnlySpan<byte> encodedValue)
85116
{
86-
return HashCodeMask & string.GetHashCode(key, StringComparison.OrdinalIgnoreCase);
117+
var hashCode = new HashCode();
118+
hashCode.AddBytes(encodedValue);
119+
return HashCodeMask & hashCode.ToHashCode();
87120
}
88121

89122
private void Resize()
90123
{
91-
var newSize = checked(count * 2 + 1);
92-
var newBuckets = new int[newSize];
124+
var newSize = checked(_count * 2 + 1);
93125
var newSlots = new Slot[newSize];
94-
Array.Copy(slots, newSlots, count);
95-
for (int i = 0; i < count; i++)
126+
127+
var newBuckets = new int[newSize];
128+
var newCaseSensitiveBuckets = new int[newSize];
129+
130+
Array.Copy(_slots, newSlots, _count);
131+
132+
for (int i = 0; i < _count; i++)
96133
{
97134
int bucket = newSlots[i].hashCode % newSize;
98135
newSlots[i].next = newBuckets[bucket] - 1;
99136
newBuckets[bucket] = i + 1;
137+
138+
int caseSensitiveBucket = newSlots[i].caseSensitiveHashCode % newSize;
139+
newSlots[i].caseSensitiveNext = newCaseSensitiveBuckets[caseSensitiveBucket] - 1;
140+
newCaseSensitiveBuckets[caseSensitiveBucket] = i + 1;
100141
}
101-
buckets = newBuckets;
102-
slots = newSlots;
142+
143+
_slots = newSlots;
144+
145+
_buckets = newBuckets;
146+
_caseSensitiveBuckets = newCaseSensitiveBuckets;
103147
}
104148

105-
internal struct Slot
149+
private struct Slot
106150
{
107151
internal int hashCode;
108-
internal int next;
109-
internal string key;
152+
internal int caseSensitiveHashCode;
153+
110154
internal string value;
155+
internal byte[] encodedValue;
156+
157+
internal int next;
158+
internal int caseSensitiveNext;
111159
}
112160
}

0 commit comments

Comments
 (0)