Skip to content

[SDK] Tracer::StartSpan benchmark and optimizations#4248

Open
dbarker wants to merge 21 commits into
open-telemetry:mainfrom
dbarker:perf_start_span_optimizations
Open

[SDK] Tracer::StartSpan benchmark and optimizations#4248
dbarker wants to merge 21 commits into
open-telemetry:mainfrom
dbarker:perf_start_span_optimizations

Conversation

@dbarker

@dbarker dbarker commented Jul 15, 2026

Copy link
Copy Markdown
Member

Fixes # (issue)
The Tracer::StartSpan function currently performs some unnecessary dynamic memory allocation (allocating a SpanContext and potentially a DefaultSpan) and copying of global static shared pointers by constructing temporary span contexts (which call TraceState::GetDefault()).

This latency can be removed without breaking changes. The added benchmark showed a latency reduction of ~11-23% for the range of span creation scenarios (tracer disabled, span with no parent, implicit parent, and explicit parent).

Changes

  • Adds a benchmark for Tracer::StartSpan. See baseline vs optimized benchmark results
  • Implements several optimizations to reduce dynamic memory allocation and shared_ptr copies (9746077)
    • Use the DefaultSpan as a non-recording span with a stack allocated SpanContext
    • Allocate one non recording span per tracer on construction to serve as the fallback noop span
    • Updates the SDK span to take a stack allocated SpanContext and the SamplingResult
    • Set the SamplingResult attributes in the SDK Span constructor to avoid locking the span mutex unecessarily
    • Changes the default value of the SpanStartOptions::parent to an empty Context instead of the more expensive SpanContext.
    • use std::make_shared to allocate the span when building with exceptions
    • clean up StartSpan to not instantiate and copy SpanContext if not needed.
    • add GetSpanContext helper to avoid dynamically allocating a DefaultSpan object to get an invalid SpanContext (trace::GetSpan is no longer called).
  • Update tracer test cases to check for a non-recording span using the Span::IsRecording method

For significant contributions please make sure you have completed the following items:

  • CHANGELOG.md updated for non-trivial changes
  • Unit tests have been added
  • Changes in public API reviewed

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.18919% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.12%. Comparing base (54f57f3) to head (b208179).

Files with missing lines Patch % Lines
sdk/src/trace/tracer.cc 87.10% 8 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4248      +/-   ##
==========================================
- Coverage   81.13%   81.12%   -0.00%     
==========================================
  Files         444      444              
  Lines       18848    18861      +13     
==========================================
+ Hits        15291    15300       +9     
- Misses       3557     3561       +4     
Files with missing lines Coverage Δ
api/include/opentelemetry/trace/context.h 95.66% <100.00%> (ø)
api/include/opentelemetry/trace/default_span.h 36.85% <ø> (+21.06%) ⬆️
sdk/include/opentelemetry/sdk/trace/sampler.h 100.00% <100.00%> (ø)
sdk/include/opentelemetry/sdk/trace/tracer.h 100.00% <ø> (ø)
sdk/src/trace/span.cc 93.75% <100.00%> (+0.18%) ⬆️
sdk/src/trace/span.h 100.00% <100.00%> (ø)
sdk/src/trace/tracer.cc 80.22% <87.10%> (-7.28%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…te the NonRecordingSpan. Fix iwyu warnings. Cleanup
Comment thread sdk/test/trace/tracer_benchmark.cc Outdated

// clang-format off
//
// ~/build/sdk/test/trace/tracer_benchmark --benchmark_repetitions=5 --benchmark_display_aggregates_only=true

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optimizations in this PR result in a 11-23% reduction in latency for StartSpan compared to main.

Benchmark Baseline (mean ± stddev) Optimized (mean ± stddev) Difference %
BM_StartSpanTracerDisabled 6.85 ± 0.025 ns 5.24 ± 0.046 ns -23.5%
BM_StartSpan 187 ± 1.22 ns 152 ± 2.74 ns -18.7%
BM_StartSpanWithScope 255 ± 1.09 ns 218 ± 1.75 ns -14.5%
BM_StartSpanWithImplicitParent 172 ± 0.45 ns 153 ± 0.80 ns -11.0%
BM_StartSpanWithExplicitParentContext 173 ± 0.34 ns 154 ± 0.90 ns -11.0%
BM_StartSpanWithExplicitRootContext 187 ± 0.68 ns 148 ± 0.69 ns -20.9%

Comment thread sdk/test/trace/sampler_benchmark.cc Outdated
// BM_TraceIdRatioBasedSamplerShouldSample_median 4.00 ns 4.00 ns 5
// BM_TraceIdRatioBasedSamplerShouldSample_stddev 0.032 ns 0.032 ns 5
// BM_TraceIdRatioBasedSamplerShouldSample_cv 0.80 % 0.80 % 5
// BM_SpanCreation_mean 205 ns 205 ns 5

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparison with main and this PR on span sampler benchmark. The NoopSpanCreation case is where the tracer is enabled but the sampler decides to not sample the span (AlwaysOffSampler).

Benchmark Baseline (mean ± stddev) Optimized (mean ± stddev) Difference %
BM_SpanCreation 235 ± 0.62 ns 205 ± 1.00 ns -12.8%
BM_NoopSpanCreation 59.3 ± 0.42 ns 27.7 ± 0.14 ns -53.3%

@dbarker
dbarker marked this pull request as ready for review July 15, 2026 16:52
@dbarker
dbarker requested a review from a team as a code owner July 15, 2026 16:52
inline SpanContext GetSpanContext(const context::Context &context) noexcept
{
if (!context.HasKey(kSpanKey))
const context::ContextValue context_value = context.GetValue(kSpanKey);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed context.HasKey call since it just calls context.GetValue(key) and checks for the nostd::monostate. The span key lookup and context value copy can be done once.

// parent of the currently active Span (if any) in the current context.
//
nostd::variant<SpanContext, context::Context> parent = SpanContext::GetInvalid();
nostd::variant<SpanContext, context::Context> parent = context::Context{};

@dbarker dbarker Jul 17, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For most callers the value of parent is never changed. For callers that set a parent the initial value is often default constructed then replaced. To keep this change ABI safe we don't add a nostd::monostate (which is really what we want) and instead construct the cheapest object by default. The SpanContext is a complex object with a shared_ptr<TraceState> that is initialized to a global TraceState default object. The full Context is simpler and only has a shared_ptr<DataList> member that is default initialized to null.

Comment thread sdk/src/trace/tracer.cc Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants