From 48339fe8d8010accbfc1df954e7f3f116c4ed578 Mon Sep 17 00:00:00 2001 From: Francisco Javier Honduvilla Coto Date: Tue, 19 Sep 2023 13:07:59 +0100 Subject: [PATCH] proc_maps: Parse address and device without allocating In our project we need to parse proc maps pretty frequently and we've noticed that there are lots of small allocations coming from parsing the device and addresses from procfs' maps file. The rough split of memory allocated is: - bufio.(*Scanner).Text: 25% - strings.Split: 50% - string.Fields: 25% The two callers of strings.Split are the two parsing functions that we are optimising here. I've added some benchmarks to show the improvements. Before ====== ``` $ go test -benchmem -run=^$ -bench ^BenchmarkParse.*$ github.com/prometheus/procfs goos: linux goarch: amd64 pkg: github.com/prometheus/procfs cpu: Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz BenchmarkParseAddress-12 12218004 123.0 ns/op 32 B/op 1 allocs/op BenchmarkParseDevice-12 15074881 85.11 ns/op 32 B/op 1 allocs/op PASS ok github.com/prometheus/procfs 2.978s ``` After ===== ``` $ go test -benchmem -run=^$ -bench ^BenchmarkParse.*$ github.com/prometheus/procfs goos: linux goarch: amd64 pkg: github.com/prometheus/procfs cpu: Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz BenchmarkParseAddress-12 28619314 50.45 ns/op 0 B/op 0 allocs/op BenchmarkParseDevice-12 49721935 29.66 ns/op 0 B/op 0 allocs/op PASS ok github.com/prometheus/procfs 2.991s ``` Signed-off-by: Francisco Javier Honduvilla Coto --- proc_maps.go | 20 ++++++++++---------- proc_maps64_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/proc_maps.go b/proc_maps.go index 727549a13..7e75c286b 100644 --- a/proc_maps.go +++ b/proc_maps.go @@ -63,17 +63,17 @@ type ProcMap struct { // parseDevice parses the device token of a line and converts it to a dev_t // (mkdev) like structure. func parseDevice(s string) (uint64, error) { - toks := strings.Split(s, ":") - if len(toks) < 2 { - return 0, fmt.Errorf("%w: unexpected number of fields, expected: 2, got: %q", ErrFileParse, len(toks)) + i := strings.Index(s, ":") + if i == -1 { + return 0, fmt.Errorf("%w: expected separator `:` in %s", ErrFileParse, s) } - major, err := strconv.ParseUint(toks[0], 16, 0) + major, err := strconv.ParseUint(s[0:i], 16, 0) if err != nil { return 0, err } - minor, err := strconv.ParseUint(toks[1], 16, 0) + minor, err := strconv.ParseUint(s[i+1:], 16, 0) if err != nil { return 0, err } @@ -93,17 +93,17 @@ func parseAddress(s string) (uintptr, error) { // parseAddresses parses the start-end address. func parseAddresses(s string) (uintptr, uintptr, error) { - toks := strings.Split(s, "-") - if len(toks) < 2 { - return 0, 0, fmt.Errorf("%w: invalid address", ErrFileParse) + idx := strings.Index(s, "-") + if idx == -1 { + return 0, 0, fmt.Errorf("%w: expected separator `-` in %s", ErrFileParse, s) } - saddr, err := parseAddress(toks[0]) + saddr, err := parseAddress(s[0:idx]) if err != nil { return 0, 0, err } - eaddr, err := parseAddress(toks[1]) + eaddr, err := parseAddress(s[idx+1:]) if err != nil { return 0, 0, err } diff --git a/proc_maps64_test.go b/proc_maps64_test.go index 66ef1349d..44ec68c46 100644 --- a/proc_maps64_test.go +++ b/proc_maps64_test.go @@ -138,3 +138,40 @@ func TestProcMaps(t *testing.T) { } } + +var start, end uintptr + +func BenchmarkParseAddress(b *testing.B) { + b.ReportAllocs() + var ( + s, e uintptr + err error + ) + for i := 0; i < b.N; i++ { + s, e, err = parseAddresses("7f7d7469e000-7f7d746a0000") + if err != nil { + b.Fatal(err) + } + } + // Prevent the compiler from optimizing away benchmark code. + start = s + end = e +} + +var device uint64 + +func BenchmarkParseDevice(b *testing.B) { + b.ReportAllocs() + var ( + d uint64 + err error + ) + for i := 0; i < b.N; i++ { + d, err = parseDevice("00:22") + if err != nil { + b.Fatal(err) + } + } + // Prevent the compiler from optimizing away benchmark code. + device = d +}