Repository: cilium/ebpf
Pull request #1785 (2025-05-20 16:03:43)

kallsyms: optimize things a bit
Two small optimisations which drop CPU time by 25% on my machine, plus a lot less allocations.


Patch:
diff --git a/internal/kallsyms/kallsyms.go b/internal/kallsyms/kallsyms.go
index c7b12c4..9154a8a 100644
--- a/internal/kallsyms/kallsyms.go
+++ b/internal/kallsyms/kallsyms.go
@@ -1,13 +1,13 @@
 package kallsyms
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
 	"io"
 	"os"
 	"slices"
 	"strconv"
-	"strings"
 
 	"github.com/cilium/ebpf/internal"
 	"github.com/cilium/ebpf/internal/platform"
@@ -114,11 +114,11 @@ func assignModules(f io.Reader, symbols map[string]string) error {
 			continue
 		}
 
-		if _, requested := symbols[s.name]; !requested {
+		if _, requested := symbols[string(s.name)]; !requested {
 			continue
 		}
 
-		if _, ok := found[s.name]; ok {
+		if _, ok := found[string(s.name)]; ok {
 			// We've already seen this symbol. Return an error to avoid silently
 			// attaching to a symbol in the wrong module. libbpf also rejects
 			// referring to ambiguous symbols.
@@ -129,8 +129,8 @@ func assignModules(f io.Reader, symbols map[string]string) error {
 				s.name, s.addr, s.mod, errAmbiguousKsym)
 		}
 
-		symbols[s.name] = s.mod
-		found[s.name] = struct{}{}
+		symbols[string(s.name)] = string(s.mod)
+		found[string(s.name)] = struct{}{}
 	}
 	if err := r.Err(); err != nil {
 		return fmt.Errorf("reading kallsyms: %w", err)
@@ -230,7 +230,7 @@ func assignAddresses(f io.Reader, symbols map[string]uint64) error {
 			continue
 		}
 
-		existing, requested := symbols[s.name]
+		existing, requested := symbols[string(s.name)]
 		if existing != 0 {
 			// Multiple addresses for a symbol have been found. Return a friendly
 			// error to avoid silently attaching to the wrong symbol. libbpf also
@@ -238,7 +238,7 @@ func assignAddresses(f io.Reader, symbols map[string]uint64) error {
 			return fmt.Errorf("symbol %s(0x%x): duplicate found at address 0x%x: %w", s.name, existing, s.addr, errAmbiguousKsym)
 		}
 		if requested {
-			symbols[s.name] = s.addr
+			symbols[string(s.name)] = s.addr
 		}
 	}
 	if err := r.Err(); err != nil {
@@ -250,15 +250,18 @@ func assignAddresses(f io.Reader, symbols map[string]uint64) error {
 
 type ksym struct {
 	addr uint64
-	name string
-	mod  string
+	name []byte
+	mod  []byte
 }
 
 // parseSymbol parses a line from /proc/kallsyms into an address, type, name and
 // module. Skip will be true if the symbol doesn't match any of the given symbol
 // types. See `man 1 nm` for all available types.
 //
-// Example line: `ffffffffc1682010 T nf_nat_init  [nf_nat]`
+// Only yields symbols whose type is contained in types. An empty value for types
+// disables this filtering.
+//
+// Example line: `ffffffffc1682010 T nf_nat_init\t[nf_nat]`
 func parseSymbol(r *reader, types []rune) (s ksym, err error, skip bool) {
 	for i := 0; r.Word(); i++ {
 		switch i {
@@ -276,10 +279,10 @@ func parseSymbol(r *reader, types []rune) (s ksym, err error, skip bool) {
 			}
 		// Name of the symbol.
 		case 2:
-			s.name = r.Text()
+			s.name = r.Bytes()
 		// Kernel module the symbol is provided by.
 		case 3:
-			s.mod = strings.Trim(r.Text(), "[]")
+			s.mod = bytes.Trim(r.Bytes(), "[]")
 		// Ignore any future fields.
 		default:
 			return
diff --git a/internal/kallsyms/reader.go b/internal/kallsyms/reader.go
index 2bd4f8e..3011e83 100644
--- a/internal/kallsyms/reader.go
+++ b/internal/kallsyms/reader.go
@@ -2,9 +2,8 @@ package kallsyms
 
 import (
 	"bufio"
+	"bytes"
 	"io"
-	"unicode"
-	"unicode/utf8"
 )
 
 // reader is a line and word-oriented reader built for reading /proc/kallsyms.
@@ -71,46 +70,18 @@ func (r *reader) Line() bool {
 // Returns true if a word is found and Word should be called again. Returns
 // false when all words on the line have been read.
 func (r *reader) Word() bool {
-	if len(r.line) == 0 {
-		return false
-	}
-
-	// Find next word start, skipping leading spaces.
-	start := 0
-	for width := 0; start < len(r.line); start += width {
-		var c rune
-		c, width = utf8.DecodeRune(r.line[start:])
-		if !unicode.IsSpace(c) {
-			break
-		}
-	}
+	line := bytes.TrimSpace(r.line)
 
-	// Whitespace scanning reached the end of the line due to trailing whitespace,
-	// meaning there are no more words to read
-	if start == len(r.line) {
+	if len(line) == 0 {
 		return false
 	}
 
-	// Find next word end.
-	for width, i := 0, start; i < len(r.line); i += width {
-		var c rune
-		c, width = utf8.DecodeRune(r.line[i:])
-		if unicode.IsSpace(c) {
-			r.word = r.line[start:i]
-			r.line = r.line[i:]
-			return true
-		}
+	var found bool
+	r.word, r.line, found = bytes.Cut(line, []byte{' '})
+	if !found {
+		r.word, r.line, _ = bytes.Cut(line, []byte{'\t'})
 	}
-
-	// The line contains data, but no end-of-word boundary was found. This is the
-	// last, unterminated word in the line.
-	if len(r.line) > start {
-		r.word = r.line[start:]
-		r.line = nil
-		return true
-	}
-
-	return false
+	return true
 }
 
 func (r *reader) Err() error {


Test patch:
diff --git a/internal/kallsyms/kallsyms_test.go b/internal/kallsyms/kallsyms_test.go
index c037993..588d947 100644
--- a/internal/kallsyms/kallsyms_test.go
+++ b/internal/kallsyms/kallsyms_test.go
@@ -23,7 +23,9 @@ A0000000000000AA a nft_counter_seq	[nft_counter]
 A0000000000000BA b bootconfig_found
 A0000000000000CA d __func__.10
 A0000000000000DA r __ksymtab_LZ4_decompress_fast
-A0000000000000EA t writenote`)
+A0000000000000EA t writenote
+A0000000000000FA T bench_sym	[bench_mod]
+A0000000000000FF t __kstrtab_功能	[mod]`)
 
 func TestParseSyms(t *testing.T) {
 	r := newReader(bytes.NewReader(syms))
@@ -33,10 +35,10 @@ func TestParseSyms(t *testing.T) {
 		qt.Assert(t, qt.IsNil(err))
 		qt.Assert(t, qt.IsFalse(skip))
 		qt.Assert(t, qt.Not(qt.Equals(s.addr, 0)))
-		qt.Assert(t, qt.Not(qt.Equals(s.name, "")))
+		qt.Assert(t, qt.Not(qt.Equals(s.name, []byte(""))))
 	}
 	qt.Assert(t, qt.IsNil(r.Err()))
-	qt.Assert(t, qt.Equals(i, 13))
+	qt.Assert(t, qt.Equals(i, 15))
 }
 
 func TestParseProcKallsyms(t *testing.T) {
@@ -46,7 +48,7 @@ func TestParseProcKallsyms(t *testing.T) {
 		s, err, skip := parseSymbol(r, nil)
 		qt.Assert(t, qt.IsNil(err))
 		qt.Assert(t, qt.IsFalse(skip))
-		qt.Assert(t, qt.Not(qt.Equals(s.name, "")))
+		qt.Assert(t, qt.Not(qt.Equals(s.name, []byte(""))))
 	}
 	qt.Assert(t, qt.IsNil(r.Err()))
 }
@@ -79,6 +81,7 @@ func TestAssignModules(t *testing.T) {
 		"nft_counter_seq":   "",
 		"tcp_connect":       "",
 		"foo":               "",
+		"__kstrtab_功能":      "",
 	}
 	qt.Assert(t, qt.IsNil(assignModules(bytes.NewBuffer(syms), mods)))
 	qt.Assert(t, qt.DeepEquals(mods, map[string]string{
@@ -86,6 +89,7 @@ func TestAssignModules(t *testing.T) {
 		"nft_counter_seq":   "", // wrong symbol type
 		"tcp_connect":       "",
 		"foo":               "",
+		"__kstrtab_功能":      "mod",
 	}))
 
 	qt.Assert(t, qt.ErrorIs(assignModules(bytes.NewBuffer(syms),
@@ -132,7 +136,35 @@ func TestAssignAddresses(t *testing.T) {
 	qt.Assert(t, qt.ErrorIs(assignAddresses(b, ksyms), errAmbiguousKsym))
 }
 
-func BenchmarkSymbolKmods(b *testing.B) {
+func BenchmarkAssignModules(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		b.StopTimer()
+		f := bytes.NewBuffer(syms)
+		want := map[string]string{"bench_sym": ""}
+		b.StartTimer()
+
+		if err := assignModules(f, want); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkAssignAddresses(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		b.StopTimer()
+		f := bytes.NewBuffer(syms)
+		want := map[string]uint64{"bench_sym": 0}
+		b.StartTimer()
+
+		if err := assignAddresses(f, want); err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+func BenchmarkAssignModulesKallsyms(b *testing.B) {
 	b.ReportAllocs()
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()
@@ -153,7 +185,7 @@ func BenchmarkSymbolKmods(b *testing.B) {
 }
 
 // Benchmark getting 5 kernel symbols from /proc/kallsyms.
-func BenchmarkAssignAddresses(b *testing.B) {
+func BenchmarkAssignAddressesKallsyms(b *testing.B) {
 	b.ReportAllocs()
 	for i := 0; i < b.N; i++ {
 		b.StopTimer()