Skip to content

Commit bce15e7

Browse files
tomberganbradfitz
authored andcommitted
http2/hpack: speedup Encoder.searchTable
The previous algorithm was linear in the size of the table. The new algorithm is O(1). The new algorithm should behave exactly like the old algorthm, except for one unimportant change that triggered small updates to tests in encode_test.go: When encoding "Field: Value" where the table has two entries, [0]={"Field", "X"} and [1]={"Field", "Y"}, we can encode the field name using either table entry. Previously, we selected the oldest entry, but now we select the newest entry. The new implementation should actually generate very slightly better compression because new entries are encoded with smaller integers than old entries, and HPACK uses a varint encoding for integers where smaller integers are encoded in fewer bytes. I added a synthetic microbenchmark which shows a big speedup in hpack.Encoder.searchTable: BenchmarkEncoderSearchTable-40 100000 127440 ns/op # before BenchmarkEncoderSearchTable-40 50000 25121 ns/op # after Change-Id: Ib87d61b6415d9f0ff38874fe2a719b2f00351590 Reviewed-on: https://go-review.googlesource.com/37406 Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent bb80766 commit bce15e7

File tree

6 files changed

+476
-282
lines changed

6 files changed

+476
-282
lines changed

http2/hpack/encode.go

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func NewEncoder(w io.Writer) *Encoder {
3939
tableSizeUpdate: false,
4040
w: w,
4141
}
42+
e.dynTab.table.init()
4243
e.dynTab.setMaxSize(initialHeaderTableSize)
4344
return e
4445
}
@@ -88,24 +89,17 @@ func (e *Encoder) WriteField(f HeaderField) error {
8889
// only name matches, i points to that index and nameValueMatch
8990
// becomes false.
9091
func (e *Encoder) searchTable(f HeaderField) (i uint64, nameValueMatch bool) {
91-
for idx, hf := range staticTable {
92-
if hf.Name != f.Name {
93-
continue
94-
}
95-
if i == 0 {
96-
i = uint64(idx + 1)
97-
}
98-
if f.Sensitive || hf.Value != f.Value {
99-
continue
100-
}
101-
return uint64(idx + 1), true
92+
i, nameValueMatch = staticTable.search(f)
93+
if nameValueMatch {
94+
return i, true
10295
}
10396

104-
j, nameValueMatch := e.dynTab.search(f)
97+
j, nameValueMatch := e.dynTab.table.search(f)
10598
if nameValueMatch || (i == 0 && j != 0) {
106-
i = j + uint64(len(staticTable))
99+
return j + uint64(staticTable.len()), nameValueMatch
107100
}
108-
return
101+
102+
return i, false
109103
}
110104

111105
// SetMaxDynamicTableSize changes the dynamic header table size to v.

http2/hpack/encode_test.go

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ package hpack
77
import (
88
"bytes"
99
"encoding/hex"
10+
"fmt"
11+
"math/rand"
1012
"reflect"
1113
"strings"
1214
"testing"
@@ -101,17 +103,20 @@ func TestEncoderSearchTable(t *testing.T) {
101103
wantMatch bool
102104
}{
103105
// Name and Value match
104-
{pair("foo", "bar"), uint64(len(staticTable) + 3), true},
105-
{pair("blake", "miz"), uint64(len(staticTable) + 2), true},
106+
{pair("foo", "bar"), uint64(staticTable.len()) + 3, true},
107+
{pair("blake", "miz"), uint64(staticTable.len()) + 2, true},
106108
{pair(":method", "GET"), 2, true},
107109

108-
// Only name match because Sensitive == true
109-
{HeaderField{":method", "GET", true}, 2, false},
110+
// Only name match because Sensitive == true. This is allowed to match
111+
// any ":method" entry. The current implementation uses the last entry
112+
// added in newStaticTable.
113+
{HeaderField{":method", "GET", true}, 3, false},
110114

111115
// Only Name matches
112-
{pair("foo", "..."), uint64(len(staticTable) + 3), false},
113-
{pair("blake", "..."), uint64(len(staticTable) + 2), false},
114-
{pair(":method", "..."), 2, false},
116+
{pair("foo", "..."), uint64(staticTable.len()) + 3, false},
117+
{pair("blake", "..."), uint64(staticTable.len()) + 2, false},
118+
// As before, this is allowed to match any ":method" entry.
119+
{pair(":method", "..."), 3, false},
115120

116121
// None match
117122
{pair("foo-", "bar"), 0, false},
@@ -328,3 +333,54 @@ func TestEncoderSetMaxDynamicTableSizeLimit(t *testing.T) {
328333
func removeSpace(s string) string {
329334
return strings.Replace(s, " ", "", -1)
330335
}
336+
337+
func BenchmarkEncoderSearchTable(b *testing.B) {
338+
e := NewEncoder(nil)
339+
340+
// A sample of possible header fields.
341+
// This is not based on any actual data from HTTP/2 traces.
342+
var possible []HeaderField
343+
for _, f := range staticTable.ents {
344+
if f.Value == "" {
345+
possible = append(possible, f)
346+
continue
347+
}
348+
// Generate 5 random values, except for cookie and set-cookie,
349+
// which we know can have many values in practice.
350+
num := 5
351+
if f.Name == "cookie" || f.Name == "set-cookie" {
352+
num = 25
353+
}
354+
for i := 0; i < num; i++ {
355+
f.Value = fmt.Sprintf("%s-%d", f.Name, i)
356+
possible = append(possible, f)
357+
}
358+
}
359+
for k := 0; k < 10; k++ {
360+
f := HeaderField{
361+
Name: fmt.Sprintf("x-header-%d", k),
362+
Sensitive: rand.Int()%2 == 0,
363+
}
364+
for i := 0; i < 5; i++ {
365+
f.Value = fmt.Sprintf("%s-%d", f.Name, i)
366+
possible = append(possible, f)
367+
}
368+
}
369+
370+
// Add a random sample to the dynamic table. This very loosely simulates
371+
// a history of 100 requests with 20 header fields per request.
372+
for r := 0; r < 100*20; r++ {
373+
f := possible[rand.Int31n(int32(len(possible)))]
374+
// Skip if this is in the staticTable verbatim.
375+
if _, has := staticTable.search(f); !has {
376+
e.dynTab.add(f)
377+
}
378+
}
379+
380+
b.ResetTimer()
381+
for n := 0; n < b.N; n++ {
382+
for _, f := range possible {
383+
e.searchTable(f)
384+
}
385+
}
386+
}

http2/hpack/hpack.go

Lines changed: 24 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func NewDecoder(maxDynamicTableSize uint32, emitFunc func(f HeaderField)) *Decod
102102
emit: emitFunc,
103103
emitEnabled: true,
104104
}
105+
d.dynTab.table.init()
105106
d.dynTab.allowedMaxSize = maxDynamicTableSize
106107
d.dynTab.setMaxSize(maxDynamicTableSize)
107108
return d
@@ -154,12 +155,9 @@ func (d *Decoder) SetAllowedMaxDynamicTableSize(v uint32) {
154155
}
155156

156157
type dynamicTable struct {
157-
// ents is the FIFO described at
158158
// http://http2.github.io/http2-spec/compression.html#rfc.section.2.3.2
159-
// The newest (low index) is append at the end, and items are
160-
// evicted from the front.
161-
ents []HeaderField
162-
size uint32
159+
table headerFieldTable
160+
size uint32 // in bytes
163161
maxSize uint32 // current maxSize
164162
allowedMaxSize uint32 // maxSize may go up to this, inclusive
165163
}
@@ -169,77 +167,45 @@ func (dt *dynamicTable) setMaxSize(v uint32) {
169167
dt.evict()
170168
}
171169

172-
// TODO: change dynamicTable to be a struct with a slice and a size int field,
173-
// per http://http2.github.io/http2-spec/compression.html#rfc.section.4.1:
174-
//
175-
//
176-
// Then make add increment the size. maybe the max size should move from Decoder to
177-
// dynamicTable and add should return an ok bool if there was enough space.
178-
//
179-
// Later we'll need a remove operation on dynamicTable.
180-
181170
func (dt *dynamicTable) add(f HeaderField) {
182-
dt.ents = append(dt.ents, f)
171+
dt.table.addEntry(f)
183172
dt.size += f.Size()
184173
dt.evict()
185174
}
186175

187-
// If we're too big, evict old stuff (front of the slice)
176+
// If we're too big, evict old stuff.
188177
func (dt *dynamicTable) evict() {
189-
base := dt.ents // keep base pointer of slice
190-
for dt.size > dt.maxSize {
191-
dt.size -= dt.ents[0].Size()
192-
dt.ents = dt.ents[1:]
193-
}
194-
195-
// Shift slice contents down if we evicted things.
196-
if len(dt.ents) != len(base) {
197-
copy(base, dt.ents)
198-
dt.ents = base[:len(dt.ents)]
178+
var n int
179+
for dt.size > dt.maxSize && n < dt.table.len() {
180+
dt.size -= dt.table.ents[n].Size()
181+
n++
199182
}
200-
}
201-
202-
// Search searches f in the table. The return value i is 0 if there is
203-
// no name match. If there is name match or name/value match, i is the
204-
// index of that entry (1-based). If both name and value match,
205-
// nameValueMatch becomes true.
206-
func (dt *dynamicTable) search(f HeaderField) (i uint64, nameValueMatch bool) {
207-
l := len(dt.ents)
208-
for j := l - 1; j >= 0; j-- {
209-
ent := dt.ents[j]
210-
if ent.Name != f.Name {
211-
continue
212-
}
213-
if i == 0 {
214-
i = uint64(l - j)
215-
}
216-
if f.Sensitive {
217-
continue
218-
}
219-
if ent.Value != f.Value {
220-
continue
221-
}
222-
return uint64(l - j), true
223-
}
224-
return i, false
183+
dt.table.evictOldest(n)
225184
}
226185

227186
func (d *Decoder) maxTableIndex() int {
228-
return len(d.dynTab.ents) + len(staticTable)
187+
// This should never overflow. RFC 7540 Section 6.5.2 limits the size of
188+
// the dynamic table to 2^32 bytes, where each entry will occupy more than
189+
// one byte. Further, the staticTable has a fixed, small length.
190+
return d.dynTab.table.len() + staticTable.len()
229191
}
230192

231193
func (d *Decoder) at(i uint64) (hf HeaderField, ok bool) {
232-
if i < 1 {
194+
// See Section 2.3.3.
195+
if i == 0 {
233196
return
234197
}
198+
if i <= uint64(staticTable.len()) {
199+
return staticTable.ents[i-1], true
200+
}
235201
if i > uint64(d.maxTableIndex()) {
236202
return
237203
}
238-
if i <= uint64(len(staticTable)) {
239-
return staticTable[i-1], true
240-
}
241-
dents := d.dynTab.ents
242-
return dents[len(dents)-(int(i)-len(staticTable))], true
204+
// In the dynamic table, newer entries have lower indices.
205+
// However, dt.ents[0] is the oldest entry. Hence, dt.ents is
206+
// the reversed dynamic table.
207+
dt := d.dynTab.table
208+
return dt.ents[dt.len()-(int(i)-staticTable.len())], true
243209
}
244210

245211
// Decode decodes an entire block.

0 commit comments

Comments
 (0)