Skip to content

Commit dd4d949

Browse files
committed
collection: share kernel BTF when loading a collection
We currently create a new copy of kernel BTF when loading each program in a collection, which is quite expensive. Introduce a new type btf.Cache which allows re-using kernel BTF as-needed. As an optimisation, the Cache is opportunistically populated from the global cache on creation. core: 1 goos: linux goarch: amd64 pkg: github.com/cilium/ebpf cpu: 13th Gen Intel(R) Core(TM) i7-1365U │ base.txt │ cache.txt │ │ sec/op │ sec/op vs base │ NewCollectionManyProgs 347.84m ± 1% 14.54m ± 2% -95.82% (p=0.002 n=6) │ base.txt │ cache.txt │ │ B/op │ B/op vs base │ NewCollectionManyProgs 108.056Mi ± 0% 3.841Mi ± 0% -96.44% (p=0.002 n=6) │ base.txt │ cache.txt │ │ allocs/op │ allocs/op vs base │ NewCollectionManyProgs 1026.83k ± 0% 38.04k ± 0% -96.30% (p=0.002 n=6) Signed-off-by: Lorenz Bauer <lmb@isovalent.com>
1 parent 3c63cc3 commit dd4d949

File tree

6 files changed

+241
-68
lines changed

6 files changed

+241
-68
lines changed

btf/kernel.go

Lines changed: 133 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import (
1212
"github.com/cilium/ebpf/internal/platform"
1313
)
1414

