Skip to content

[performance-profiler] Reduce GenericEventConverter normalizeSlice allocations for custom slice elements #50546

@github-actions

Description

@github-actions

Hot Path

(*GenericEventConverter).normalizeSlice in libbeat/common/event.go:127-167 is a benchmarked hot path for custom typed slices (BenchmarkConvertToGenericEventCustomStringSlice in libbeat/common/event_test.go:455-462).

The baseline implementation:

  • appended into a zero-length []interface{} (growth allocations), and
  • always called normalizeValue with append(keys, strconv.Itoa(i))... (per-element key-slice/string work), even for primitive wrapper elements.

Profiling Data

Before:

go test -run '^$' -bench '^BenchmarkConvertToGenericEventCustomStringSlice$' -benchmem -count=5 -cpuprofile=cpu_before.prof -memprofile=mem_before.prof ./libbeat/common

BenchmarkConvertToGenericEventCustomStringSlice-4    1605834   725.1 ns/op   864 B/op   13 allocs/op
BenchmarkConvertToGenericEventCustomStringSlice-4    1689210   758.3 ns/op   864 B/op   13 allocs/op
BenchmarkConvertToGenericEventCustomStringSlice-4    1701534   723.4 ns/op   864 B/op   13 allocs/op
BenchmarkConvertToGenericEventCustomStringSlice-4    1660251  1024.0 ns/op   864 B/op   13 allocs/op
BenchmarkConvertToGenericEventCustomStringSlice-4     912933  3742.0 ns/op   864 B/op   13 allocs/op

Proposed Change

Use a pre-sized output slice and a primitive-kind fast path in normalizeSlice, and avoid creating indexed key paths unless an error path needs them.

libbeat/common/event.go
- var sliceValues []interface{}
+ sliceValues := make([]interface{}, n)
...
- sliceValue, err := e.normalizeValue(v.Index(i).Interface(), append(keys, strconv.Itoa(i))...)
+ elem := v.Index(i)
+ switch elem.Kind() { ... primitive fast path ... }
+ elemValue := elem.Interface()
+ sliceValue, err := e.normalizeValue(elemValue, keys...)
+ if len(err) > 0 {
+   sliceValue, err = e.normalizeValue(elemValue, append(keys, strconv.Itoa(i))...)
+ }

Also remove one allocation in Convert by switching from make([]string, 0, 10) to stack-backed var keys [10]string (libbeat/common/event.go:56-59).

Results

After:

go test -run '^$' -bench '^BenchmarkConvertToGenericEventCustomStringSlice$' -benchmem -count=5 -cpuprofile=cpu_after.prof -memprofile=mem_after.prof ./libbeat/common

BenchmarkConvertToGenericEventCustomStringSlice-4    1836873   642.0 ns/op   816 B/op   10 allocs/op
BenchmarkConvertToGenericEventCustomStringSlice-4    1878728   614.3 ns/op   816 B/op   10 allocs/op
BenchmarkConvertToGenericEventCustomStringSlice-4    2092221   578.8 ns/op   816 B/op   10 allocs/op
BenchmarkConvertToGenericEventCustomStringSlice-4    2070322   573.6 ns/op   816 B/op   10 allocs/op
BenchmarkConvertToGenericEventCustomStringSlice-4    1994182   595.6 ns/op   816 B/op   10 allocs/op

Improvement:

  • allocs/op: 13 -> 10 (23.08% fewer allocations)
  • B/op: 864 -> 816 (5.56% lower memory)
  • ns/op: first 4-run central tendency drops from ~807 to ~602 (~25% faster)

Verification

  • go test ./libbeat/common passes with the optimization.
  • Benchmark command for before/after is identical and measured against different code states.
  • Change is behavior-preserving (normalization semantics unchanged; only allocation/call-path reductions).

Evidence

Commands used:

  • go test -run '^$' -bench '^BenchmarkConvertToGenericEventCustomStringSlice$' -benchmem -count=5 -cpuprofile=cpu_before.prof -memprofile=mem_before.prof ./libbeat/common
  • go test -run '^$' -bench '^BenchmarkConvertToGenericEventCustomStringSlice$' -benchmem -count=5 -cpuprofile=cpu_after.prof -memprofile=mem_after.prof ./libbeat/common
  • go test ./libbeat/common

Code references:

  • libbeat/common/event.go:56-59
  • libbeat/common/event.go:127-167
  • libbeat/common/event_test.go:455-462

Note

🔒 Integrity filter blocked 18 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

What is this? | From workflow: Performance Profiler

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

  • expires on May 14, 2026, 3:13 PM UTC

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs_teamIndicates that the issue/PR needs a Team:* label

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions