Plan ID: plan-243af5b1
Status: reviewing
Created: 2026-01-25
Updated: 2026-01-25
Use profiling insights from a TPCH-like analytical workload to reduce end-to-end query execution time by targeting a dominant hotspot in the string read path.
Primary objective: remove atomic/refcount churn on the hot path of per-row string access by switching to a zero-copy, pointer-cached access strategy, validated with unit tests + sanitizers, and verified via perf/flamegraph deltas.
cpp/nd/string_array_holder.hppcpp/nd/string_array_holder.cpppg_deeplake/build/deeplake/cpp/deeplake_pg/duckdb_deeplake_scan.cpppg_deeplake/build/deeplake/cpp/deeplake_pg/table_data.hpppg_deeplake/build/deeplake/cpp/deeplake_pg/table_data_impl.hppcpp/chunk_format/impl/chunk_array.hppcpp/CMakeLists.txtcpp/CMakeLists.tests.cmakefactory/docker/benchmark.py
Profiling indicates a large portion of runtime is spent in atomic operations related to reference counting / shared ownership during repeated string extraction. In the baseline run, the top hotspot was an atomic refcount increment on ARM (acquire/release) and a secondary hotspot was a reference-count base class atomic path.
Interpretation: a per-access helper function returns a string view by passing through a shared-ownership buffer wrapper. Even if the returned view is “zero-copy”, the wrapper copy triggers atomic refcount operations each time, which dominates runtime when executed per element.
Key insight: we can preserve zero-copy semantics while eliminating atomic refcount overhead by extracting and caching raw backing pointers once per batch/segment initialization, then using those cached pointers in subsequent string reads.
In scope
- Removing refcount-related atomics for the project’s string array “holder/streamer” component.
- Adding minimal unit tests to prevent regressions.
- Sanitizer validation for lifetime and bounds safety.
- Perf validation that the refcount atomic hotspot is eliminated.
Not in scope (for this iteration)
- Optimizing downstream query engine internals (e.g., engine-owned buffers, vector caches, destructors).
- Achieving a guaranteed 2× reduction in end-to-end TPCH time (may be bounded by other engine overhead).
We have a String Holder abstraction that serves string values to a scan/operator layer. It supports two underlying layouts:
- Range/offset-backed strings: strings stored in a contiguous buffer with an offset table.
- Dynamic strings: strings stored as variable chunks/arrays, accessed through an indirection layer.
Baseline hot path characteristics:
- Per-element
get_string(i)calls cause a shared-ownership wrapper to be copied. - That copy triggers atomic refcount operations at very high frequency.
Optimized hot path strategy:
- During holder initialization (per batch/segment), extract:
- pointer to raw data buffer
- pointer to offsets buffer (if applicable)
- base offset/stride info (if applicable)
- Store these as a read-only cache inside the holder.
- For dynamic strings, maintain a cache of
(char*, length)pairs (or equivalent) for zero-copy views. - Ensure a safe fallback path exists for unsupported adapters/layouts.
Assumption (based on internal prototype): a pointer-caching implementation exists and needs verification, tests, comments, and safety validation.
Work
- Add a minimal unit test suite for the String Holder API (range + dynamic).
- Run a profiling sanity check to confirm refcount/atomic samples collapse to near-zero.
- Validate initialization validity logic (no false-invalid states).
- Document that the “fast” sequential access path also covers dynamic strings (even if not inlined).
- Run ASAN/UBSAN to validate lifetime/bounds safety.
- Commit with success criteria focused on eliminating the holder-level atomic hotspot.
Complexity: low
Risk: low-to-medium (lifetime safety must be proven)
If the scan layer accesses strings sequentially within a batch, route reads through a get_string_fast(i) API that avoids per-access searching (e.g., binary search on ranges).
Complexity: low
Risk: low
Expected impact: marginal unless search overhead is visible after atomic removal
After atomics are removed, check whether offset computation (not memory access) dominates. If yes, implement vectorized offset decoding (NEON/AVX).
Complexity: medium
Risk: low
Expected impact: only meaningful if offset compute shows up prominently in profiling
Select A (Verify + test + validate pointer-caching optimization)
Rationale:
- The baseline profile shows refcount/atomic operations dominate the string holder’s hot path.
- Caching raw pointers once during initialization avoids repeated shared-ownership wrapper copies.
- This targets the dominant overhead with minimal code change and is testable and measurable.
Create a focused test file covering:
- Range/offset-backed strings:
- correctness of
get_string(i) - correctness of
get_string_fast(i)for sequential access - correctness of
get_batch(start, count)style API
- correctness of
- Dynamic strings:
- correctness of zero-copy views (pointer/length stable within owner lifetime)
- fallback path correctness if caching is not applicable
Test cases
- Small array with known strings (including empty string and long string).
- A batch boundary case (transition between batches/segments).
- Dynamic strings with varying lengths.
Pass criteria
- Results match expected values.
- Fast and slow APIs return identical content for sequential access patterns.
- Batch API populates output buffers correctly.
Run the benchmark workload with perf sampling and generate a collapsed stack trace / flamegraph.
Check
- Search for the baseline atomic/refcount symbol(s) dominating the hot path.
- Expect them to drop from “dominant” to “near-zero.”
Pass criteria
- Atomic/refcount operations no longer appear as a significant fraction of samples in the string holder read path.
- The string holder read stack is now dominated by memory access / offset decode rather than refcount churn.
(Public note: exact symbol names vary by platform and toolchain; we validate by comparing before/after flamegraphs.)
Review the initialization flow to ensure:
- successful initialization sets
is_valid = true - fallback initialization also sets
is_valid = truewhen it is intended to be usable - failure paths clear caches and do not leave partially initialized pointers
Pass criteria
- No successful initialization leaves
is_validfalse. - Fallback is selected only when caching is not applicable.
If get_string_fast(i) uses a generic helper for dynamic strings (not inlined), add a comment explaining:
- range strings: served directly from cached buffer + offsets
- dynamic strings: served via cached
(ptr, len)entries (or equivalent) even if the code path looks indirect
Pass criteria
- Comment accurately describes behavior.
- No logic change required.
Because we cache raw pointers, lifetime must be correct:
- backing storage must outlive the holder
- no out-of-bounds reads
- no use-after-free
Build
- Debug + ASAN (and optionally UBSAN)
Run
- new unit tests (string holder tests)
- existing unit tests (at least a smoke run)
Pass criteria
- No sanitizer reports.
Commit message should include:
- what was slow (refcount atomics in per-element string reads)
- why (shared-ownership wrapper copied per access)
- what changed (cached raw pointers / cached dynamic string views during initialization)
- what it affects (holder-level overhead)
- what it does not affect (downstream engine overhead)
Run the benchmark at the same scale/config as baseline (warmup + multiple measurements), generate flamegraph, and compare:
Primary metric
- Holder-level atomic/refcount hotspot: “dominant” → “near-zero”
Secondary metrics
- Query time improvement (expect improvement, but not necessarily 2×)
- If overall speedup is limited, identify next hotspots (often downstream engine internals)
Review scan/operator integration to ensure holder lifetime is safe:
- holder does not outlive the backing array/buffer owner
- batch/segment transitions correctly refresh caches (if applicable)
Pass criteria
- No cached pointer used after backing owner is destroyed.
- Batch switching calls cache reset/refresh where required.
Only do this if profiling shows meaningful time in search/dispatch overhead after atomics are removed.
Change
- Replace per-element calls with
get_string_fast(i)within a sequential batch loop. - Ensure cache reset occurs at batch boundaries.
Pass criteria
- Correctness maintained.
- Profiling shows reduced search overhead (if it was visible).
Mitigation
- Sanitizers (ASAN/UBSAN)
- Integration lifetime review
- Unit tests covering batch boundary transitions
Mitigation
- Keep a correct fallback path
- Add tests for fallback behavior
Mitigation
- Define success as elimination of holder-level atomic hotspot
- Use post-opt profiling to decide next targets
REQUIRED
- The baseline refcount/atomic hotspot in the string holder read path is reduced to near-zero in perf/flamegraphs.
- New unit tests for range + dynamic strings pass.
- Sanitizers report no errors.
- No regressions in existing test suite.
EXPECTED
- Some measurable improvement in benchmark runtime, but magnitude may be bounded by downstream components.
NICE-TO-HAVE
- If a new dominant hotspot emerges (e.g., engine buffer lifecycle), document it as the next optimization candidate.
Replace these with your project’s exact build and benchmark commands.
- Build tests
cmake -S . -B build -DENABLE_TESTS=ON
cmake --build build -j
ctest --test-dir build- Sanitizer build
cmake -S . -B build-asan -DENABLE_TESTS=ON -DENABLE_ASAN=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build-asan -j
ctest --test-dir build-asan- Perf profile baseline vs optimized
# Baseline
./run_benchmark --profile --warmup 2 --runs 30 --out baseline_profile
# Optimized
./run_benchmark --profile --warmup 2 --runs 30 --out optimized_profile
# Compare flamegraphs / collapsed stacks
# (Exact tools vary; the goal is “atomic/refcount hotspot disappears”.)