From f2d8adecd644a18311cee2e380b745bd9c991bfd Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Tue, 10 Mar 2026 13:46:08 -0700 Subject: [PATCH 1/8] Optimize attr slice conversion --- attribute/internal/attribute.go | 92 ++++++++-------------------- attribute/internal/attribute_test.go | 48 +++++++++++---- attribute/value.go | 25 ++++---- 3 files changed, 74 insertions(+), 91 deletions(-) diff --git a/attribute/internal/attribute.go b/attribute/internal/attribute.go index 7f5eae877da..d7ecb023d6a 100644 --- a/attribute/internal/attribute.go +++ b/attribute/internal/attribute.go @@ -11,80 +11,42 @@ import ( "reflect" ) -// BoolSliceValue converts a bool slice into an array with same elements as slice. -func BoolSliceValue(v []bool) any { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[bool]())).Elem() +// SliceValue converts a slice into an array with the same elements. +func SliceValue[T any](v []T) any { + switch len(v) { + case 0: + return [0]T{} + case 1: + return [1]T{v[0]} + case 2: + return [2]T{v[0], v[1]} + case 3: + return [3]T{v[0], v[1], v[2]} + } + + cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[T]())).Elem() reflect.Copy(cp, reflect.ValueOf(v)) return cp.Interface() } -// Int64SliceValue converts an int64 slice into an array with same elements as slice. -func Int64SliceValue(v []int64) any { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]())).Elem() - reflect.Copy(cp, reflect.ValueOf(v)) - return cp.Interface() -} - -// Float64SliceValue converts a float64 slice into an array with same elements as slice. -func Float64SliceValue(v []float64) any { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[float64]())).Elem() - reflect.Copy(cp, reflect.ValueOf(v)) - return cp.Interface() -} - -// StringSliceValue converts a string slice into an array with same elements as slice. -func StringSliceValue(v []string) any { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[string]())).Elem() - reflect.Copy(cp, reflect.ValueOf(v)) - return cp.Interface() -} - -// AsBoolSlice converts a bool array into a slice into with same elements as array. -func AsBoolSlice(v any) []bool { - rv := reflect.ValueOf(v) - if rv.Type().Kind() != reflect.Array { - return nil +// AsSlice converts an array into a slice with the same elements. +func AsSlice[T any](v any) []T { + switch a := v.(type) { + case [0]T: + return []T{} + case [1]T: + return []T{a[0]} + case [2]T: + return []T{a[0], a[1]} + case [3]T: + return []T{a[0], a[1], a[2]} } - cpy := make([]bool, rv.Len()) - if len(cpy) > 0 { - _ = reflect.Copy(reflect.ValueOf(cpy), rv) - } - return cpy -} - -// AsInt64Slice converts an int64 array into a slice into with same elements as array. -func AsInt64Slice(v any) []int64 { - rv := reflect.ValueOf(v) - if rv.Type().Kind() != reflect.Array { - return nil - } - cpy := make([]int64, rv.Len()) - if len(cpy) > 0 { - _ = reflect.Copy(reflect.ValueOf(cpy), rv) - } - return cpy -} - -// AsFloat64Slice converts a float64 array into a slice into with same elements as array. -func AsFloat64Slice(v any) []float64 { - rv := reflect.ValueOf(v) - if rv.Type().Kind() != reflect.Array { - return nil - } - cpy := make([]float64, rv.Len()) - if len(cpy) > 0 { - _ = reflect.Copy(reflect.ValueOf(cpy), rv) - } - return cpy -} -// AsStringSlice converts a string array into a slice into with same elements as array. -func AsStringSlice(v any) []string { rv := reflect.ValueOf(v) - if rv.Type().Kind() != reflect.Array { + if !rv.IsValid() || rv.Kind() != reflect.Array || rv.Type().Elem() != reflect.TypeFor[T]() { return nil } - cpy := make([]string, rv.Len()) + cpy := make([]T, rv.Len()) if len(cpy) > 0 { _ = reflect.Copy(reflect.ValueOf(cpy), rv) } diff --git a/attribute/internal/attribute_test.go b/attribute/internal/attribute_test.go index e0ebb06439a..142554eaf08 100644 --- a/attribute/internal/attribute_test.go +++ b/attribute/internal/attribute_test.go @@ -10,37 +10,37 @@ import ( var wrapFloat64SliceValue = func(v any) any { if vi, ok := v.([]float64); ok { - return Float64SliceValue(vi) + return SliceValue(vi) } return nil } var wrapInt64SliceValue = func(v any) any { if vi, ok := v.([]int64); ok { - return Int64SliceValue(vi) + return SliceValue(vi) } return nil } var wrapBoolSliceValue = func(v any) any { if vi, ok := v.([]bool); ok { - return BoolSliceValue(vi) + return SliceValue(vi) } return nil } var wrapStringSliceValue = func(v any) any { if vi, ok := v.([]string); ok { - return StringSliceValue(vi) + return SliceValue(vi) } return nil } var ( - wrapAsBoolSlice = func(v any) any { return AsBoolSlice(v) } - wrapAsInt64Slice = func(v any) any { return AsInt64Slice(v) } - wrapAsFloat64Slice = func(v any) any { return AsFloat64Slice(v) } - wrapAsStringSlice = func(v any) any { return AsStringSlice(v) } + wrapAsBoolSlice = func(v any) any { return AsSlice[bool](v) } + wrapAsInt64Slice = func(v any) any { return AsSlice[int64](v) } + wrapAsFloat64Slice = func(v any) any { return AsSlice[float64](v) } + wrapAsStringSlice = func(v any) any { return AsSlice[string](v) } ) func TestSliceValue(t *testing.T) { @@ -95,6 +95,28 @@ func TestSliceValue(t *testing.T) { } } +func TestAsSliceMismatchedType(t *testing.T) { + tests := []struct { + name string + fn func() any + }{ + {name: "bool from int64 array", fn: func() any { return AsSlice[bool]([2]int64{1, 2}) }}, + {name: "int64 from float64 array", fn: func() any { return AsSlice[int64]([2]float64{1, 2}) }}, + {name: "float64 from string array", fn: func() any { return AsSlice[float64]([2]string{"1", "2"}) }}, + {name: "string from bool array", fn: func() any { return AsSlice[string]([2]bool{true, false}) }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.fn() + rv := reflect.ValueOf(got) + if !rv.IsNil() { + t.Fatalf("got %v, want nil", got) + } + }) + } +} + // sync is a global used to ensure the benchmark are not optimized away. var sync any @@ -103,7 +125,7 @@ func BenchmarkBoolSliceValue(b *testing.B) { s := []bool{true, false, true, false} for b.Loop() { - sync = BoolSliceValue(s) + sync = SliceValue(s) } } @@ -112,7 +134,7 @@ func BenchmarkInt64SliceValue(b *testing.B) { s := []int64{1, 2, 3, 4} for b.Loop() { - sync = Int64SliceValue(s) + sync = SliceValue(s) } } @@ -121,7 +143,7 @@ func BenchmarkFloat64SliceValue(b *testing.B) { s := []float64{1.2, 3.4, 5.6, 7.8} for b.Loop() { - sync = Float64SliceValue(s) + sync = SliceValue(s) } } @@ -130,7 +152,7 @@ func BenchmarkStringSliceValue(b *testing.B) { s := []string{"a", "b", "c", "d"} for b.Loop() { - sync = StringSliceValue(s) + sync = SliceValue(s) } } @@ -139,6 +161,6 @@ func BenchmarkAsFloat64Slice(b *testing.B) { var in any = [2]float64{1, 2.3} for b.Loop() { - sync = AsFloat64Slice(in) + sync = AsSlice[float64](in) } } diff --git a/attribute/value.go b/attribute/value.go index 5931e71291a..c991ad22799 100644 --- a/attribute/value.go +++ b/attribute/value.go @@ -6,7 +6,6 @@ package attribute // import "go.opentelemetry.io/otel/attribute" import ( "encoding/json" "fmt" - "reflect" "strconv" attribute "go.opentelemetry.io/otel/attribute/internal" @@ -56,7 +55,7 @@ func BoolValue(v bool) Value { // BoolSliceValue creates a BOOLSLICE Value. func BoolSliceValue(v []bool) Value { - return Value{vtype: BOOLSLICE, slice: attribute.BoolSliceValue(v)} + return Value{vtype: BOOLSLICE, slice: attribute.SliceValue(v)} } // IntValue creates an INT64 Value. @@ -64,15 +63,15 @@ func IntValue(v int) Value { return Int64Value(int64(v)) } -// IntSliceValue creates an INTSLICE Value. +// IntSliceValue creates an INT64SLICE Value. func IntSliceValue(v []int) Value { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]())) + cp := make([]int64, len(v)) for i, val := range v { - cp.Elem().Index(i).SetInt(int64(val)) + cp[i] = int64(val) } return Value{ vtype: INT64SLICE, - slice: cp.Elem().Interface(), + slice: attribute.SliceValue(cp), } } @@ -86,7 +85,7 @@ func Int64Value(v int64) Value { // Int64SliceValue creates an INT64SLICE Value. func Int64SliceValue(v []int64) Value { - return Value{vtype: INT64SLICE, slice: attribute.Int64SliceValue(v)} + return Value{vtype: INT64SLICE, slice: attribute.SliceValue(v)} } // Float64Value creates a FLOAT64 Value. @@ -99,7 +98,7 @@ func Float64Value(v float64) Value { // Float64SliceValue creates a FLOAT64SLICE Value. func Float64SliceValue(v []float64) Value { - return Value{vtype: FLOAT64SLICE, slice: attribute.Float64SliceValue(v)} + return Value{vtype: FLOAT64SLICE, slice: attribute.SliceValue(v)} } // StringValue creates a STRING Value. @@ -112,7 +111,7 @@ func StringValue(v string) Value { // StringSliceValue creates a STRINGSLICE Value. func StringSliceValue(v []string) Value { - return Value{vtype: STRINGSLICE, slice: attribute.StringSliceValue(v)} + return Value{vtype: STRINGSLICE, slice: attribute.SliceValue(v)} } // Type returns a type of the Value. @@ -136,7 +135,7 @@ func (v Value) AsBoolSlice() []bool { } func (v Value) asBoolSlice() []bool { - return attribute.AsBoolSlice(v.slice) + return attribute.AsSlice[bool](v.slice) } // AsInt64 returns the int64 value. Make sure that the Value's type is @@ -155,7 +154,7 @@ func (v Value) AsInt64Slice() []int64 { } func (v Value) asInt64Slice() []int64 { - return attribute.AsInt64Slice(v.slice) + return attribute.AsSlice[int64](v.slice) } // AsFloat64 returns the float64 value. Make sure that the Value's @@ -174,7 +173,7 @@ func (v Value) AsFloat64Slice() []float64 { } func (v Value) asFloat64Slice() []float64 { - return attribute.AsFloat64Slice(v.slice) + return attribute.AsSlice[float64](v.slice) } // AsString returns the string value. Make sure that the Value's type @@ -193,7 +192,7 @@ func (v Value) AsStringSlice() []string { } func (v Value) asStringSlice() []string { - return attribute.AsStringSlice(v.slice) + return attribute.AsSlice[string](v.slice) } type unknownValueType struct{} From 5b95036fdb1fd01e6dec7f6a20673d3e1447b37d Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Tue, 10 Mar 2026 14:36:53 -0700 Subject: [PATCH 2/8] Doc fast-path --- attribute/internal/attribute.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/attribute/internal/attribute.go b/attribute/internal/attribute.go index d7ecb023d6a..4ab809d5384 100644 --- a/attribute/internal/attribute.go +++ b/attribute/internal/attribute.go @@ -13,6 +13,10 @@ import ( // SliceValue converts a slice into an array with the same elements. func SliceValue[T any](v []T) any { + // Keep the common tiny-slice cases out of reflection. This matches the + // short lengths that show up most often in local benchmarks and semantic + // convention examples while leaving larger, less predictable slices on the + // generic reflective path. switch len(v) { case 0: return [0]T{} @@ -31,6 +35,7 @@ func SliceValue[T any](v []T) any { // AsSlice converts an array into a slice with the same elements. func AsSlice[T any](v any) []T { + // Mirror the small fixed-array fast path used by SliceValue. switch a := v.(type) { case [0]T: return []T{} From aed3f27498f3b9b4f74709581b111709f57a4c76 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Wed, 11 Mar 2026 10:17:50 -0700 Subject: [PATCH 3/8] Expand slice attr bmarks --- attribute/benchmark_test.go | 248 ++++++++++++++++----------- attribute/internal/attribute_test.go | 90 +++++++--- 2 files changed, 214 insertions(+), 124 deletions(-) diff --git a/attribute/benchmark_test.go b/attribute/benchmark_test.go index 65eda55371e..34c7f0e754c 100644 --- a/attribute/benchmark_test.go +++ b/attribute/benchmark_test.go @@ -60,28 +60,38 @@ func BenchmarkBool(b *testing.B) { } func BenchmarkBoolSlice(b *testing.B) { - k, v := "bool slice", []bool{true, false, true} - kv := attribute.BoolSlice(k, v) + for _, bench := range []struct { + name string + v []bool + }{ + {name: "Len2", v: []bool{true, false}}, + {name: "Len8", v: []bool{true, false, true, false, true, false, true, false}}, + } { + b.Run(bench.name, func(b *testing.B) { + k, v := "bool slice", bench.v + 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)) + 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) { @@ -104,22 +114,32 @@ func BenchmarkInt(b *testing.B) { } func BenchmarkIntSlice(b *testing.B) { - k, v := "int slice", []int{42, -3, 12} - kv := attribute.IntSlice(k, v) + for _, bench := range []struct { + name string + v []int + }{ + {name: "Len2", v: []int{42, -3}}, + {name: "Len8", v: []int{42, -3, 12, 7, 9, 11, -5, 0}}, + } { + b.Run(bench.name, func(b *testing.B) { + k, v := "int slice", bench.v + 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)) + 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) { @@ -148,28 +168,38 @@ func BenchmarkInt64(b *testing.B) { } func BenchmarkInt64Slice(b *testing.B) { - k, v := "int64 slice", []int64{42, -3, 12} - kv := attribute.Int64Slice(k, v) + for _, bench := range []struct { + name string + v []int64 + }{ + {name: "Len2", v: []int64{42, -3}}, + {name: "Len8", v: []int64{42, -3, 12, 7, 9, 11, -5, 0}}, + } { + b.Run(bench.name, func(b *testing.B) { + k, v := "int64 slice", bench.v + 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)) + 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) { @@ -198,28 +228,38 @@ func BenchmarkFloat64(b *testing.B) { } func BenchmarkFloat64Slice(b *testing.B) { - k, v := "float64 slice", []float64{42, -3, 12} - kv := attribute.Float64Slice(k, v) + for _, bench := range []struct { + name string + v []float64 + }{ + {name: "Len2", v: []float64{42, -3}}, + {name: "Len8", v: []float64{42, -3, 12, 7, 9, 11, -5, 0}}, + } { + b.Run(bench.name, func(b *testing.B) { + k, v := "float64 slice", bench.v + 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)) + 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) { @@ -248,28 +288,38 @@ func BenchmarkString(b *testing.B) { } func BenchmarkStringSlice(b *testing.B) { - k, v := "float64 slice", []string{"forty-two", "negative three", "twelve"} - kv := attribute.StringSlice(k, v) + for _, bench := range []struct { + name string + v []string + }{ + {name: "Len2", v: []string{"forty-two", "negative three"}}, + {name: "Len8", v: []string{"forty-two", "negative three", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen"}}, + } { + b.Run(bench.name, func(b *testing.B) { + k, v := "float64 slice", bench.v + 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)) + 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)) + }) + } } func BenchmarkSetEquals(b *testing.B) { diff --git a/attribute/internal/attribute_test.go b/attribute/internal/attribute_test.go index 142554eaf08..59d1d4138c6 100644 --- a/attribute/internal/attribute_test.go +++ b/attribute/internal/attribute_test.go @@ -121,46 +121,86 @@ func TestAsSliceMismatchedType(t *testing.T) { var sync any func BenchmarkBoolSliceValue(b *testing.B) { - b.ReportAllocs() - s := []bool{true, false, true, false} - - for b.Loop() { - sync = SliceValue(s) + for _, bench := range []struct { + name string + s []bool + }{ + {name: "Len2", s: []bool{true, false}}, + {name: "Len8", s: []bool{true, false, true, false, true, false, true, false}}, + } { + b.Run(bench.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + sync = SliceValue(bench.s) + } + }) } } func BenchmarkInt64SliceValue(b *testing.B) { - b.ReportAllocs() - s := []int64{1, 2, 3, 4} - - for b.Loop() { - sync = SliceValue(s) + for _, bench := range []struct { + name string + s []int64 + }{ + {name: "Len2", s: []int64{1, 2}}, + {name: "Len8", s: []int64{1, 2, 3, 4, 5, 6, 7, 8}}, + } { + b.Run(bench.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + sync = SliceValue(bench.s) + } + }) } } func BenchmarkFloat64SliceValue(b *testing.B) { - b.ReportAllocs() - s := []float64{1.2, 3.4, 5.6, 7.8} - - for b.Loop() { - sync = SliceValue(s) + for _, bench := range []struct { + name string + s []float64 + }{ + {name: "Len2", s: []float64{1.2, 3.4}}, + {name: "Len8", s: []float64{1.2, 3.4, 5.6, 7.8, 9.1, 2.3, 4.5, 6.7}}, + } { + b.Run(bench.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + sync = SliceValue(bench.s) + } + }) } } func BenchmarkStringSliceValue(b *testing.B) { - b.ReportAllocs() - s := []string{"a", "b", "c", "d"} - - for b.Loop() { - sync = SliceValue(s) + for _, bench := range []struct { + name string + s []string + }{ + {name: "Len2", s: []string{"a", "b"}}, + {name: "Len8", s: []string{"a", "b", "c", "d", "e", "f", "g", "h"}}, + } { + b.Run(bench.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + sync = SliceValue(bench.s) + } + }) } } func BenchmarkAsFloat64Slice(b *testing.B) { - b.ReportAllocs() - var in any = [2]float64{1, 2.3} - - for b.Loop() { - sync = AsSlice[float64](in) + for _, bench := range []struct { + name string + in any + }{ + {name: "Len2", in: [2]float64{1, 2.3}}, + {name: "Len8", in: [8]float64{1, 2.3, 3.4, 4.5, 5.6, 6.7, 7.8, 8.9}}, + } { + b.Run(bench.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + sync = AsSlice[float64](bench.in) + } + }) } } From 207e722bc521b46222193c44fcaf68b60f6e16a2 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Wed, 11 Mar 2026 10:20:41 -0700 Subject: [PATCH 4/8] Refactor internal attr funcs Scope generic type. Encapsulate the reflect functionality. --- attribute/internal/attribute.go | 34 ++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/attribute/internal/attribute.go b/attribute/internal/attribute.go index 4ab809d5384..d9f51fa2d7f 100644 --- a/attribute/internal/attribute.go +++ b/attribute/internal/attribute.go @@ -11,12 +11,20 @@ import ( "reflect" ) +// sliceElem is the exact set of element types stored in attribute slice values. +// Using a closed set prevents accidental instantiations for unsupported types. +type sliceElem interface { + bool | int64 | float64 | string +} + // SliceValue converts a slice into an array with the same elements. -func SliceValue[T any](v []T) any { - // Keep the common tiny-slice cases out of reflection. This matches the - // short lengths that show up most often in local benchmarks and semantic - // convention examples while leaving larger, less predictable slices on the - // generic reflective path. +func SliceValue[T sliceElem](v []T) any { + // Keep only the common tiny-slice cases out of reflection. Extending this + // much further increases code size for diminishing benefit while larger + // slices still need the generic reflective path to preserve comparability. + // This matches the short lengths that show up most often in local + // benchmarks and semantic convention examples while leaving larger, less + // predictable slices on the generic reflective path. switch len(v) { case 0: return [0]T{} @@ -28,13 +36,11 @@ func SliceValue[T any](v []T) any { return [3]T{v[0], v[1], v[2]} } - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[T]())).Elem() - reflect.Copy(cp, reflect.ValueOf(v)) - return cp.Interface() + return sliceValueReflect(v) } // AsSlice converts an array into a slice with the same elements. -func AsSlice[T any](v any) []T { +func AsSlice[T sliceElem](v any) []T { // Mirror the small fixed-array fast path used by SliceValue. switch a := v.(type) { case [0]T: @@ -47,6 +53,16 @@ func AsSlice[T any](v any) []T { return []T{a[0], a[1], a[2]} } + return asSliceReflect[T](v) +} + +func sliceValueReflect[T sliceElem](v []T) any { + cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[T]())).Elem() + reflect.Copy(cp, reflect.ValueOf(v)) + return cp.Interface() +} + +func asSliceReflect[T sliceElem](v any) []T { rv := reflect.ValueOf(v) if !rv.IsValid() || rv.Kind() != reflect.Array || rv.Type().Elem() != reflect.TypeFor[T]() { return nil From f3cbea7f14e9a22456ad0e89d7415a69dcfd04be Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Wed, 11 Mar 2026 10:28:51 -0700 Subject: [PATCH 5/8] Add a changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e3a1c0c7c9..71e76ee0230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add service detection with `WithService` in `go.opentelemetry.io/otel/sdk/resource`. (#7642) +### Changed + +- Refactor slice handling in `go.opentelemetry.io/otel/attribute` to optimize short slice values with fixed-size fast paths. (#8039) + From 310c7621cca71956f6154ba15405d4fcd6c9b7f9 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Wed, 11 Mar 2026 10:53:07 -0700 Subject: [PATCH 6/8] Update attribute/benchmark_test.go Fix BenchmarkStringSlice key name Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- attribute/benchmark_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attribute/benchmark_test.go b/attribute/benchmark_test.go index 34c7f0e754c..01b06fbbb50 100644 --- a/attribute/benchmark_test.go +++ b/attribute/benchmark_test.go @@ -296,7 +296,7 @@ func BenchmarkStringSlice(b *testing.B) { {name: "Len8", v: []string{"forty-two", "negative three", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen"}}, } { b.Run(bench.name, func(b *testing.B) { - k, v := "float64 slice", bench.v + k, v := "string slice", bench.v kv := attribute.StringSlice(k, v) b.Run("Value", func(b *testing.B) { From 2c652739947a5b7602ee00a99d6e2925b4bdb137 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Wed, 11 Mar 2026 10:59:44 -0700 Subject: [PATCH 7/8] Add a fast-path for IntSliceValue --- attribute/value.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/attribute/value.go b/attribute/value.go index c991ad22799..0d256f8336d 100644 --- a/attribute/value.go +++ b/attribute/value.go @@ -65,14 +65,28 @@ func IntValue(v int) Value { // IntSliceValue creates an INT64SLICE Value. func IntSliceValue(v []int) Value { - cp := make([]int64, len(v)) - for i, val := range v { - cp[i] = int64(val) - } - return Value{ - vtype: INT64SLICE, - slice: attribute.SliceValue(cp), + val := Value{vtype: INT64SLICE} + + // Avoid the common tiny-slice cases from allocating a new slice. + switch len(v) { + case 0: + val.slice = [0]int64{} + case 1: + val.slice = [1]int64{int64(v[0])} + case 2: + val.slice = [2]int64{int64(v[0]), int64(v[1])} + case 3: + val.slice = [3]int64{int64(v[0]), int64(v[1]), int64(v[2])} + default: + // Fallback to a new slice for larger slices. + cp := make([]int64, len(v)) + for i, val := range v { + cp[i] = int64(val) + } + val.slice = attribute.SliceValue(cp) } + + return val } // Int64Value creates an INT64 Value. From a4e08986ac60f8e68131d402034c8783db28d053 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Thu, 12 Mar 2026 09:07:58 -0700 Subject: [PATCH 8/8] Fix changelog --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5bde0c6d4a..18eb7103214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed - Introduce the `EMPTY` Type in `go.opentelemetry.io/otel/attribute` to reflect that an empty value is now a valid value, with `INVALID` remaining as a deprecated alias of `EMPTY`. (#8038) +- Refactor slice handling in `go.opentelemetry.io/otel/attribute` to optimize short slice values with fixed-size fast paths. (#8039) ### Deprecated @@ -27,10 +28,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Return spec-compliant `TraceIdRatioBased` description. This is a breaking behavioral change, but it is necessary to make the implementation [spec-compliant](https://opentelemetry.io/docs/specs/otel/trace/sdk/#traceidratiobased). (#8027) -### Changed - -- Refactor slice handling in `go.opentelemetry.io/otel/attribute` to optimize short slice values with fixed-size fast paths. (#8039) -