cilium / ebpf

ebpf-go is a pure-Go library to read, modify and load eBPF programs and attach them to various hooks in the Linux kernel.

Home Page:https://ebpf-go.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

sysenc: dealing with `[]struct{ ... }` forces allocations

lmb opened this issue · comments

We currently inherit an allocation problem from encoding/binary: binary.Size doesn't cache it's result for slices of structs. This means that our zero-alloc code path via sysenc isn't zero-alloc, since we end up hitting an allocation in reflect.

A benchmark to illustrate:

$ go test -run XX -bench 'BenchmarkUnmarshal/sysenc\.explicitPad' ./internal/sysenc/
goos: linux
goarch: amd64
pkg: github.com/cilium/ebpf/internal/sysenc
cpu: 12th Gen Intel(R) Core(TM) i7-1260P
BenchmarkUnmarshal/*sysenc.explicitPad-16         	25056632	       44.09 ns/op	      0 B/op	      0 allocs/op
BenchmarkUnmarshal/[]sysenc.explicitPad-16        	18953799	       64.01 ns/op	      8 B/op	      1 allocs/op
BenchmarkUnmarshal/[]sysenc.explicitPad#01-16     	18458588	       65.54 ns/op	      8 B/op	      1 allocs/op
BenchmarkUnmarshal/[]sysenc.explicitPad#02-16     	18186916	       67.05 ns/op	      8 B/op	      1 allocs/op
PASS
ok  	github.com/cilium/ebpf/internal/sysenc	5.075s

Going from a single explicitPad to a slice of them causes an allocation. The allocation happens in binary.dataSize:

Total: 152MB
ROUTINE ======================== encoding/binary.Size in /usr/local/go/src/encoding/binary/binary.go
         0      152MB (flat, cum)   100% of Total
         .          .    466:func Size(v any) int {
         .      152MB    467:	return dataSize(reflect.Indirect(reflect.ValueOf(v)))
         .          .    468:}
         .          .    469:
         .          .    470:var structSize sync.Map // map[reflect.Type]int
         .          .    471:
         .          .    472:// dataSize returns the number of bytes the actual data represented by v occupies in memory.
ROUTINE ======================== encoding/binary.dataSize in /usr/local/go/src/encoding/binary/binary.go
         0      152MB (flat, cum)   100% of Total
         .          .    476:func dataSize(v reflect.Value) int {
         .          .    477:	switch v.Kind() {
         .          .    478:	case reflect.Slice:
         .      152MB    479:		if s := sizeof(v.Type().Elem()); s >= 0 {
         .          .    480:			return s * v.Len()
         .          .    481:		}
         .          .    482:
         .          .    483:	case reflect.Struct:
         .          .    484:		t := v.Type()

In the past I've fixed the same problem for plain structs using a cache: golang/go@c9d89f6 I didn't take slices of structs into account since they are kind of esoteric. Turns out we now need to encode those when doing zero-alloc batch lookups, see #1254

One option is to fix this upstream, but it'll take a long time for this to reach us. I think we should fix upstream and in addition copy the implementation of binary.Size and fix the problem in our copy.