Skip to content

Add newline_delimited, slice_at, and skip documents by delimiter - #2803

Open
lemire wants to merge 2 commits into
masterfrom
faster-ndjson-stream
Open

Add newline_delimited, slice_at, and skip documents by delimiter#2803
lemire wants to merge 2 commits into
masterfrom
faster-ndjson-stream

Conversation

@lemire

@lemire lemire commented Jul 31, 2026

Copy link
Copy Markdown
Member

next_document() reaches the next document with skip_child(0), which loads the input byte behind every remaining structural and branches on it. When the caller reads only part of each document, that walk covers the rest of it.

For a delimiter-framed stream the document ends at the next delimiter, so memchr plus a galloping search of the structural index gets there without touching the structurals in between. This applies to json_sequence and to a new stream_format::newline_delimited, where the caller guarantees one document per line. whitespace_delimited cannot use it: it permits several documents on one line and line feeds inside a document, so a line feed is a document boundary but not necessarily the next one.

The shortcut is gated on depth() > 0, which means the caller left the document part-read, so there is a walk worth avoiding, and the iterator is still inside the document, so the next delimiter terminates it.

simdjson::slice_at cuts a padded_string_view into document-aligned slices. Documents are independent, so callers can parse the slices on as many threads as they like instead of relying on the built-in stage-1 thread, which caps out near a factor of two. doc/iterate_many.md shows the pattern.

document_stream also allocated a stage1_worker, which holds a thread, a mutex and a condition variable, on every construction even with threading off. It is now allocated only when a stage-1 thread is started.

Benchmarks

benchmark/bench_stream_formats.cpp now runs whitespace_delimited and newline_delimited side by side, plus a bench_sliced variant using slice_at across threads. On a 64-core Xeon Gold 6548N, small-document NDJSON:

configuration GiB/s
single thread 2.42
built-in stage-1 thread (2 threads) 3.35
slice_at, 2 threads 4.40
slice_at, 4 threads 8.02
slice_at, 8 threads 14.36

Testing

131/131 tests pass on x86-64 (Xeon Gold 6548N, GCC 14) and 118/118 on arm64 (Apple Silicon, clang). New tests cover partial and full document reads, blank lines and CRLF, scalar documents, agreement with whitespace_delimited across batch sizes, and slice_at reassembling its input exactly for a range of block sizes.

Notes

document_stream now befriends token_iterator to reach peek() and set_position(). Narrow accessors may be preferable.

slice_at returns an empty view when a block falls entirely inside one document, which happens only if that document is longer than block_size. Callers should iterate while index * block_size < size and skip empty slices rather than stopping at the first one.

lemire added 2 commits July 30, 2026 21:27
next_document() reaches the next document with skip_child(0), which loads
the input byte behind every remaining structural and branches on it. When
the caller reads only part of each document, that walk covers the rest of
it. For a delimiter-framed stream the document ends at the next delimiter,
so memchr plus a galloping search of the structural index gets there
without touching the structurals in between. This applies to json_sequence
and to a new stream_format::newline_delimited, where the caller guarantees
one document per line. whitespace_delimited cannot use it: it permits
several documents on one line and line feeds inside a document.

The shortcut is gated on depth() > 0, which means the caller left the
document part-read, so there is a walk worth avoiding, and the iterator is
still inside the document, so the next delimiter terminates it.

simdjson::slice_at cuts a padded_string_view into document-aligned slices.
Documents are independent, so callers can parse the slices on as many
threads as they like instead of relying on the built-in stage-1 thread,
which caps out near a factor of two. doc/iterate_many.md shows the pattern.

document_stream also allocated a stage1_worker, which holds a thread, a
mutex and a condition variable, on every construction even with threading
off. It is now allocated only when a stage-1 thread is started.
Move <cstring> outside SIMDJSON_CONDITIONAL_INCLUDE and use
std::memchr so amalgamation always sees the declaration. Document
stream_format::newline_delimited in iterate_many/parse_many docs and
note that delimiter jumps skip structure-validation of unread
document remainders under the one-line/RS contract.
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