GH-50481: [C++] Fix CSV reader mis-parsing rows with an embedded NUL byte#50483
GH-50481: [C++] Fix CSV reader mis-parsing rows with an embedded NUL byte#50483HannaWeissberg wants to merge 4 commits into
Conversation
…d NUL byte
SSE42Filter::Matches used _mm_cmpistrc, an implicit-length SSE4.2
string-compare intrinsic that treats 0x00 as a terminator. Since it
scans 8 raw CSV bytes at a time, a NUL byte embedded in a field could
hide a real delimiter/quote/newline sharing the same 8-byte word,
causing RunBulkFilter to blindly bulk-copy the word and silently
mis-split the row.
Switch to the explicit-length _mm_cmpestrc, passing the true lengths
of both operands (sizeof(WordType) for the data word, sizeof(BulkFilterType)
for the filter) instead of relying on NUL-termination.
Verified locally: reverting just this fix reproduces the exact
predicted failure ("Expected 64 columns, got 1") in the added
regression test; with the fix, that test and the rest of the CSV
test suite (272 tests) pass.
This fix and its regression test were AI-generated (Claude), under
human review and local verification.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
There was a problem hiding this comment.
Pull request overview
Fixes an SSE4.2-specific CSV parsing correctness bug where embedded NUL (0x00) bytes could cause the SIMD bulk-filter path to miss structural characters (quote/comma/newline), leading to silent row mis-splitting once bulk filtering activates.
Changes:
- Switch
SSE42Filter::Matchesfrom implicit-length_mm_cmpistrcto explicit-length_mm_cmpestrcto avoid NUL-termination behavior. - Add a regression test that forces bulk-filter activation and then parses a quoted field containing an embedded NUL right before the closing quote.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| cpp/src/arrow/csv/lexing_internal.h | Updates SSE4.2 bulk-filter matching to use explicit-length string-compare semantics. |
| cpp/src/arrow/csv/parser_test.cc | Adds a regression test covering embedded-NUL behavior after bulk filtering activates. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
@AntoinePrv I've added you as a reviewer in case you can take a look |
|
CI failure appears to be unrelated to this PR, could a committer check/re-run it please? |
| // same 8-byte SIMD word, causing the parser to keep consuming subsequent | ||
| // bytes as if still inside the quoted field. | ||
| constexpr int32_t num_cols = 64; | ||
| constexpr int32_t num_filler_rows = 512; // = kTargetChunkSize / num_cols |
There was a problem hiding this comment.
Instead of having filler rows with no NUL bytes, this test would be more robust by putting NUL bytes in every cell value, IMHO.
| // Look up every byte in `w` in the SIMD filter. Use the explicit-length | ||
| // comparison since `w` may contain an embedded NUL byte, which the | ||
| // implicit-length _mm_cmpistrc would otherwise treat as a terminator. | ||
| return _mm_cmpestrc(_mm_set1_epi64x(w), static_cast<int>(sizeof(WordType)), filter_, |
There was a problem hiding this comment.
The explicit-length instructions are slower according to various source out there, we should run some benchmarks to see if there are significant regressions.
|
@ursabot please benchmark lang=C++ |
|
Benchmark runs are scheduled for commit 92cb521. Watch https://buildkite.com/apache-arrow and https://conbench.arrow-dev.org for updates. A comment will be posted here when the runs are complete. |
|
I see massive regressions locally on an AMD Zen 2 CPU: |
|
To avoid the regression, we could arrange to run the bulk filter only if the entire block does not contain a NUL byte (which can be checked extremely quickly using NUL bytes being exceptional in CSV files, common cases would not see a slowdown. (of course, this would be only for SSE4.2, not NEON) |
Rationale for this change
Fixes #50481.
arrow::csv::TableReader/BlockParsercan silently mis-split a row when a text field contains an embedded NUL (0x00) byte, once the reader has processed enough data to switch into its SIMD "bulk filter" scanning path.What changes are included in this PR?
SSE42Filter::Matches(cpp/src/arrow/csv/lexing_internal.h) used_mm_cmpistrc, an implicit-length SSE4.2 string-compare intrinsic that treats0x00as a terminator in both operands. Since the caller feeds it 8 raw CSV bytes at a time, a real quote/comma/newline sharing an 8-byte word with an embedded NUL becomes invisible to the filter —RunBulkFilterthen trusts the filter's "no special chars" answer and bulk-copies the whole word, silently swallowing the structural character.This switches to the explicit-length
_mm_cmpestrc, passing the true length of each operand (sizeof(WordType)for the 8-byte data word,sizeof(BulkFilterType)for the 16-byte filter — derived from the type rather than hardcoded, so it stays correct if this class is ever widened to a larger SIMD register) instead of relying on NUL-termination.Also adds a regression test (
BlockParser.EmbeddedNulInQuotedFieldAfterBulkFilterActivatesinparser_test.cc) that reproduces the exact trigger: enough filler rows to cross the bulk filter's average-bytes-per-value activation threshold, followed by a row whose first field has a real embedded NUL immediately before its closing quote.Are these changes tested?
Yes. Verified locally (not just by inspection):
Expected 64 columns, got 1: "abc\0def",...and a wrong row count.arrow-csv-testsuite (272 tests, 47 suites — parser, chunker, converter, reader, writer, column_builder, column_decoder) passes with the fix, with no regressions.Are there any user-facing changes?
No API changes. This only affects internal CSV byte-scanning correctness on SSE4.2-capable x86 builds; behavior for data without embedded NUL bytes is unchanged (verified via a direct intrinsic-level comparison of both instructions against the actual filter construction, with and without a NUL present).
This PR (fix, test, and description) was AI-generated (Claude), under human review and local verification described above.