Skip to content

Commit f2133fa

Browse files
committed
perf(hpack): replace List with ring buffer to eliminate O(n) eviction
1 parent cc7ae18 commit f2133fa

1 file changed

Lines changed: 61 additions & 22 deletions

File tree

src/TurboHTTP/Protocol/Http2/Hpack/HpackDynamicTable.cs

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@ namespace TurboHTTP.Protocol.Http2.Hpack;
1212
/// </summary>
1313
internal sealed class HpackDynamicTable
1414
{
15-
// Each slot stores the header, its name byte length, and total RFC 7541 §4.1 entry size.
16-
private readonly List<(HpackHeader Header, int NameByteLength, int EncodedSize)> _entries = [];
15+
private (HpackHeader Header, int NameByteLength, int EncodedSize)[] _ring;
16+
private int _head;
17+
private int _count;
1718

1819
private readonly Dictionary<string, int> _nameIndex = new(StringComparer.OrdinalIgnoreCase);
1920

2021
private int _evictedCount;
2122

23+
public HpackDynamicTable() : this(16) { }
24+
25+
private HpackDynamicTable(int initialCapacity)
26+
{
27+
_ring = new (HpackHeader, int, int)[initialCapacity];
28+
}
29+
2230
/// <summary>Currently configured maximum table size in bytes.</summary>
2331
public int MaxSize { get; private set; } = 4096;
2432

@@ -56,8 +64,15 @@ public void Add(string name, string value)
5664
return;
5765
}
5866

59-
var absolutePos = _evictedCount + _entries.Count;
60-
_entries.Add((new HpackHeader(name, value), nameByteLength, entrySize));
67+
if (_count == _ring.Length)
68+
{
69+
Grow();
70+
}
71+
72+
var absolutePos = _evictedCount + _count;
73+
var index = (_head + _count) % _ring.Length;
74+
_ring[index] = (new HpackHeader(name, value), nameByteLength, entrySize);
75+
_count++;
6176
_nameIndex[name] = absolutePos;
6277
CurrentSize += entrySize;
6378
Evict();
@@ -69,12 +84,14 @@ public void Add(string name, string value)
6984
/// </summary>
7085
public HpackHeader? GetEntry(int dynamicIndex)
7186
{
72-
if (dynamicIndex <= 0 || dynamicIndex > _entries.Count)
87+
if (dynamicIndex <= 0 || dynamicIndex > _count)
7388
{
7489
return null;
7590
}
7691

77-
return _entries[^dynamicIndex].Header;
92+
var listIndex = _count - dynamicIndex;
93+
var ringIndex = (_head + listIndex) % _ring.Length;
94+
return _ring[ringIndex].Header;
7895
}
7996

8097
/// <summary>
@@ -83,17 +100,19 @@ public void Add(string name, string value)
83100
/// </summary>
84101
public (HpackHeader Header, int NameByteLength, int EncodedSize)? GetEntryWithSizes(int dynamicIndex)
85102
{
86-
if (dynamicIndex <= 0 || dynamicIndex > _entries.Count)
103+
if (dynamicIndex <= 0 || dynamicIndex > _count)
87104
{
88105
return null;
89106
}
90107

91-
var entry = _entries[^dynamicIndex];
108+
var listIndex = _count - dynamicIndex;
109+
var ringIndex = (_head + listIndex) % _ring.Length;
110+
var entry = _ring[ringIndex];
92111
return (entry.Header, entry.NameByteLength, entry.EncodedSize);
93112
}
94113

95114
/// <summary>Number of entries currently in the dynamic table.</summary>
96-
public int Count => _entries.Count;
115+
public int Count => _count;
97116

98117
/// <summary>
99118
/// O(1) lookup: finds the 1-based dynamic index for a full (name+value) match.
@@ -107,24 +126,26 @@ public int FindFullMatch(string name, string value)
107126
}
108127

109128
var listIndex = absolutePos - _evictedCount;
110-
if (listIndex < 0 || listIndex >= _entries.Count)
129+
if (listIndex < 0 || listIndex >= _count)
111130
{
112131
return 0;
113132
}
114133

115-
var entry = _entries[listIndex];
134+
var ringIndex = (_head + listIndex) % _ring.Length;
135+
var entry = _ring[ringIndex];
116136
if (string.Equals(entry.Header.Value, value, StringComparison.Ordinal))
117137
{
118-
return _entries.Count - listIndex;
138+
return _count - listIndex;
119139
}
120140

121-
for (var i = _entries.Count - 1; i >= 0; i--)
141+
for (var i = _count - 1; i >= 0; i--)
122142
{
123-
var e = _entries[i];
143+
var ri = (_head + i) % _ring.Length;
144+
var e = _ring[ri];
124145
if (string.Equals(e.Header.Name, name, StringComparison.OrdinalIgnoreCase) &&
125146
string.Equals(e.Header.Value, value, StringComparison.Ordinal))
126147
{
127-
return _entries.Count - i;
148+
return _count - i;
128149
}
129150
}
130151

@@ -143,35 +164,53 @@ public int FindNameMatch(string name)
143164
}
144165

145166
var listIndex = absolutePos - _evictedCount;
146-
if (listIndex < 0 || listIndex >= _entries.Count)
167+
if (listIndex < 0 || listIndex >= _count)
147168
{
148169
return 0;
149170
}
150171

151-
return _entries.Count - listIndex;
172+
return _count - listIndex;
152173
}
153174

154175
private void Evict()
155176
{
156-
while (CurrentSize > MaxSize && _entries.Count > 0)
177+
while (CurrentSize > MaxSize && _count > 0)
157178
{
158-
var oldest = _entries[0];
179+
var oldest = _ring[_head];
159180
CurrentSize -= oldest.EncodedSize;
160181

161182
if (_nameIndex.TryGetValue(oldest.Header.Name, out var pos) && pos == _evictedCount)
162183
{
163184
_nameIndex.Remove(oldest.Header.Name);
164185
}
165186

166-
_entries.RemoveAt(0);
187+
_ring[_head] = default;
188+
_head = (_head + 1) % _ring.Length;
189+
_count--;
167190
_evictedCount++;
168191
}
169192
}
170193

171194
private void Clear()
172195
{
173-
_entries.Clear();
196+
Array.Clear(_ring, 0, _ring.Length);
197+
_head = 0;
198+
_count = 0;
174199
_nameIndex.Clear();
175200
CurrentSize = 0;
176201
}
177-
}
202+
203+
private void Grow()
204+
{
205+
var newCapacity = _ring.Length * 2;
206+
var newRing = new (HpackHeader, int, int)[newCapacity];
207+
208+
for (var i = 0; i < _count; i++)
209+
{
210+
newRing[i] = _ring[(_head + i) % _ring.Length];
211+
}
212+
213+
_ring = newRing;
214+
_head = 0;
215+
}
216+
}

0 commit comments

Comments
 (0)