Skip to content

Avoid quadratic Vec::remove(0) when splitting batches - #372

Merged
mheffner merged 1 commit into
rotel-dev:mainfrom
mheffner:perf-batch-split-drain
Oct 30, 2026
Merged

mheffner merged 1 commit into
rotel-dev:mainfrom
mheffner:perf-batch-split-drain

Conversation

@mheffner

Copy link
Copy Markdown
Member

When a request doesn't fit in the remainder of the current batch, BatchSplittable::split moved the first N spans (metrics, log records) into the new ScopeSpans one at a time with Vec::remove(0), shifting the rest of the vector on every call: O(N * len) memmove per split. With 1000-span requests and the default 8192 batch size this was 13% of SUT CPU in an OTLP->OTLP saturation profile (31% with the blackhole exporter), almost all of it in memmove.

Move the items with a single drain(..n) instead.

Benchmark (rotel-bench, Intel N95, SUT pinned to 1 core, closed loop, 1000 items/request, 5 runs per variant, ABBA, mean ± stddev):

traces OTLP gRPC -> OTLP gRPC (gzip) 277.5k±0.4k -> 307.4k±0.6k spans/s (+10.8%)
CPU/span -9.9%, p50 -10%, p99 -12%
traces, realistic span shape 156.1k±5.6k -> 167.4k±3.0k spans/s (+7.3%)
logs OTLP gRPC -> OTLP gRPC 287.2k±0.5k -> 310.8k±0.5k logs/s (+8.2%)
metrics (150 metrics x 7 points/req) unchanged (+0.3%)
traces -> blackhole, on top of the TCP_NODELAY change:
902.3k±1.5k -> 1294.3k±2.3k spans/s (+43%)
CPU/span -31%, p50 -31%, p99 -32%
Fixed-rate scenarios with 100-item requests: CPU/item -0.3..-1.2%,
latency and RSS unchanged.

When a request doesn't fit in the remainder of the current batch,
BatchSplittable::split moved the first N spans (metrics, log records)
into the new ScopeSpans one at a time with Vec::remove(0), shifting the
rest of the vector on every call: O(N * len) memmove per split. With
1000-span requests and the default 8192 batch size this was 13% of SUT
CPU in an OTLP->OTLP saturation profile (31% with the blackhole
exporter), almost all of it in memmove.

Move the items with a single drain(..n) instead.

Benchmark (rotel-bench, Intel N95, SUT pinned to 1 core, closed loop,
1000 items/request, 5 runs per variant, ABBA, mean ± stddev):

  traces OTLP gRPC -> OTLP gRPC (gzip)  277.5k±0.4k -> 307.4k±0.6k spans/s (+10.8%)
                                        CPU/span -9.9%, p50 -10%, p99 -12%
  traces, realistic span shape          156.1k±5.6k -> 167.4k±3.0k spans/s (+7.3%)
  logs OTLP gRPC -> OTLP gRPC           287.2k±0.5k -> 310.8k±0.5k logs/s (+8.2%)
  metrics (150 metrics x 7 points/req)  unchanged (+0.3%)
  traces -> blackhole, on top of the TCP_NODELAY change:
                                        902.3k±1.5k -> 1294.3k±2.3k spans/s (+43%)
                                        CPU/span -31%, p50 -31%, p99 -32%
  Fixed-rate scenarios with 100-item requests: CPU/item -0.3..-1.2%,
  latency and RSS unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mheffner
mheffner merged commit 5650680 into rotel-dev:main Oct 30, 2026
6 checks passed
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.

1 participant