From c6752c35086a8117b4f410e9be1c819f2205f3dc Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Fri, 16 May 2025 13:51:24 -0700 Subject: [PATCH 1/4] trace: optimize id parsing and string functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With specialized routines, we can avoid the allocation of hex.DecodeString since we know the structure of the IDs. We can use `==` instead of bytes.Equal for arrays. From the Go [spec]: > Array types are comparable if their array element types are comparable. Two > array values are equal if their corresponding element values are equal. The > elements are compared in ascending index order, and comparison stops as soon > as two element values differ (or all elements have been compared). [spec]: https://go.dev/ref/spec#Comparison_operators To generate: ```sh mkdir private cd sdk go test -run=xxxxMatchNothingxxxx -bench=. -count=10 go.opentelemetry.io/otel/sdk/trace -timeout=30m | tee ../private/base.txt go test -run=xxxxMatchNothingxxxx -bench=. -count=10 go.opentelemetry.io/otel/sdk/trace -timeout=30m | tee ../private/new.txt benchstat ../private/base.txt ../private/new.txt ``` Results: ``` goos: darwin goarch: arm64 pkg: go.opentelemetry.io/otel/sdk/trace cpu: Apple M2 Max │ ../private/base.txt │ ../private/new.txt │ │ sec/op │ sec/op vs base │ Truncate/Unlimited-12 0.2086n ± 1% 0.2140n ± 3% +2.59% (p=0.017 n=10) Truncate/Zero-12 0.3048n ± 0% 0.3070n ± 1% +0.71% (p=0.014 n=10) Truncate/Short-12 0.2083n ± 2% 0.2097n ± 1% ~ (p=0.148 n=10) Truncate/ASCII-12 0.6870n ± 0% 0.6855n ± 0% ~ (p=0.493 n=10) Truncate/ValidUTF-8-12 1.298n ± 0% 1.302n ± 1% +0.31% (p=0.003 n=10) Truncate/InvalidUTF-8-12 9.457n ± 0% 9.420n ± 1% ~ (p=0.529 n=10) Truncate/MixedUTF-8-12 17.30n ± 1% 17.29n ± 0% ~ (p=0.359 n=10) RecordingSpanSetAttributes/WithLimit/false-12 2.055µ ± 1% 2.082µ ± 9% +1.29% (p=0.014 n=10) RecordingSpanSetAttributes/WithLimit/true-12 4.368µ ± 0% 4.364µ ± 0% -0.08% (p=0.049 n=10) SpanEnd-12 72.57n ± 17% 73.75n ± 16% ~ (p=0.853 n=10) TraceStart/with_a_simple_span-12 320.1n ± 10% 314.6n ± 10% ~ (p=0.165 n=10) TraceStart/with_several_links-12 432.7n ± 2% 429.4n ± 1% ~ (p=0.063 n=10) TraceStart/with_attributes-12 477.3n ± 1% 468.1n ± 6% -1.94% (p=0.005 n=10) SpanLimits/AttributeValueLengthLimit-12 4.401µ ± 1% 4.439µ ± 2% ~ (p=0.089 n=10) SpanLimits/AttributeCountLimit-12 4.125µ ± 1% 4.151µ ± 1% +0.62% (p=0.014 n=10) SpanLimits/EventCountLimit-12 3.900µ ± 2% 3.935µ ± 1% +0.88% (p=0.023 n=10) SpanLimits/LinkCountLimit-12 3.870µ ± 2% 3.901µ ± 1% ~ (p=0.148 n=10) SpanLimits/AttributePerEventCountLimit-12 4.212µ ± 1% 4.243µ ± 1% +0.75% (p=0.008 n=10) SpanLimits/AttributePerLinkCountLimit-12 4.200µ ± 1% 4.224µ ± 0% +0.57% (p=0.041 n=10) SpanSetAttributesOverCapacity-12 1.661µ ± 1% 1.653µ ± 0% -0.48% (p=0.049 n=10) StartEndSpan/AlwaysSample-12 317.9n ± 0% 316.5n ± 0% -0.44% (p=0.007 n=10) StartEndSpan/NeverSample-12 152.3n ± 0% 152.0n ± 0% -0.23% (p=0.005 n=10) SpanWithAttributes_4/AlwaysSample-12 527.2n ± 0% 532.4n ± 1% +1.00% (p=0.000 n=10) SpanWithAttributes_4/NeverSample-12 240.6n ± 0% 241.6n ± 0% +0.46% (p=0.000 n=10) SpanWithAttributes_8/AlwaysSample-12 704.5n ± 0% 718.3n ± 1% +1.97% (p=0.000 n=10) SpanWithAttributes_8/NeverSample-12 325.0n ± 0% 327.2n ± 1% +0.68% (p=0.000 n=10) SpanWithAttributes_all/AlwaysSample-12 576.8n ± 0% 584.7n ± 1% +1.37% (p=0.000 n=10) SpanWithAttributes_all/NeverSample-12 264.6n ± 1% 263.3n ± 0% -0.47% (p=0.045 n=10) SpanWithAttributes_all_2x/AlwaysSample-12 818.6n ± 1% 834.9n ± 0% +1.98% (p=0.000 n=10) SpanWithAttributes_all_2x/NeverSample-12 378.3n ± 0% 382.9n ± 1% +1.23% (p=0.000 n=10) SpanWithEvents_4/AlwaysSample-12 715.1n ± 1% 721.1n ± 0% +0.83% (p=0.003 n=10) SpanWithEvents_4/NeverSample-12 156.1n ± 1% 155.1n ± 1% -0.64% (p=0.002 n=10) SpanWithEvents_8/AlwaysSample-12 1.098µ ± 0% 1.104µ ± 0% +0.55% (p=0.000 n=10) SpanWithEvents_8/NeverSample-12 158.8n ± 0% 158.6n ± 1% ~ (p=0.288 n=10) SpanWithEvents_WithStackTrace/AlwaysSample-12 438.8n ± 0% 438.5n ± 0% ~ (p=0.868 n=10) SpanWithEvents_WithStackTrace/NeverSample-12 168.2n ± 1% 167.4n ± 1% -0.48% (p=0.014 n=10) SpanWithEvents_WithTimestamp/AlwaysSample-12 430.6n ± 0% 432.9n ± 0% +0.53% (p=0.001 n=10) SpanWithEvents_WithTimestamp/NeverSample-12 193.7n ± 0% 190.1n ± 1% -1.91% (p=0.000 n=10) TraceID_DotString-12 42.37n ± 0% 24.80n ± 0% -41.45% (p=0.000 n=10) SpanID_DotString-12 31.30n ± 0% 17.22n ± 0% -44.98% (p=0.000 n=10) SpanProcessorOnEnd/batch:_10,_spans:_10-12 163.3n ± 0% 163.4n ± 0% ~ (p=0.120 n=10) SpanProcessorOnEnd/batch:_10,_spans:_100-12 1.639µ ± 0% 1.635µ ± 0% -0.27% (p=0.000 n=10) SpanProcessorOnEnd/batch:_100,_spans:_10-12 163.3n ± 0% 163.2n ± 0% ~ (p=0.115 n=10) SpanProcessorOnEnd/batch:_100,_spans:_100-12 1.636µ ± 0% 1.635µ ± 1% ~ (p=0.509 n=10) SpanProcessorVerboseLogging-12 6.769µ ± 2% 6.600µ ± 2% -2.49% (p=0.030 n=10) geomean 221.4n 216.4n -2.29% │ ../private/base.txt │ ../private/new.txt │ │ B/op │ B/op vs base │ Truncate/Unlimited-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/Zero-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/Short-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/ASCII-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/ValidUTF-8-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/InvalidUTF-8-12 16.00 ± 0% 16.00 ± 0% ~ (p=1.000 n=10) ¹ Truncate/MixedUTF-8-12 32.00 ± 0% 32.00 ± 0% ~ (p=1.000 n=10) ¹ RecordingSpanSetAttributes/WithLimit/false-12 6.891Ki ± 0% 6.891Ki ± 0% ~ (p=1.000 n=10) ¹ RecordingSpanSetAttributes/WithLimit/true-12 7.023Ki ± 0% 7.023Ki ± 0% ~ (p=1.000 n=10) ¹ SpanEnd-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ TraceStart/with_a_simple_span-12 528.0 ± 0% 528.0 ± 0% ~ (p=1.000 n=10) ¹ TraceStart/with_several_links-12 704.0 ± 0% 704.0 ± 0% ~ (p=1.000 n=10) ¹ TraceStart/with_attributes-12 784.0 ± 0% 784.0 ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/AttributeValueLengthLimit-12 10.56Ki ± 0% 10.56Ki ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/AttributeCountLimit-12 9.844Ki ± 0% 9.844Ki ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/EventCountLimit-12 9.422Ki ± 0% 9.422Ki ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/LinkCountLimit-12 9.031Ki ± 0% 9.031Ki ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/AttributePerEventCountLimit-12 10.47Ki ± 0% 10.47Ki ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/AttributePerLinkCountLimit-12 10.47Ki ± 0% 10.47Ki ± 0% ~ (p=1.000 n=10) ¹ SpanSetAttributesOverCapacity-12 592.0 ± 0% 592.0 ± 0% ~ (p=1.000 n=10) ¹ StartEndSpan/AlwaysSample-12 528.0 ± 0% 528.0 ± 0% ~ (p=1.000 n=10) ¹ StartEndSpan/NeverSample-12 144.0 ± 0% 144.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_4/AlwaysSample-12 1.016Ki ± 0% 1.016Ki ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_4/NeverSample-12 400.0 ± 0% 400.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_8/AlwaysSample-12 1.516Ki ± 0% 1.516Ki ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_8/NeverSample-12 656.0 ± 0% 656.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_all/AlwaysSample-12 1.141Ki ± 0% 1.141Ki ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_all/NeverSample-12 464.0 ± 0% 464.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_all_2x/AlwaysSample-12 1.891Ki ± 0% 1.891Ki ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_all_2x/NeverSample-12 848.0 ± 0% 848.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_4/AlwaysSample-12 1.016Ki ± 0% 1.016Ki ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_4/NeverSample-12 144.0 ± 0% 144.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_8/AlwaysSample-12 1.641Ki ± 0% 1.641Ki ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_8/NeverSample-12 144.0 ± 0% 144.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_WithStackTrace/AlwaysSample-12 624.0 ± 0% 624.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_WithStackTrace/NeverSample-12 160.0 ± 0% 160.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_WithTimestamp/AlwaysSample-12 648.0 ± 0% 648.0 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_WithTimestamp/NeverSample-12 184.0 ± 0% 184.0 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorOnEnd/batch:_10,_spans:_10-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorOnEnd/batch:_10,_spans:_100-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorOnEnd/batch:_100,_spans:_10-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorOnEnd/batch:_100,_spans:_100-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorVerboseLogging-12 9.547Ki ± 0% 9.547Ki ± 0% ~ (p=1.000 n=10) ¹ geomean ² +0.00% ² ¹ all samples are equal ² summaries must be >0 to compute geomean │ ../private/base.txt │ ../private/new.txt │ │ allocs/op │ allocs/op vs base │ Truncate/Unlimited-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/Zero-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/Short-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/ASCII-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/ValidUTF-8-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/InvalidUTF-8-12 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=10) ¹ Truncate/MixedUTF-8-12 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=10) ¹ RecordingSpanSetAttributes/WithLimit/false-12 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ RecordingSpanSetAttributes/WithLimit/true-12 10.00 ± 0% 10.00 ± 0% ~ (p=1.000 n=10) ¹ SpanEnd-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ TraceStart/with_a_simple_span-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10) ¹ TraceStart/with_several_links-12 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ TraceStart/with_attributes-12 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/AttributeValueLengthLimit-12 41.00 ± 0% 41.00 ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/AttributeCountLimit-12 38.00 ± 0% 38.00 ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/EventCountLimit-12 35.00 ± 0% 35.00 ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/LinkCountLimit-12 35.00 ± 0% 35.00 ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/AttributePerEventCountLimit-12 38.00 ± 0% 38.00 ± 0% ~ (p=1.000 n=10) ¹ SpanLimits/AttributePerLinkCountLimit-12 38.00 ± 0% 38.00 ± 0% ~ (p=1.000 n=10) ¹ SpanSetAttributesOverCapacity-12 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ StartEndSpan/AlwaysSample-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10) ¹ StartEndSpan/NeverSample-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_4/AlwaysSample-12 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_4/NeverSample-12 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_8/AlwaysSample-12 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_8/NeverSample-12 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_all/AlwaysSample-12 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_all/NeverSample-12 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_all_2x/AlwaysSample-12 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithAttributes_all_2x/NeverSample-12 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_4/AlwaysSample-12 5.000 ± 0% 5.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_4/NeverSample-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_8/AlwaysSample-12 6.000 ± 0% 6.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_8/NeverSample-12 2.000 ± 0% 2.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_WithStackTrace/AlwaysSample-12 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_WithStackTrace/NeverSample-12 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_WithTimestamp/AlwaysSample-12 5.000 ± 0% 5.000 ± 0% ~ (p=1.000 n=10) ¹ SpanWithEvents_WithTimestamp/NeverSample-12 4.000 ± 0% 4.000 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorOnEnd/batch:_10,_spans:_10-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorOnEnd/batch:_10,_spans:_100-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorOnEnd/batch:_100,_spans:_10-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorOnEnd/batch:_100,_spans:_100-12 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹ SpanProcessorVerboseLogging-12 35.00 ± 0% 35.00 ± 0% ~ (p=1.000 n=10) ¹ geomean ² +0.00% ² ¹ all samples are equal ² summaries must be >0 to compute geomean ``` ``` goos: darwin goarch: arm64 pkg: go.opentelemetry.io/otel/sdk/trace cpu: Apple M2 Max │ ../private/base_hex.txt │ ../private/new_hex.txt │ │ sec/op │ sec/op vs base │ TraceIDFromHex-12 56.47n ± 0% 15.96n ± 0% -71.74% (p=0.000 n=10) SpanIDFromHex-12 34.680n ± 0% 8.742n ± 1% -74.79% (p=0.000 n=10) geomean 44.26n 11.81n -73.31% │ ../private/base_hex.txt │ ../private/new_hex.txt │ │ B/op │ B/op vs base │ TraceIDFromHex-12 16.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10) SpanIDFromHex-12 8.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10) geomean 11.31 ? ¹ ² ¹ summaries must be >0 to compute geomean ² ratios must be >0 to compute geomean │ ../private/base_hex.txt │ ../private/new_hex.txt │ │ allocs/op │ allocs/op vs base │ TraceIDFromHex-12 1.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10) SpanIDFromHex-12 1.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10) geomean 1.000 ? ¹ ² ¹ summaries must be >0 to compute geomean ² ratios must be >0 to compute geomean ``` Issue: #6721 --- sdk/trace/benchmark_test.go | 22 ++++++ trace/hex.go | 27 +++++++ trace/trace.go | 138 +++++++++++++++++++++++------------- 3 files changed, 139 insertions(+), 48 deletions(-) create mode 100644 trace/hex.go diff --git a/sdk/trace/benchmark_test.go b/sdk/trace/benchmark_test.go index efed513496c..6a538abf096 100644 --- a/sdk/trace/benchmark_test.go +++ b/sdk/trace/benchmark_test.go @@ -293,6 +293,28 @@ func BenchmarkSpanWithEvents_WithTimestamp(b *testing.B) { }) } +func BenchmarkTraceIDFromHex(b *testing.B) { + want := trace.TraceID{0xde, 0xad, 0xbe, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67} + b.ReportAllocs() + for i := 0; i < b.N; i++ { + got, _ := trace.TraceIDFromHex("deadbeef0123456789abcdef01234567") + if got != want { + b.Fatalf("got = %q want = %q", got.String(), want) + } + } +} + +func BenchmarkSpanIDFromHex(b *testing.B) { + want := trace.SpanID{0xde, 0xad, 0xbe, 0xef, 0x01, 0x23, 0x45, 0x67} + b.ReportAllocs() + for i := 0; i < b.N; i++ { + got, _ := trace.SpanIDFromHex("deadbeef01234567") + if got != want { + b.Fatalf("got = %q want = %q", got.String(), want) + } + } +} + func BenchmarkTraceID_DotString(b *testing.B) { t, _ := trace.TraceIDFromHex("0000000000000001000000000000002a") sc := trace.NewSpanContext(trace.SpanContextConfig{TraceID: t}) diff --git a/trace/hex.go b/trace/hex.go new file mode 100644 index 00000000000..a5980a38c74 --- /dev/null +++ b/trace/hex.go @@ -0,0 +1,27 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package trace // import "go.opentelemetry.io/otel/trace" + +const ( + // hexLU is a hex lookup table. + hexLU = "0123456789abcdef" + // hexRev is a reverse hex lookup table for lowercase hex digits. + hexRev = "" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\x0a\x0b\x0c\x0d\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +) diff --git a/trace/trace.go b/trace/trace.go index c8d9618b359..f1e1a6ea48f 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -4,8 +4,6 @@ package trace // import "go.opentelemetry.io/otel/trace" import ( - "bytes" - "encoding/hex" "encoding/json" ) @@ -41,18 +39,44 @@ var ( // IsValid reports whether the trace TraceID is valid. A valid trace ID does // not consist of zeros only. func (t TraceID) IsValid() bool { - return !bytes.Equal(t[:], nilTraceID[:]) + return t != nilTraceID } // MarshalJSON implements a custom marshal function to encode TraceID // as a hex string. func (t TraceID) MarshalJSON() ([]byte, error) { - return json.Marshal(t.String()) + b := [32 + 2]byte{0: '"', 33: '"'} + h := t.hexBytes() + copy(b[1:], h[:]) + return b[:], nil } // String returns the hex string representation form of a TraceID. func (t TraceID) String() string { - return hex.EncodeToString(t[:]) + h := t.hexBytes() + return string(h[:]) +} + +// hexBytes returns the hex string representation form of a TraceID. +func (t TraceID) hexBytes() [32]byte { + return [32]byte{ + hexLU[t[0x0]>>4], hexLU[t[0x0]&0xf], + hexLU[t[0x1]>>4], hexLU[t[0x1]&0xf], + hexLU[t[0x2]>>4], hexLU[t[0x2]&0xf], + hexLU[t[0x3]>>4], hexLU[t[0x3]&0xf], + hexLU[t[0x4]>>4], hexLU[t[0x4]&0xf], + hexLU[t[0x5]>>4], hexLU[t[0x5]&0xf], + hexLU[t[0x6]>>4], hexLU[t[0x6]&0xf], + hexLU[t[0x7]>>4], hexLU[t[0x7]&0xf], + hexLU[t[0x8]>>4], hexLU[t[0x8]&0xf], + hexLU[t[0x9]>>4], hexLU[t[0x9]&0xf], + hexLU[t[0xa]>>4], hexLU[t[0xa]&0xf], + hexLU[t[0xb]>>4], hexLU[t[0xb]&0xf], + hexLU[t[0xc]>>4], hexLU[t[0xc]&0xf], + hexLU[t[0xd]>>4], hexLU[t[0xd]&0xf], + hexLU[t[0xe]>>4], hexLU[t[0xe]&0xf], + hexLU[t[0xf]>>4], hexLU[t[0xf]&0xf], + } } // SpanID is a unique identity of a span in a trace. @@ -66,18 +90,35 @@ var ( // IsValid reports whether the SpanID is valid. A valid SpanID does not consist // of zeros only. func (s SpanID) IsValid() bool { - return !bytes.Equal(s[:], nilSpanID[:]) + return s != nilSpanID } // MarshalJSON implements a custom marshal function to encode SpanID // as a hex string. func (s SpanID) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) + b := [16 + 2]byte{0: '"', 17: '"'} + h := s.hexBytes() + copy(b[1:], h[:]) + return b[:], nil } // String returns the hex string representation form of a SpanID. func (s SpanID) String() string { - return hex.EncodeToString(s[:]) + b := s.hexBytes() + return string(b[:]) +} + +func (s SpanID) hexBytes() [16]byte { + return [16]byte{ + hexLU[s[0]>>4], hexLU[s[0]&0xf], + hexLU[s[1]>>4], hexLU[s[1]&0xf], + hexLU[s[2]>>4], hexLU[s[2]&0xf], + hexLU[s[3]>>4], hexLU[s[3]&0xf], + hexLU[s[4]>>4], hexLU[s[4]&0xf], + hexLU[s[5]>>4], hexLU[s[5]&0xf], + hexLU[s[6]>>4], hexLU[s[6]&0xf], + hexLU[s[7]>>4], hexLU[s[7]&0xf], + } } // TraceIDFromHex returns a TraceID from a hex string if it is compliant with @@ -85,59 +126,52 @@ func (s SpanID) String() string { // https://www.w3.org/TR/trace-context/#trace-id // nolint:revive // revive complains about stutter of `trace.TraceIDFromHex`. func TraceIDFromHex(h string) (TraceID, error) { - t := TraceID{} if len(h) != 32 { - return t, errInvalidTraceIDLength + return [16]byte{}, errInvalidTraceIDLength } - - if err := decodeHex(h, t[:]); err != nil { - return t, err + var b [16]byte + invalidMark := byte(0) + for i := 0; i < len(h); i += 4 { + b[i/2] = (hexRev[h[i]] << 4) | hexRev[h[i+1]] + b[i/2+1] = (hexRev[h[i+2]] << 4) | hexRev[h[i+3]] + invalidMark |= hexRev[h[i]] | hexRev[h[i+1]] | hexRev[h[i+2]] | hexRev[h[i+3]] } - - if !t.IsValid() { - return t, errNilTraceID + // If the upper 4 bits of any byte are not zero, there was an invalid hex + // character since invalid hex characters are 0xff in hexLU. + if invalidMark&0xf0 != 0 { + return [16]byte{}, errInvalidHexID + } + // If we didn't set any bits, then h was all zeros. + if invalidMark == 0 { + return [16]byte{}, errNilTraceID } - return t, nil + return b, nil } // SpanIDFromHex returns a SpanID from a hex string if it is compliant // with the w3c trace-context specification. // See more at https://www.w3.org/TR/trace-context/#parent-id func SpanIDFromHex(h string) (SpanID, error) { - s := SpanID{} if len(h) != 16 { - return s, errInvalidSpanIDLength - } - - if err := decodeHex(h, s[:]); err != nil { - return s, err + return [8]byte{}, errInvalidSpanIDLength } - - if !s.IsValid() { - return s, errNilSpanID + var b [8]byte + invalidMark := byte(0) + for i := 0; i < len(h); i += 4 { + b[i/2] = (hexRev[h[i]] << 4) | hexRev[h[i+1]] + b[i/2+1] = (hexRev[h[i+2]] << 4) | hexRev[h[i+3]] + invalidMark |= hexRev[h[i]] | hexRev[h[i+1]] | hexRev[h[i+2]] | hexRev[h[i+3]] } - return s, nil -} - -func decodeHex(h string, b []byte) error { - for _, r := range h { - switch { - case 'a' <= r && r <= 'f': - continue - case '0' <= r && r <= '9': - continue - default: - return errInvalidHexID - } + // If the upper 4 bits of any byte are not zero, there was an invalid hex + // character since invalid hex characters are 0xff in hexLU. + if invalidMark&0xf0 != 0 { + return [8]byte{}, errInvalidHexID } - - decoded, err := hex.DecodeString(h) - if err != nil { - return err + // If we didn't set any bits, then h was all zeros. + if invalidMark == 0 { + return [8]byte{}, errNilSpanID } - - copy(b, decoded) - return nil + return b, nil } // TraceFlags contains flags that can be set on a SpanContext. @@ -160,12 +194,20 @@ func (tf TraceFlags) WithSampled(sampled bool) TraceFlags { // nolint:revive // // MarshalJSON implements a custom marshal function to encode TraceFlags // as a hex string. func (tf TraceFlags) MarshalJSON() ([]byte, error) { - return json.Marshal(tf.String()) + b := [2 + 2]byte{0: '"', 3: '"'} + h := tf.hexBytes() + copy(b[1:], h[:]) + return b[:], nil } // String returns the hex string representation form of TraceFlags. func (tf TraceFlags) String() string { - return hex.EncodeToString([]byte{byte(tf)}) + h := tf.hexBytes() + return string(h[:]) +} + +func (tf TraceFlags) hexBytes() [2]byte { + return [2]byte{hexLU[tf>>4], hexLU[tf&0xf]} } // SpanContextConfig contains mutable fields usable for constructing From 222bc080c023c24430d4c11a8402bd32ae018cd5 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Aug 2025 14:48:01 +0100 Subject: [PATCH 2/4] CHANGELOG entry for faster hex conversions Signed-off-by: Bryan Boreham --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d1a4f91ef..0d10b41082c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Change `AssertEqual` in `go.opentelemetry.io/otel/log/logtest` to accept `TestingT` in order to support benchmarks and fuzz tests. (#6908) - Change `SDKProcessorLogQueueCapacity`, `SDKProcessorLogQueueSize`, `SDKProcessorSpanQueueSize`, and `SDKProcessorSpanQueueCapacity` in `go.opentelemetry.io/otel/semconv/v1.36.0/otelconv` to use a `Int64ObservableUpDownCounter`. (#7041) - Change `DefaultExemplarReservoirProviderSelector` in `go.opentelemetry.io/otel/sdk/metric` to use `runtime.GOMAXPROCS(0)` instead of `runtime.NumCPU()` for the `FixedSizeReservoirProvider` default size. (#7094) +- Faster conversions in `go.opentelemetry.io/otel/trace` between hex strings and `TraceID`, `SpanID`. (#7157) ### Fixed From a8319092dd79861538af4f128ee3ef9bd91c3544 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Thu, 7 Aug 2025 18:24:39 +0100 Subject: [PATCH 3/4] Refactor: extract hexToBin function Signed-off-by: Bryan Boreham --- trace/trace.go | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/trace/trace.go b/trace/trace.go index f1e1a6ea48f..a065a65dc14 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -130,6 +130,10 @@ func TraceIDFromHex(h string) (TraceID, error) { return [16]byte{}, errInvalidTraceIDLength } var b [16]byte + return b, hexToBin(h, b[:], errNilTraceID) +} + +func hexToBin(h string, b []byte, nilError error) error { invalidMark := byte(0) for i := 0; i < len(h); i += 4 { b[i/2] = (hexRev[h[i]] << 4) | hexRev[h[i+1]] @@ -139,13 +143,13 @@ func TraceIDFromHex(h string) (TraceID, error) { // If the upper 4 bits of any byte are not zero, there was an invalid hex // character since invalid hex characters are 0xff in hexLU. if invalidMark&0xf0 != 0 { - return [16]byte{}, errInvalidHexID + return errInvalidHexID } // If we didn't set any bits, then h was all zeros. if invalidMark == 0 { - return [16]byte{}, errNilTraceID + return nilError } - return b, nil + return nil } // SpanIDFromHex returns a SpanID from a hex string if it is compliant @@ -156,22 +160,7 @@ func SpanIDFromHex(h string) (SpanID, error) { return [8]byte{}, errInvalidSpanIDLength } var b [8]byte - invalidMark := byte(0) - for i := 0; i < len(h); i += 4 { - b[i/2] = (hexRev[h[i]] << 4) | hexRev[h[i+1]] - b[i/2+1] = (hexRev[h[i+2]] << 4) | hexRev[h[i+3]] - invalidMark |= hexRev[h[i]] | hexRev[h[i+1]] | hexRev[h[i+2]] | hexRev[h[i+3]] - } - // If the upper 4 bits of any byte are not zero, there was an invalid hex - // character since invalid hex characters are 0xff in hexLU. - if invalidMark&0xf0 != 0 { - return [8]byte{}, errInvalidHexID - } - // If we didn't set any bits, then h was all zeros. - if invalidMark == 0 { - return [8]byte{}, errNilSpanID - } - return b, nil + return b, hexToBin(h, b[:], errNilSpanID) } // TraceFlags contains flags that can be set on a SpanContext. From 1a3ce0f92327fb05003dc71e87057cc3c5c9cb48 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Aug 2025 10:17:19 +0100 Subject: [PATCH 4/4] Reduce repetition in binary to hex conversion This version runs ~5% slower. Signed-off-by: Bryan Boreham --- trace/trace.go | 58 ++++++++++++++++---------------------------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/trace/trace.go b/trace/trace.go index a065a65dc14..b7151106d4c 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -46,37 +46,17 @@ func (t TraceID) IsValid() bool { // as a hex string. func (t TraceID) MarshalJSON() ([]byte, error) { b := [32 + 2]byte{0: '"', 33: '"'} - h := t.hexBytes() - copy(b[1:], h[:]) + binTohex8(t[0:8], b[1:17]) + binTohex8(t[8:16], b[17:33]) return b[:], nil } // String returns the hex string representation form of a TraceID. func (t TraceID) String() string { - h := t.hexBytes() - return string(h[:]) -} - -// hexBytes returns the hex string representation form of a TraceID. -func (t TraceID) hexBytes() [32]byte { - return [32]byte{ - hexLU[t[0x0]>>4], hexLU[t[0x0]&0xf], - hexLU[t[0x1]>>4], hexLU[t[0x1]&0xf], - hexLU[t[0x2]>>4], hexLU[t[0x2]&0xf], - hexLU[t[0x3]>>4], hexLU[t[0x3]&0xf], - hexLU[t[0x4]>>4], hexLU[t[0x4]&0xf], - hexLU[t[0x5]>>4], hexLU[t[0x5]&0xf], - hexLU[t[0x6]>>4], hexLU[t[0x6]&0xf], - hexLU[t[0x7]>>4], hexLU[t[0x7]&0xf], - hexLU[t[0x8]>>4], hexLU[t[0x8]&0xf], - hexLU[t[0x9]>>4], hexLU[t[0x9]&0xf], - hexLU[t[0xa]>>4], hexLU[t[0xa]&0xf], - hexLU[t[0xb]>>4], hexLU[t[0xb]&0xf], - hexLU[t[0xc]>>4], hexLU[t[0xc]&0xf], - hexLU[t[0xd]>>4], hexLU[t[0xd]&0xf], - hexLU[t[0xe]>>4], hexLU[t[0xe]&0xf], - hexLU[t[0xf]>>4], hexLU[t[0xf]&0xf], - } + var b [32]byte + binTohex8(t[0:8], b[0:16]) + binTohex8(t[8:16], b[16:32]) + return string(b[:]) } // SpanID is a unique identity of a span in a trace. @@ -97,28 +77,26 @@ func (s SpanID) IsValid() bool { // as a hex string. func (s SpanID) MarshalJSON() ([]byte, error) { b := [16 + 2]byte{0: '"', 17: '"'} - h := s.hexBytes() - copy(b[1:], h[:]) + binTohex8(s[:], b[1:16]) return b[:], nil } // String returns the hex string representation form of a SpanID. func (s SpanID) String() string { - b := s.hexBytes() + var b [16]byte + binTohex8(s[:], b[:]) return string(b[:]) } -func (s SpanID) hexBytes() [16]byte { - return [16]byte{ - hexLU[s[0]>>4], hexLU[s[0]&0xf], - hexLU[s[1]>>4], hexLU[s[1]&0xf], - hexLU[s[2]>>4], hexLU[s[2]&0xf], - hexLU[s[3]>>4], hexLU[s[3]&0xf], - hexLU[s[4]>>4], hexLU[s[4]&0xf], - hexLU[s[5]>>4], hexLU[s[5]&0xf], - hexLU[s[6]>>4], hexLU[s[6]&0xf], - hexLU[s[7]>>4], hexLU[s[7]&0xf], - } +func binTohex8(s []byte, d []byte) { + d[0], d[1] = hexLU[s[0]>>4], hexLU[s[0]&0xf] + d[2], d[3] = hexLU[s[1]>>4], hexLU[s[1]&0xf] + d[4], d[5] = hexLU[s[2]>>4], hexLU[s[2]&0xf] + d[6], d[7] = hexLU[s[3]>>4], hexLU[s[3]&0xf] + d[8], d[9] = hexLU[s[4]>>4], hexLU[s[4]&0xf] + d[10], d[11] = hexLU[s[5]>>4], hexLU[s[5]&0xf] + d[12], d[13] = hexLU[s[6]>>4], hexLU[s[6]&0xf] + d[14], d[15] = hexLU[s[7]>>4], hexLU[s[7]&0xf] } // TraceIDFromHex returns a TraceID from a hex string if it is compliant with