Skip to content

perf(cpp): fill repeated TAG values in bulk - #1088

Open
ColinLeeo wants to merge 16 commits into
developfrom
perf/tag-column-bulk-fill
Open

ColinLeeo wants to merge 16 commits into
developfrom
perf/tag-column-bulk-fill

Conversation

@ColinLeeo

@ColinLeeo ColinLeeo commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A block's TAG (id) column repeats the same value on every row, but ColAppender::fill() appended it row by row — one add_row() plus one append() per row. On a table scan that reads the TAG column this showed up as ~10% of read time in a sampled profile (fill_ids -> ColAppender::fill -> append).

Change

  • Add Vector::fill_repeated(value, len, count), defaulting to the existing per-row loop, overridden by FixedLengthVector and VariableLengthVector.
  • Both overrides go through new ByteBuffer::fill_repeated_variable() / fill_repeated_fixed() helpers: write the value pattern once, then double the filled region, so the per-row work becomes one memcpy step instead of a full append.
  • ColAppender::fill() now calls fill_repeated() and keeps the same capacity/error checks.
  • Guard the size computation against uint64_t overflow before the capacity checks, and reject a zero element size.

Benchmark

3.2M-row full scan, single thread:

read
before 0.297–0.299 s
after 0.267–0.272 s

~10% faster with identical output.

Testing

Full C++ suite: 1535 tests passed.

The branch also carries test-only changes that make clock-derived .tsfile names unique per process (parallel CTest could otherwise collide). They are being landed separately in #1102; once that merges into develop, merging develop here collapses them and this PR's diff returns to the fill change plus its tests.

A block's TAG (id) column repeats the same value on every row, but
ColAppender::fill() appended it row by row: one add_row() plus one
append() per row. On a 3.2M-row table scan reading the TAG column this
showed up as ~10% of read time in a sampled profile.

Add Vector::fill_repeated() (defaulting to a per-row loop) with overrides
for the fixed-length and variable-length vectors. Both build the value
pattern once and then double the filled region, so the per-row work
becomes a single memcpy copy step instead of a full append.

3.2M-row full scan: 0.298s -> 0.269s (~10%) with unchanged output.
Exercise the fixed-length and variable-length overrides directly: value
and length correctness after multiple fills, row counters, capacity
overflow rejection, and the zero-count no-op.
@codecov-commenter

codecov-commenter commented Nov 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 15.78947% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.89%. Comparing base (aa7ed1e) to head (7db18a8).

Files with missing lines Patch % Lines
cpp/src/common/container/byte_buffer.h 0.00% 37 Missing ⚠️
cpp/src/common/tsblock/vector/vector.h 0.00% 4 Missing ⚠️
...pp/src/common/tsblock/vector/fixed_length_vector.h 40.00% 1 Missing and 2 partials ⚠️
...src/common/tsblock/vector/variable_length_vector.h 40.00% 2 Missing and 1 partial ⚠️
cpp/src/common/tsblock/tsblock.h 83.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1088      +/-   ##
===========================================
- Coverage    66.94%   66.89%   -0.06%     
===========================================
  Files          788      788              
  Lines        57573    57630      +57     
  Branches      9304     9310       +6     
===========================================
+ Hits         38542    38551       +9     
- Misses       17355    17400      +45     
- Partials      1676     1679       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Review follow-ups on the new fill path:

* Both ByteBuffer fill helpers wrote the first element before looking at
  count, so a count == 0 call wrote into the slack after the data — or
  past the end of an exactly-full buffer.  Return early instead.
* FixedLengthVector::fill_repeated used the caller's len as the element
  stride while every read path advances by type_len_.  Use type_len_ and
  assert the caller agrees.
* ColAppender::fill reported every failure (including E_OOM from a failed
  growth) as E_INVALID_ARG; propagate the actual code.

Tests: zero-count at the ByteBuffer level (fails without the guard) and
a variable-length fill larger than the per-row reservation, which
exercises the grow-and-copy path.
@ColinLeeo
ColinLeeo requested a balanced review from Copilot November 12, 2026 15:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

3 open issues
What changed in this PR

This PR optimizes filling repeated TAG/id column values by adding bulk “fill repeated” APIs at the Vector/ByteBuffer layers and updating ColAppender to use them, reducing per-row append overhead.

Changes:

  • Added Vector::fill_repeated() with specialized overrides in fixed/variable-length vectors.
  • Added ByteBuffer::fill_repeated_variable() / fill_repeated_fixed() using a doubling memcpy strategy.
  • Updated ColAppender::fill() to use bulk fill and added targeted regression tests.
