You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-11-27 22:49:15 +02:00
I am looking into I am looking into
https://promlabs.com/blog/2025/07/17/why-i-recommend-native-prometheus-instrumentation-over-opentelemetry/#comparing-counter-increment-performance,
and was trying to figure out why incrementing a counter with 10
attributes was so much slower than incrementing a counter with no
attributes, or 1 attribute:
```
$ go test -run=xxxxxMatchNothingxxxxx -cpu=1 -test.benchtime=1s -bench=BenchmarkSyncMeasure/NoView/Int64Counter/Attributes
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/sdk/metric
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
BenchmarkSyncMeasure/NoView/Int64Counter/Attributes/0 9905773 121.3 ns/op
BenchmarkSyncMeasure/NoView/Int64Counter/Attributes/1 4079145 296.5 ns/op
BenchmarkSyncMeasure/NoView/Int64Counter/Attributes/10 781627 1531 ns/op
```
Looking at the profile, most of the time is spent in
"runtime.mapKeyError2" within "runtime.mapaccess2". My best guess is
that whatever we are using for Equivalent() is not very performant when
used as a map key. This seems like a good opportunity to greatly improve
the performance of our metrics (and probably other signals) API + SDK.
To start, i'm adding a simple benchmark within the attribute package to
isolate the issue. Results:
```
$ go test -run '^$' -bench '^BenchmarkEquivalentMapAccess' -benchtime .1s -cpu 1 -benchmem
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/attribute
cpu: Intel(R) Xeon(R) CPU @ 2.20GHz
BenchmarkEquivalentMapAccess/Empty 2220508 53.58 ns/op 0 B/op 0 allocs/op
BenchmarkEquivalentMapAccess/1_string_attribute 622770 196.7 ns/op 0 B/op 0 allocs/op
BenchmarkEquivalentMapAccess/10_string_attributes 77462 1558 ns/op 0 B/op 0 allocs/op
BenchmarkEquivalentMapAccess/1_int_attribute 602163 197.7 ns/op 0 B/op 0 allocs/op
BenchmarkEquivalentMapAccess/10_int_attributes 76603 1569 ns/op 0 B/op 0 allocs/op
```
This shows that it is the map lookup and storage itself that is making
the metrics API+SDK perform much worse with more attributes.
Some optimization ideas include:
* Most attribute sets are likely to be just numbers and strings. Can we
make a fast path for sets that don't include complex attributes?
* We encourage improving performance of the metrics API by re-using
attribute sets where possible. If we can lazily compute+cache a "faster"
map key, that will have a big performance improvement when attribute
sets are re-used.
* compute a uint64 hash using something like
https://github.com/gohugoio/hashstructure, or something similar to what
prometheus/client_golang does:
c79a891c6c/model/signature.go (L31)
---------
Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
Co-authored-by: Flc゛ <four_leaf_clover@foxmail.com>
330 lines
7.4 KiB
Go
330 lines
7.4 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package attribute_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
)
|
|
|
|
// Store results in a file scope var to ensure compiler does not optimize the
|
|
// test away.
|
|
var (
|
|
outV attribute.Value
|
|
outKV attribute.KeyValue
|
|
|
|
outBool bool
|
|
outBoolSlice []bool
|
|
outInt64 int64
|
|
outInt64Slice []int64
|
|
outFloat64 float64
|
|
outFloat64Slice []float64
|
|
outStr string
|
|
outStrSlice []string
|
|
)
|
|
|
|
func benchmarkEmit(kv attribute.KeyValue) func(*testing.B) {
|
|
return func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outStr = kv.Value.Emit()
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkBool(b *testing.B) {
|
|
k, v := "bool", true
|
|
kv := attribute.Bool(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.BoolValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Bool(k, v)
|
|
}
|
|
})
|
|
b.Run("AsBool", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outBool = kv.Value.AsBool()
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkBoolSlice(b *testing.B) {
|
|
k, v := "bool slice", []bool{true, false, true}
|
|
kv := attribute.BoolSlice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.BoolSliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.BoolSlice(k, v)
|
|
}
|
|
})
|
|
b.Run("AsBoolSlice", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outBoolSlice = kv.Value.AsBoolSlice()
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkInt(b *testing.B) {
|
|
k, v := "int", int(42)
|
|
kv := attribute.Int(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.IntValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Int(k, v)
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkIntSlice(b *testing.B) {
|
|
k, v := "int slice", []int{42, -3, 12}
|
|
kv := attribute.IntSlice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.IntSliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.IntSlice(k, v)
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkInt64(b *testing.B) {
|
|
k, v := "int64", int64(42)
|
|
kv := attribute.Int64(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.Int64Value(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Int64(k, v)
|
|
}
|
|
})
|
|
b.Run("AsInt64", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outInt64 = kv.Value.AsInt64()
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkInt64Slice(b *testing.B) {
|
|
k, v := "int64 slice", []int64{42, -3, 12}
|
|
kv := attribute.Int64Slice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.Int64SliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Int64Slice(k, v)
|
|
}
|
|
})
|
|
b.Run("AsInt64Slice", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outInt64Slice = kv.Value.AsInt64Slice()
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkFloat64(b *testing.B) {
|
|
k, v := "float64", float64(42)
|
|
kv := attribute.Float64(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.Float64Value(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Float64(k, v)
|
|
}
|
|
})
|
|
b.Run("AsFloat64", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outFloat64 = kv.Value.AsFloat64()
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkFloat64Slice(b *testing.B) {
|
|
k, v := "float64 slice", []float64{42, -3, 12}
|
|
kv := attribute.Float64Slice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.Float64SliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.Float64Slice(k, v)
|
|
}
|
|
})
|
|
b.Run("AsFloat64Slice", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outFloat64Slice = kv.Value.AsFloat64Slice()
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkString(b *testing.B) {
|
|
k, v := "string", "42"
|
|
kv := attribute.String(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.StringValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.String(k, v)
|
|
}
|
|
})
|
|
b.Run("AsString", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outStr = kv.Value.AsString()
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
func BenchmarkStringSlice(b *testing.B) {
|
|
k, v := "float64 slice", []string{"forty-two", "negative three", "twelve"}
|
|
kv := attribute.StringSlice(k, v)
|
|
|
|
b.Run("Value", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outV = attribute.StringSliceValue(v)
|
|
}
|
|
})
|
|
b.Run("KeyValue", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outKV = attribute.StringSlice(k, v)
|
|
}
|
|
})
|
|
b.Run("AsStringSlice", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
outStrSlice = kv.Value.AsStringSlice()
|
|
}
|
|
})
|
|
b.Run("Emit", benchmarkEmit(kv))
|
|
}
|
|
|
|
// BenchmarkEquivalentMapAccess measures how expensive it is to use
|
|
// Equivalent() as a map key. This is on the hot path for making synchronous
|
|
// measurements on the metrics API/SDK. It will likely be on the hot path for
|
|
// the trace and logs API/SDK in the future.
|
|
func BenchmarkEquivalentMapAccess(b *testing.B) {
|
|
b.Run("Empty", func(b *testing.B) {
|
|
benchmarkEquivalentMapAccess(b, attribute.EmptySet())
|
|
})
|
|
b.Run("1 string attribute", func(b *testing.B) {
|
|
set := attribute.NewSet(attribute.String("string", "42"))
|
|
benchmarkEquivalentMapAccess(b, &set)
|
|
})
|
|
b.Run("10 string attributes", func(b *testing.B) {
|
|
set := attribute.NewSet(
|
|
attribute.String("a", "42"),
|
|
attribute.String("b", "42"),
|
|
attribute.String("c", "42"),
|
|
attribute.String("d", "42"),
|
|
attribute.String("e", "42"),
|
|
attribute.String("f", "42"),
|
|
attribute.String("g", "42"),
|
|
attribute.String("h", "42"),
|
|
attribute.String("i", "42"),
|
|
attribute.String("j", "42"),
|
|
)
|
|
benchmarkEquivalentMapAccess(b, &set)
|
|
})
|
|
b.Run("1 int attribute", func(b *testing.B) {
|
|
set := attribute.NewSet(attribute.Int("string", 42))
|
|
benchmarkEquivalentMapAccess(b, &set)
|
|
})
|
|
b.Run("10 int attributes", func(b *testing.B) {
|
|
set := attribute.NewSet(
|
|
attribute.Int("a", 42),
|
|
attribute.Int("b", 42),
|
|
attribute.Int("c", 42),
|
|
attribute.Int("d", 42),
|
|
attribute.Int("e", 42),
|
|
attribute.Int("f", 42),
|
|
attribute.Int("g", 42),
|
|
attribute.Int("h", 42),
|
|
attribute.Int("i", 42),
|
|
attribute.Int("j", 42),
|
|
)
|
|
benchmarkEquivalentMapAccess(b, &set)
|
|
})
|
|
}
|
|
|
|
func benchmarkEquivalentMapAccess(b *testing.B, set *attribute.Set) {
|
|
values := map[attribute.Distinct]int{}
|
|
b.ResetTimer()
|
|
for range b.N {
|
|
values[set.Equivalent()]++
|
|
}
|
|
}
|