15-
var kernelBTF = struct {
15+
// globalCache amortises decoding BTF across all users of the library.
16+
var globalCache = struct {
1617
sync.RWMutex
1718
kernel *Spec
1819
modules map[string]*Spec
@@ -22,98 +23,112 @@ var kernelBTF = struct {
2223

2324
// FlushKernelSpec removes any cached kernel type information.
2425
func FlushKernelSpec() {
25-
kernelBTF.Lock()
26-
defer kernelBTF.Unlock()
26+
globalCache.Lock()
27+
defer globalCache.Unlock()
2728

28-
kernelBTF.kernel = nil
29-
kernelBTF.modules = make(map[string]*Spec)
29+
globalCache.kernel = nil
30+
globalCache.modules = make(map[string]*Spec)
3031
}
3132

3233
// LoadKernelSpec returns the current kernel's BTF information.
3334
//
3435
// Defaults to /sys/kernel/btf/vmlinux and falls back to scanning the file system
3536
// for vmlinux ELFs. Returns an error wrapping ErrNotSupported if BTF is not enabled.
37+
//
38+
// Consider using [Cache] instead.
3639
func LoadKernelSpec() (*Spec, error) {
37-
kernelBTF.RLock()
38-
spec := kernelBTF.kernel
39-
kernelBTF.RUnlock()
40-
41-
if spec == nil {
42-
kernelBTF.Lock()
43-
defer kernelBTF.Unlock()
40+
spec, err := loadCachedKernelSpec()
41+
return spec.Copy(), err
42+
}
4443

45-
spec = kernelBTF.kernel
46-
}
44+
// load (and cache) the kernel spec.
45+
//
46+
// Does not copy Spec.
47+
func loadCachedKernelSpec() (*Spec, error) {
48+
globalCache.RLock()
49+
spec := globalCache.kernel
50+
globalCache.RUnlock()
4751

4852
if spec != nil {
49-
return spec.Copy(), nil
53+
return spec, nil
5054
}
5155

52-
spec, _, err := loadKernelSpec()
56+
globalCache.Lock()
57+
defer globalCache.Unlock()
58+
59+
spec, err := loadKernelSpec()
5360
if err != nil {
5461
return nil, err
5562
}
5663

57-
kernelBTF.kernel = spec
58-
return spec.Copy(), nil
64+
globalCache.kernel = spec
65+
return spec, nil
5966
}
6067

6168
// LoadKernelModuleSpec returns the BTF information for the named kernel module.
6269
//
70+
// Using [Cache.Module] is faster when loading BTF for more than one module.
71+
//
6372
// Defaults to /sys/kernel/btf/<module>.
6473
// Returns an error wrapping ErrNotSupported if BTF is not enabled.
6574
// Returns an error wrapping fs.ErrNotExist if BTF for the specific module doesn't exist.
6675
func LoadKernelModuleSpec(module string) (*Spec, error) {
67-
kernelBTF.RLock()
68-
spec := kernelBTF.modules[module]
69-
kernelBTF.RUnlock()
76+
spec, err := loadCachedKernelModuleSpec(module)
77+
return spec.Copy(), err
78+
}
79+
80+
// load (and cache) a module spec.
81+
//
82+
// Does not copy Spec.
83+
func loadCachedKernelModuleSpec(module string) (*Spec, error) {
84+
globalCache.RLock()
85+
spec := globalCache.modules[module]
86+
globalCache.RUnlock()
7087

7188
if spec != nil {
72-
return spec.Copy(), nil
89+
return spec, nil
7390
}
7491

75-
base, err := LoadKernelSpec()
92+
base, err := loadCachedKernelSpec()
7693
if err != nil {
77-
return nil, fmt.Errorf("load kernel spec: %w", err)
94+
return nil, err
7895
}
7996

80-
kernelBTF.Lock()
81-
defer kernelBTF.Unlock()
82-
83-
if spec = kernelBTF.modules[module]; spec != nil {
84-
return spec.Copy(), nil
85-
}
97+
// NB: This only allows a single module to be parsed at a time. Not sure
98+
// it makes a difference.
99+
globalCache.Lock()
100+
defer globalCache.Unlock()
86101

87102
spec, err = loadKernelModuleSpec(module, base)
88103
if err != nil {
89-
return nil, fmt.Errorf("load kernel module: %w", err)
104+
return nil, err
90105
}
91106

92-
kernelBTF.modules[module] = spec
93-
return spec.Copy(), nil
107+
globalCache.modules[module] = spec
108+
return spec, nil
94109
}
95110

96-
func loadKernelSpec() (_ *Spec, fallback bool, _ error) {
111+
func loadKernelSpec() (_ *Spec, _ error) {
97112
if platform.IsWindows {
98-
return nil, false, internal.ErrNotSupportedOnOS
113+
return nil, internal.ErrNotSupportedOnOS
99114
}
100115

101116
fh, err := os.Open("/sys/kernel/btf/vmlinux")
102117
if err == nil {
103118
defer fh.Close()
104119

105120
spec, err := loadRawSpec(fh, internal.NativeEndian, nil)
106-
return spec, false, err
121+
return spec, err
107122
}
108123

109124
file, err := findVMLinux()
110125
if err != nil {
111-
return nil, false, err
126+
return nil, err
112127
}
113128
defer file.Close()
114129

115130
spec, err := LoadSpecFromReader(file)
116-
return spec, true, err
131+
return spec, err
117132
}
118133

119134
func loadKernelModuleSpec(module string, base *Spec) (*Spec, error) {
@@ -168,3 +183,83 @@ func findVMLinux() (*os.File, error) {
168183

169184
return nil, fmt.Errorf("no BTF found for kernel version %s: %w", release, internal.ErrNotSupported)
170185
}
186+
187+
// Cache allows to amortise the cost of decoding BTF across multiple call-sites.
188+
//
189+
// It is not safe for concurrent use.
190+
type Cache struct {
191+
KernelTypes *Spec
192+
KernelModules map[string]*Spec
193+
}
194+
195+
// NewCache creates a new Cache.
196+
//
197+
// Opportunistically reuses a global cache if possible.
198+
func NewCache() *Cache {
199+
globalCache.RLock()
200+
defer globalCache.RUnlock()
201+
202+
// This copy is either a no-op or very cheap, since the spec won't contain
203+
// any inflated types.
204+
kernel := globalCache.kernel.Copy()
205+
if kernel == nil {
206+
return &Cache{}
207+
}
208+
209+
modules := make(map[string]*Spec, len(globalCache.modules))
210+
for name, spec := range globalCache.modules {
211+
decoder, _ := rebaseDecoder(spec.decoder, kernel.decoder)
212+
// NB: Kernel module BTF can't contain ELF fixups because it is always
213+
// read from sysfs.
214+
modules[name] = &Spec{decoder: decoder}
215+
}
216+
217+
return &Cache{kernel, modules}
218+
}
219+
220+
// Kernel is equivalent to [LoadKernelSpec], except that repeated calls do
221+
// not copy the Spec.
222+
func (c *Cache) Kernel() (*Spec, error) {
223+
if c.KernelTypes != nil {
224+
return c.KernelTypes, nil
225+
}
226+
227+
var err error
228+
c.KernelTypes, err = LoadKernelSpec()
229+
return c.KernelTypes, err
230+
}
231+
232+
// Module is equivalent to [LoadKernelModuleSpec], except that repeated calls do
233+
// not copy the spec.
234+
//
235+
// All modules also share the return value of [Kernel] as their base.
236+
func (c *Cache) Module(name string) (*Spec, error) {
237+
if spec := c.KernelModules[name]; spec != nil {
238+
return spec, nil
239+
}
240+
241+
if c.KernelModules == nil {
242+
c.KernelModules = make(map[string]*Spec)
243+
}
244+
245+
base, err := c.Kernel()
246+
if err != nil {
247+
return nil, err
248+
}
249+
250+
spec, err := loadCachedKernelModuleSpec(name)
251+
if err != nil {
252+
return nil, err
253+
}
254+
255+
// Important: base is shared between modules. This allows inflating common
256+
// types only once.
257+
decoder, err := rebaseDecoder(spec.decoder, base.decoder)
258+
if err != nil {
259+
return nil, err
260+
}
261+
262+
spec = &Spec{decoder: decoder}
263+
c.KernelModules[name] = spec
264+
return spec, err
265+
}

btf/kernel_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"os"
55
"testing"
66

7+
"github.com/cilium/ebpf/internal/testutils"
8+
79
"github.com/go-quicktest/qt"
810
)
911

@@ -26,3 +28,54 @@ func TestLoadKernelModuleSpec(t *testing.T) {
2628
_, err := LoadKernelModuleSpec("bpf_testmod")
2729
qt.Assert(t, qt.IsNil(err))
2830
}
31+
32+
func TestCache(t *testing.T) {
33+
FlushKernelSpec()
34+
c := NewCache()
35+
36+
qt.Assert(t, qt.IsNil(c.KernelTypes))
37+
qt.Assert(t, qt.HasLen(c.KernelModules, 0))
38+
39+
// Test that Kernel() creates only one copy
40+
spec1, err := c.Kernel()
41+
testutils.SkipIfNotSupported(t, err)
42+
qt.Assert(t, qt.IsNil(err))
43+
qt.Assert(t, qt.IsNotNil(spec1))
44+
45+
spec2, err := c.Kernel()
46+
qt.Assert(t, qt.IsNil(err))
47+
qt.Assert(t, qt.IsNotNil(spec2))
48+
49+
qt.Assert(t, qt.Equals(spec1, spec2))
50+
51+
// Test that Module() creates only one copy
52+
mod1, err := c.Module("bpf_testmod")
53+
if !os.IsNotExist(err) {
54+
qt.Assert(t, qt.IsNil(err))
55+
qt.Assert(t, qt.IsNotNil(mod1))
56+
57+
mod2, err := c.Module("bpf_testmod")
58+
qt.Assert(t, qt.IsNil(err))
59+
qt.Assert(t, qt.IsNotNil(mod2))
60+
61+
qt.Assert(t, qt.Equals(mod1, mod2))
62+
}
63+
64+
// Pre-populate global cache
65+
vmlinux, err := LoadKernelSpec()
66+
qt.Assert(t, qt.IsNil(err))
67+
68+
testmod, err := LoadKernelModuleSpec("bpf_testmod")
69+
if !os.IsNotExist(err) {
70+
qt.Assert(t, qt.IsNil(err))
71+
}
72+
73+
// Test that NewCache populates from global cache
74+
c = NewCache()
75+
qt.Assert(t, qt.IsNotNil(c.KernelTypes))
76+
qt.Assert(t, qt.Not(qt.Equals(c.KernelTypes, vmlinux)))
77+
if testmod != nil {
78+
qt.Assert(t, qt.IsNotNil(c.KernelModules["bpf_testmod"]))
79+
qt.Assert(t, qt.Not(qt.Equals(c.KernelModules["bpf_testmod"], testmod)))
80+
}
81+
}

btf/unmarshal.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,31 @@ func allBtfTypeOffsets(buf []byte, bo binary.ByteOrder, header *btfType) iter.Se
165165
}
166166
}
167167

168+
func rebaseDecoder(d *decoder, base *decoder) (*decoder, error) {
169+
if d.base == nil {
170+
return nil, fmt.Errorf("rebase split spec: not a split spec")
171+
}
172+
173+
if &d.base.raw[0] != &base.raw[0] || len(d.base.raw) != len(base.raw) {
174+
return nil, fmt.Errorf("rebase split spec: raw BTF differs")
175+
}
176+
177+
return &decoder{
178+
base,
179+
d.byteOrder,
180+
d.raw,
181+
d.strings,
182+
d.firstTypeID,
183+
d.offsets,
184+
d.declTags,
185+
d.namedTypes,
186+
sync.Mutex{},
187+
make(map[TypeID]Type),
188+
make(map[Type]TypeID),
189+
make(map[TypeID][2]Bits),
190+
}, nil
191+
}
192+
168193
// Copy performs a deep copy of a decoder and its base.
169194
func (d *decoder) Copy() *decoder {
170195
if d == nil {

collection.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ type collectionLoader struct {
412412
maps map[string]*Map
413413
programs map[string]*Program
414414
vars map[string]*Variable
415+
types *btf.Cache
415416
}
416417

417418
func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collectionLoader, error) {
@@ -436,6 +437,7 @@ func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collec
436437
make(map[string]*Map),
437438
make(map[string]*Program),
438439
make(map[string]*Variable),
440+
newBTFCache(&opts.Programs),
439441
}, nil
440442
}
441443

@@ -587,7 +589,7 @@ func (cl *collectionLoader) loadProgram(progName string) (*Program, error) {
587589
}
588590
}
589591

590-
prog, err := newProgramWithOptions(progSpec, cl.opts.Programs)
592+
prog, err := newProgramWithOptions(progSpec, cl.opts.Programs, cl.types)
591593
if err != nil {
592594
return nil, fmt.Errorf("program %s: %w", progName, err)
593595
}

0 commit comments

Comments
 (0)