File Description
cpp/​test/​common/​tsblock/​tslock_test.cc Adds tests covering repeated fill for fixed/variable vectors, overflow handling, grow path, and zero-count no-op.
cpp/​src/​common/​tsblock/​vector/​vector.h Introduces Vector::fill_repeated() default implementation.
cpp/​src/​common/​tsblock/​vector/​variable_length_vector.h Overrides fill_repeated() to use ByteBuffer::fill_repeated_variable().
cpp/​src/​common/​tsblock/​vector/​fixed_length_vector.h Overrides fill_repeated() to use ByteBuffer::fill_repeated_fixed().
cpp/​src/​common/​tsblock/​tsblock.h Switches ColAppender::fill() to call vec_->fill_repeated() and handles row count/capacity checks.
cpp/​src/​common/​container/​byte_buffer.h Adds bulk repeated-fill helpers implementing doubling-copy strategy.

Review effort: Lite

Comment thread cpp/src/common/container/byte_buffer.h Resolved
Comment thread cpp/test/common/tsblock/tslock_test.cc Resolved
Return E_INVALID_ARG when the caller's len differs from type_len_
instead of relying on ASSERT, which is compiled out in release and
would make the fill read past a shorter value buffer. Also include
<cstring> for std::memcpy and copy test values into aligned locals
instead of reinterpret_cast'ing the row buffer.
@ColinLeeo
ColinLeeo requested a balanced review from Copilot November 12, 2026 16:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread cpp/src/common/container/byte_buffer.h Resolved
Comment thread cpp/src/common/container/byte_buffer.h Resolved
Comment thread cpp/test/common/tsblock/tslock_test.cc Resolved
Comment thread cpp/test/common/tsblock/tslock_test.cc Resolved
Comment thread cpp/test/common/tsblock/tslock_test.cc Resolved
Return E_INVALID_ARG if variable_type_len_ ever exceeds sizeof(uint32_t)
before the length prefix is copied from a uint32_t (it is fixed to
sizeof(uint32_t) today, but make the invariant explicit). Also copy the
int32_t test value out with memcpy instead of reinterpret_cast'ing the
row buffer, which is what <cstring> was added for.
Same isolation fix as the writer tests: parallel CTest processes can seed
the name generator in the same clock tick, and one then removes the file
another is opening (E_ALREADY_EXIST at the fixture's open, observed on
the macOS 3.11 and py-msvc 3.11 jobs). Append pid and a per-process
counter.
develop added set_data_size() (direct-decode support) where this branch
adds the fill_repeated_* helpers; keep both.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread cpp/src/common/container/byte_buffer.h Resolved
elem_size * count multiplies two 32-bit values, so with len near
UINT32_MAX and a large count the product wraps uint64_t, need lands
below the capacity check and a ~4 GiB copy runs into the small buffer.
Bound the count by the remaining 32-bit capacity first and return
E_OOM; the fixed-width helper gets the same check. Test reproduces the
wrapping case (crashes without the guard).
Same parallel-CTest collision as the reader tests: two processes can seed
from the same clock tick and one removes the file the other opens
(E_FILE_OPEN_ERR), observed on the macOS Release job.
Several fixtures build their .tsfile name from a clock count alone.  CTest
runs cases in parallel processes, so two can read the same tick and then
one removes the file the other is opening or reopening
(E_ALREADY_EXIST / E_FILE_OPEN_ERR, seen as SEGFAULT on macOS).

Append the process id and a per-process counter, the pattern the writer
tests already use.  Covers table_read_failure, table_field_filter,
chunk_reader_resource, aligned_chunk_reader_bulk,
aligned_field_filter_regression, masked_bulk_copy and
timeseries_schema_codec.
… perf/tag-column-bulk-fill

# Conflicts:
#	cpp/test/reader/table_field_filter_test.cc
@ColinLeeo
ColinLeeo requested a balanced review from Copilot November 13, 2026 10:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread cpp/src/common/container/byte_buffer.h Resolved
getpid()/_getpid() are used in the fixtures unconditionally, but the
process.h/unistd.h block landed inside the ENABLE_THREADS guard in
aligned_chunk_reader_bulk_test and chunk_reader_resource_test, which
would break a build with threads disabled.
Reject elem_size == 0 before dividing, and let the variable-length
vector return the underlying error code (E_INVALID_ARG vs E_OOM) instead
of always reporting E_OOM.
Resolve the table field filter test conflict using the unique-file-name changes already merged in develop.
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