Skip to content

Reserve each chunk's string bytes once - #1220

Open
quinnj wants to merge 2 commits into
mainfrom
claude/presize-string-bytes
Open

quinnj wants to merge 2 commits into
mainfrom
claude/presize-string-bytes

Conversation

@quinnj

@quinnj quinnj commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

String columns copy every cell longer than the 12-byte inline payload into a buffer the chunk owns. That buffer grew by one resize! per cell, reallocating as it went. With several tasks the reallocation copies and the page faults of each new block dominated the parse: in an 8-task profile of a text-heavy file, 65% of the chunk workers' samples sat on that resize! line.

At its first long cell, the loop now sums the field spans of the column's remaining long cells in the chunk (an upper bound on what it can copy, since content never exceeds its span) and reserves that once. Columns whose cells are all inline-length never run the pass. Values are unchanged; only buffer capacity changes. A side effect: finished columns no longer retain the growth slack.

strings.csv (163 MB, 1.2M rows, 10 text columns), interleaved process pairs on a loaded M3 Max, Julia 1.12.6:

main this PR
8 tasks, wall (min of 3 per process) 0.19-0.22 s 0.13-0.15 s
8 tasks, total CPU 0.62-0.71 s 0.36-0.61 s
1 task, peak RSS 959-994 MB 841 MB
retained string-buffer capacity / bytes 146 / 106 MiB 106 / 106 MiB

A mixed file whose text cells are mostly inline-length showed no difference beyond noise.

Review: my own pass, then an independent agent review (values, row coverage, copy bounds, sizehint! on 1.10/1.12, thread ownership, 40 cases on 1.10 and 1.12 compared with main), which found the over-reservation below.

  • Every string parse path (direct wave, stale re-parse, masked Scan pass, Chunks batches) goes through parsecolchunk!(::StringColumn, ...), and each passes a column whose buffer is private to the call. The transposed reader calls _own! on its own and is unchanged.
  • The reservation pass applies the loop's own row filters (mask, report limit) and counts the raw span of every long cell, which bounds all three copy paths: malformed quotes copy the raw span, escaped cells unescape to at most their content, plain cells copy their content.
  • Over-reservation: the pass counts every span longer than the inline payload, and some such cells copy nothing (a quoted 12-byte value, a short escaped value, a long sentinel, a padded cell under stripwhitespace). A second review found that the finished column kept that slack (one million quoted phone numbers retained 13.8 MB for 9,500 bytes; main 9,873). Fixed in the second commit: after the loop the buffer shrinks to what it holds when the slack exceeds half its size and 64 KiB. Same input now retains 9,500 bytes.
  • sizehint! on a fresh buffer only grows it, on Julia 1.10 and later.

Verification:

  • New testset DataString: owned bytes are reserved at their final size: (1) plain, quoted, and quote-escaped long cells over many chunks, serial and parallel: values, every byte owned once, and a capacity bound that main fails (328,601 bytes retained against a 208,728 bound); (2) quoted 12-byte and short escaped values with occasional long ones: a capacity bound that the first commit alone fails (1.40 MB and 1.50 MB against a 70 KB bound). Julia 1.10's summarysize ignores capacity, so the bounds only bind on 1.11+.
  • Full suite --check-bounds=yes -t4: 331,788 passed (head).
  • 300 seeds each of a ground-truth structural fuzzer and a cross-configuration consistency fuzzer (every reader, task count, and chunk size): no differences.

🤖 Generated with Claude Code

A string column copies every cell longer than the inline payload into a
buffer the chunk owns. That buffer grew one reallocation at a time, and
under parallel parsing the copies and the page faults of each new block
dominated: on a 163 MB file of text columns, 65% of the 8-task parse wave
sat on the resize. At its first long cell the loop now reserves the span
bytes of the column's remaining long cells (an upper bound on what it can
copy), so the buffer is allocated once. Columns of inline-length cells
pay nothing. The finished column also stops retaining growth slack.

strings.csv (163 MB, 1.2M rows x 10 text columns), interleaved runs:
8 tasks 0.20 -> 0.14 s, total CPU 0.62-0.71 -> 0.36-0.61 s; 1 task peak
RSS 959-994 -> 841 MB; retained string-buffer capacity 146 -> 106 MiB for
106 MiB of bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.81%. Comparing base (2f84f42) to head (93a52ab).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1220      +/-   ##
==========================================
+ Coverage   95.79%   95.81%   +0.01%     
==========================================
  Files           9        9              
  Lines        5566     5591      +25     
==========================================
+ Hits         5332     5357      +25     
  Misses        234      234              

☔ 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.

The reservation counts every cell span longer than the inline payload,
but a quoted 12-byte value, a short escaped value, a long sentinel, or a
padded cell under stripwhitespace copies nothing, and the finished
column kept the unused capacity: one million quoted phone numbers
retained 13.8 MB for 9,500 bytes. After the loop the buffer shrinks to
what it holds when the slack exceeds half its size and 64 KiB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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