Skip to content

Commit 601e57c

Browse files
committed
btf: avoid constructing strings for named type index
The most common use case of a Spec is to look up a type by its name. For this purpose we maintain a map[essentialName][]TypeID. This requires allocating a string for each named type, which causes a very large overhead when parsing BTF. In reality, only a very small number of the named types will ever be looked up. The intuition here is that a couple of structs in the kernel contain most of the interesting information, for example struct sk_buff. Move as much of the cost of looking up a type by name to the actual lookup. Instead of spending a lot of time constructing an index up front we only maintaing an index going from the hash of a name to a type ID. 1. We can compute the hash on a byte slice and therefore avoid allocating a string. 2. Storing the index as a (hash, id) tuple allows us to store it in a slice. Lookups are just a binary search into the index. 3. Hash collisions do not introduce additional complexity because types can already share the same name. At the same time the common case of a 1:1 mapping from name to type is fast. Signed-off-by: Lorenz Bauer <[email protected]>
1 parent 3da296f commit 601e57c

File tree

6 files changed

+177
-52
lines changed

6 files changed

+177
-52
lines changed

btf/btf.go

+10-17
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"math"
1313
"os"
1414
"reflect"
15+
"slices"
1516

1617
"github.com/cilium/ebpf/internal"
1718
"github.com/cilium/ebpf/internal/sys"
@@ -399,28 +400,20 @@ func (s *Spec) TypeID(typ Type) (TypeID, error) {
399400
//
400401
// Returns an error wrapping ErrNotFound if no matching Type exists in the Spec.
401402
func (s *Spec) AnyTypesByName(name string) ([]Type, error) {
402-
typeIDs := s.TypeIDsByName(newEssentialName(name))
403-
if len(typeIDs) == 0 {
404-
return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
405-
}
406-
407-
// Return a copy to prevent changes to namedTypes.
408-
result := make([]Type, 0, len(typeIDs))
409-
for _, id := range typeIDs {
410-
typ, err := s.TypeByID(id)
411-
if errors.Is(err, ErrNotFound) {
412-
return nil, fmt.Errorf("no type with ID %d", id)
413-
} else if err != nil {
414-
return nil, err
415-
}
403+
types, err := s.decoder.TypesByName(newEssentialName(name))
404+
if err != nil {
405+
return nil, err
406+
}
416407

408+
for i := 0; i < len(types); i++ {
417409
// Match against the full name, not just the essential one
418410
// in case the type being looked up is a struct flavor.
419-
if typ.TypeName() == name {
420-
result = append(result, typ)
411+
if types[i].TypeName() != name {
412+
types = slices.Delete(types, i, i+1)
421413
}
422414
}
423-
return result, nil
415+
416+
return types, nil
424417
}
425418

426419
// AnyTypeByName returns a Type with the given name.

btf/btf_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ func BenchmarkIterateVmlinux(b *testing.B) {
249249
func TestParseCurrentKernelBTF(t *testing.T) {
250250
spec := vmlinuxSpec(t)
251251

252-
if len(spec.namedTypes) == 0 {
252+
if len(spec.offsets) == 0 {
253253
t.Fatal("Empty kernel BTF")
254254
}
255255
}
@@ -267,7 +267,7 @@ func TestFindVMLinux(t *testing.T) {
267267
t.Fatal("Can't load BTF:", err)
268268
}
269269

270-
if len(spec.namedTypes) == 0 {
270+
if len(spec.offsets) == 0 {
271271
t.Fatal("Empty kernel BTF")
272272
}
273273
}

btf/core.go

+5-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"math"
88
"reflect"
9-
"slices"
109
"strconv"
1110
"strings"
1211

@@ -265,16 +264,12 @@ func CORERelocate(relos []*CORERelocation, targets []*Spec, bo binary.ByteOrder,
265264

266265
var targetTypes []Type
267266
for _, target := range targets {
268-
namedTypeIDs := target.TypeIDsByName(essentialName)
269-
targetTypes = slices.Grow(targetTypes, len(namedTypeIDs))
270-
for _, id := range namedTypeIDs {
271-
typ, err := target.TypeByID(id)
272-
if err != nil {
273-
return nil, err
274-
}
275-
276-
targetTypes = append(targetTypes, typ)
267+
namedTypes, err := target.TypesByName(essentialName)
268+
if err != nil {
269+
return nil, err
277270
}
271+
272+
targetTypes = append(targetTypes, namedTypes...)
278273
}
279274

280275
fixups, err := coreCalculateFixups(group.relos, targetTypes, bo, resolveTargetTypeID)

btf/strings.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,20 @@ func (st *stringTable) Lookup(offset uint32) (string, error) {
5454
return "", nil
5555
}
5656

57+
b, err := st.lookupSlow(offset)
58+
return string(b), err
59+
}
60+
61+
func (st *stringTable) LookupBytes(offset uint32) ([]byte, error) {
62+
// Fast path: zero offset is the empty string, looked up frequently.
63+
if offset == 0 {
64+
return nil, nil
65+
}
66+
5767
return st.lookupSlow(offset)
5868
}
5969

60-
func (st *stringTable) lookupSlow(offset uint32) (string, error) {
70+
func (st *stringTable) lookupSlow(offset uint32) ([]byte, error) {
6171
if st.base != nil {
6272
n := uint32(len(st.base.bytes))
6373
if offset < n {
@@ -67,15 +77,15 @@ func (st *stringTable) lookupSlow(offset uint32) (string, error) {
6777
}
6878

6979
if offset > uint32(len(st.bytes)) {
70-
return "", fmt.Errorf("offset %d is out of bounds of string table", offset)
80+
return nil, fmt.Errorf("offset %d is out of bounds of string table", offset)
7181
}
7282

7383
if offset > 0 && st.bytes[offset-1] != 0 {
74-
return "", fmt.Errorf("offset %d is not the beginning of a string", offset)
84+
return nil, fmt.Errorf("offset %d is not the beginning of a string", offset)
7585
}
7686

7787
i := bytes.IndexByte(st.bytes[offset:], 0)
78-
return string(st.bytes[offset : offset+uint32(i)]), nil
88+
return st.bytes[offset : offset+uint32(i)], nil
7989
}
8090

8191
// stringTableBuilder builds BTF string tables.

btf/unmarshal.go

+110-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package btf
22

33
import (
4+
"bytes"
45
"encoding/binary"
56
"fmt"
7+
"hash/maphash"
68
"io"
79
"iter"
810
"maps"
911
"math"
12+
"slices"
1013
"sync"
1114
)
1215

@@ -21,9 +24,10 @@ type decoder struct {
2124
firstTypeID TypeID
2225
// Map from TypeID to offset of the marshaled data in raw. Contains an entry
2326
// for each TypeID, including 0 aka Void. The offset for Void is invalid.
24-
offsets []int
25-
declTags map[TypeID][]TypeID
26-
namedTypes map[essentialName][]TypeID
27+
offsets []int
28+
declTags map[TypeID][]TypeID
29+
// An index from essentialName to TypeID.
30+
namedTypes *fuzzyStringIndex
2731

2832
// Protection for mutable fields below.
2933
mu sync.Mutex
@@ -74,7 +78,7 @@ func newDecoder(raw []byte, bo binary.ByteOrder, strings *stringTable, base *dec
7478

7579
offsets := make([]int, 0, numTypes)
7680
declTags := make(map[TypeID][]TypeID, numDeclTags)
77-
namedTypes := make(map[essentialName][]TypeID, numNamedTypes)
81+
namedTypes := newFuzzyStringIndex(numNamedTypes)
7882

7983
if firstTypeID == 0 {
8084
// Add a sentinel for Void.
@@ -94,27 +98,27 @@ func newDecoder(raw []byte, bo binary.ByteOrder, strings *stringTable, base *dec
9498
}
9599

96100
// Build named type index.
97-
name, err := strings.Lookup(header.NameOff)
101+
name, err := strings.LookupBytes(header.NameOff)
98102
if err != nil {
99103
return nil, fmt.Errorf("lookup type name for id %v: %w", id, err)
100104
}
101105

102-
if name := newEssentialName(name); name != "" {
103-
ids := namedTypes[name]
104-
if ids == nil {
105-
// Almost all names will only have a single name to them.
106-
// Explicitly allocate a slice of capacity 1 instead of relying
107-
// on append behaviour.
108-
ids = []TypeID{id}
109-
} else {
110-
ids = append(ids, id)
106+
if len(name) > 0 {
107+
if i := bytes.Index(name, []byte("___")); i != -1 {
108+
// Flavours are rare. It's cheaper to find the first index for some
109+
// reason.
110+
i = bytes.LastIndex(name, []byte("___"))
111+
name = name[:i]
111112
}
112-
namedTypes[name] = ids
113+
114+
namedTypes.Add(name, id)
113115
}
114116

115117
id++
116118
}
117119

120+
namedTypes.Build()
121+
118122
return &decoder{
119123
base,
120124
bo,
@@ -211,11 +215,28 @@ func (d *decoder) TypeID(typ Type) (TypeID, error) {
211215
return id, nil
212216
}
213217

214-
// TypeIDsByName returns all type IDs which have the given essential name.
218+
// TypesByName returns all types which have the given essential name.
215219
//
216-
// The returned slice must not be modified.
217-
func (d *decoder) TypeIDsByName(name essentialName) []TypeID {
218-
return d.namedTypes[name]
220+
// Returns ErrNotFound if no matching Type exists.
221+
func (d *decoder) TypesByName(name essentialName) ([]Type, error) {
222+
var types []Type
223+
for id := range d.namedTypes.Find(string(name)) {
224+
typ, err := d.TypeByID(id)
225+
if err != nil {
226+
return nil, err
227+
}
228+
229+
if newEssentialName(typ.TypeName()) == name {
230+
// Deal with hash collisions by checking against the name.
231+
types = append(types, typ)
232+
}
233+
}
234+
235+
if len(types) == 0 {
236+
return nil, fmt.Errorf("type with name %s: %w", name, ErrNotFound)
237+
}
238+
239+
return types, nil
219240
}
220241

221242
// TypeByID decodes a type and any of its descendants.
@@ -649,3 +670,73 @@ func (d *decoder) inflateType(id TypeID) (typ Type, err error) {
649670

650671
return typ, nil
651672
}
673+
674+
// An index from string to TypeID.
675+
//
676+
// Fuzzy because it may return false positive matches.
677+
type fuzzyStringIndex struct {
678+
seed maphash.Seed
679+
entries []fuzzyStringIndexEntry
680+
}
681+
682+
func newFuzzyStringIndex(capacity int) *fuzzyStringIndex {
683+
return &fuzzyStringIndex{
684+
maphash.MakeSeed(),
685+
make([]fuzzyStringIndexEntry, 0, capacity),
686+
}
687+
}
688+
689+
// Add a string to the index.
690+
//
691+
// Calling the method with identical arguments will create duplicate entries.
692+
func (idx *fuzzyStringIndex) Add(name []byte, id TypeID) {
693+
hash := uint32(maphash.Bytes(idx.seed, name))
694+
idx.entries = append(idx.entries, newFuzzyStringIndexEntry(hash, id))
695+
}
696+
697+
// Build the index.
698+
//
699+
// Must be called after [Add] and before [Match].
700+
func (idx *fuzzyStringIndex) Build() {
701+
slices.Sort(idx.entries)
702+
}
703+
704+
// Find TypeIDs which may match the name.
705+
//
706+
// May return false positives, but is guaranteed to not have false negatives.
707+
//
708+
// You must call [Build] at least once before calling this method.
709+
func (idx *fuzzyStringIndex) Find(name string) iter.Seq[TypeID] {
710+
return func(yield func(TypeID) bool) {
711+
hash := uint32(maphash.String(idx.seed, name))
712+
713+
// We match only on the first 32 bits here, so ignore found.
714+
i, _ := slices.BinarySearch(idx.entries, fuzzyStringIndexEntry(hash)<<32)
715+
for i := i; i < len(idx.entries); i++ {
716+
if idx.entries[i].hash() != hash {
717+
break
718+
}
719+
720+
if !yield(idx.entries[i].id()) {
721+
return
722+
}
723+
}
724+
}
725+
}
726+
727+
// Tuple mapping the hash of an essential name to a type.
728+
//
729+
// Encoded in an uint64 so that it implements cmp.Ordered.
730+
type fuzzyStringIndexEntry uint64
731+
732+
func newFuzzyStringIndexEntry(hash uint32, id TypeID) fuzzyStringIndexEntry {
733+
return fuzzyStringIndexEntry(hash)<<32 | fuzzyStringIndexEntry(id)
734+
}
735+
736+
func (e fuzzyStringIndexEntry) hash() uint32 {
737+
return uint32(e >> 32)
738+
}
739+
740+
func (e fuzzyStringIndexEntry) id() TypeID {
741+
return TypeID(e)
742+
}

btf/unmarshal_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package btf
2+
3+
import (
4+
"iter"
5+
"math"
6+
"testing"
7+
8+
"github.com/go-quicktest/qt"
9+
)
10+
11+
func TestFuzzyStringIndex(t *testing.T) {
12+
idx := newFuzzyStringIndex(10)
13+
count := testing.AllocsPerRun(1, func() {
14+
idx.Add([]byte("foo"), 1)
15+
})
16+
qt.Assert(t, qt.Equals(count, 0))
17+
18+
idx.entries = idx.entries[:0]
19+
idx.Add([]byte("foo"), 1)
20+
idx.Add([]byte("bar"), 2)
21+
idx.Add([]byte("baz"), 3)
22+
idx.Build()
23+
24+
all := func(it iter.Seq[TypeID]) (ids []TypeID) {
25+
for id := range it {
26+
ids = append(ids, id)
27+
}
28+
return
29+
}
30+
31+
qt.Assert(t, qt.SliceContains(all(idx.Find("foo")), 1))
32+
qt.Assert(t, qt.SliceContains(all(idx.Find("bar")), 2))
33+
qt.Assert(t, qt.SliceContains(all(idx.Find("baz")), 3))
34+
35+
qt.Assert(t, qt.IsTrue(newFuzzyStringIndexEntry(0, math.MaxUint32) < newFuzzyStringIndexEntry(1, 0)))
36+
}

0 commit comments

Comments
 (0)