Rewrite CSV.jl internals for 1.0 - #1196
Merged
Merged
Conversation
Fold each task-local problem log into one globally capped reservoir as soon as the task completes. Keep chunk tags for final row rebasing, preserve deterministic source order and the exact dropped count, and release local retained entries. Co-Authored-By: Codex <codex@openai.com>
Describe the fused probe and reparse flow, the inline-or-view KStr layout, exact segment sizing, and the allocating hash implementation accurately. Refresh the test-count and benchmark wording. Co-Authored-By: Codex <codex@openai.com>
Tape emission (flat offsets, hygiene deferred): - scanners now emit ONE UInt32 per structural event -- (relpos << 2) | kind -- and nothing else; CRLF pairing, comment/empty-row hygiene, row boundaries, and row-start offsets all move to assemblerows!, a single pass over the compact tape (4 B/event ~ 1/10th of input bytes). FieldSpan, emitfield!, endrow!, the CRLF lastcr dance, and per-event push! all die; the hot loop's per-event work is one store and a cursor bump with capacity ensured once per 64-byte block. fieldspan() reconstructs (pos, len) in O(1) from the tape + per-row start offsets (rowstartrel keeps dropped rows from corrupting first-field starts). Index memory halves. Vector scan engine (deliberately NOT tuned to one platform): - scanner=:vec (the new default) builds block masks from width-generic LLVM IR (<64 x i8> icmp -> <64 x i1> -> bitcast i64): LLVM lowers it per HOST -- one vpcmpeqb into a mask register on AVX-512, paired 32-byte compares + vpmovmskb on AVX2, compare + reduction sequences on NEON. One implementation covers M-series and x86; nothing is Apple-specific. - scanner=:swar keeps the no-SIMD-assumptions fallback; :scalar remains the oracle; the test matrix runs all three x sequential/parallel x chunk sizes (7638 assertions green at -t1 and -t4). - prefix-XOR via carry-less multiply where universal (PCLMULQDQ on x86_64, PMULL on Apple aarch64 -- llvm.aarch64.neon.pmull64; verified against the shift ladder), shift ladder elsewhere. Single-thread structural scan (200 MiB): mixed 1.56 -> 2.37 GiB/s (tape 1.87, +vec 2.37); long-field 2.8 -> 4.3 GiB/s -- at the practical NEON ceiling; the AVX-512 lowering of the same IR is where the 8 GB/s-class numbers live (pending access to such a machine). Parallel index: 15 GiB/s mixed, 28.5 GiB/s long-field at 8 threads. Giant-single-row bound tightens to 1 GiB (tape packs relpos in 30 bits). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Store the delimiter skip as Int so tape reconstruction supports the same multi-byte delimiter widths as the scalar scanner. Co-Authored-By: Codex <codex@openai.com>
Validate scanner selection before empty-input returns. Pin automatic, explicit, disabled-fast-path, and scalar-fallback dispatch semantics. Co-Authored-By: Codex <codex@openai.com>
Reject chunks of 1 GiB or more before scanning. An unterminated chunk needs one synthetic event beyond its last byte, so the previous boundary overflowed the 30-bit relative position. Co-Authored-By: Codex <codex@openai.com>
Pin tape reserve growth, CRLF boundaries, dropped-row starts, synthetic endings, scanner plumbing, vector mask bit order, prefix-XOR equivalence, and malformed raw-byte differentials. Co-Authored-By: Codex <codex@openai.com>
Update the kernel overview for tape assembly, vector-default scanning, the SWAR fallback, current regression count, and the new extension seams. Co-Authored-By: Codex <codex@openai.com>
Use typed-pointer load IR for Julia 1.10's LLVM 15 parser and opaque-pointer IR on Julia 1.11 and later. Keep the same unaligned width-generic vector compare. Co-Authored-By: Codex <codex@openai.com>
kernel/values.jl -- the new L3, built to the agreed constraints: total functions (value + rc, no fallback to xparse/Base.parse/C ever), zero dependencies in the core (no GMP/MPFR; the Eisel-Lemire powers-of-five table is computed at precompile time by a minimal limb helper -- mul-by-5, shifted compare/subtract, one restoring division -- that never escapes the builder), bootstrap-clean (no @generated/eval/closures), strict parse-set == detect-set spellings, and Dates-independent CivilParts + format programs with thin Dates adapters fenced in their own module. - parseint64: SWAR 8-digit gulps, exact 19/20-digit overflow (RC_OVERFLOW = the lattice Int64->Float64 promotion cue) - parsefloat64: Clinger exact tier -> Eisel-Lemire (128-bit, table-driven) -> Tao simple-decimal-conversion over a fixed 800-digit buffer (768-digit worst-case bound makes it total; subnormals store their leading bit -- the classic bug, caught by the oracle suite) - parsebool strict; findcontent/matchsentinel span utilities (unquoted strings need NO parsing at all) - CivilParts/daysfromcivil (Rata Die, Hinnant) + compilepattern format programs (Dates token rules, unsupported tokens fail at compile time, English month tables as the future Dates-locale seam) test_values.jl: ~1.2M oracle differentials -- 250k ints vs Base, 300k shortest-repr Float64 bit-exact round-trips, 150k random decimal strings, SDC long-mantissa pressure, pinned adversaries (min subnormal, PHP-hang value, 768-digit halfway), every day 1900-2100 vs Dates, format-program round-trips vs Dates.format, pinned deltas (Base throws ERANGE both directions; this layer returns +-Inf/+-0 with OK). valuebench.jl tier-1 micro (ns/value, M-series): ints 4-5 vs xparse 10-19; float short 13 vs 20; date ISO 17 vs 30; datetime 27 vs 81; bool 0.3 vs 15; string unquoted 0.6 vs 29; string quoted 7 vs 36. KNOWN ISSUE (probe_float_anomaly.jl): full-precision floats on the EL path pay ~175ns when the tier-3 call is reachable downstream -- survives @noinline and an inference barrier; signature-identical dummy callee shows no penalty. Priority item for the next review round before kernel integration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Truncated (>19 significant digit) mantissas were decided by a single-sided ambiguity heuristic and could land one ULP off (45 oracle mismatches in 150k random strings). Now the reference fast_float rule: Eisel-Lemire runs on both mant and mant+1; identical results decide, disagreement falls to the exact digit tier. Also pins the underflow half of the ERANGE delta in the adversary set. Value suite: 1,228,714/1,228,714 against the Base/Dates oracles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…om the runtime Every cell now flows through cellcontent (quote/whitespace/sentinel disposition on the raw span) + the KernelValues kernels on the content span. detecttype and parsevalue run the SAME kernels, so parse-set ≡ detect-set by construction and the per-value canonical-conflict guard (six prefix probes on the hot path of every inferred Bool/temporal column) is deleted outright, along with the xparsestring shim and its PosLen31 width workarounds — spans are Int64/Int32 end to end. Semantic deltas, all pinned in tests: Bool is strictly true/false (or the user lists, which now REPLACE the defaults); temporals are pattern-exact; a custom dateformat detects as exactly the type its components spell; a stripped-to-empty field in a typed column is cleanly missing (the old path materialized Date(1) and force-promoted the column to String). A bare mid-field quote that protects a delimiter is still surfaced as a problem — the rule is now explicit (_delimclash) instead of a Parsers span-consumption side effect. ValueOpts replaces Parsers.Options; examples (Chunks/Rows layers) migrate to the same cell layer. Parsers remains only as a benchmark baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apply the standard Eisel-Lemire denormal shift instead of routing every compact subnormal through simple decimal conversion. Correct the anomaly probe to count real tier-3 entries and pin the compact subnormal route. Co-Authored-By: Codex <codex@openai.com>
Reject Bool spellings that collide with earlier inferred types, enforce custom temporal target classes, preserve invalid structural spans in String columns, and return controlled errors for unsupported Number columns and oversized civil years. Align Eisel midpoint detection with fast_float and expand value-layer regression coverage. Co-Authored-By: Codex <codex@openai.com>
…ring truestrings=["1"]/falsestrings=["0"] is a common, legitimate configuration; rejecting it at options construction (round 6) threw away real capability. The sample-dependence hazard only exists for INFERRED columns, so a colliding spelling now just removes Bool from the inference cascade (ValueOpts.inferbool) while user-typed Bool columns keep parsing the lists. Deterministic in both worlds, pinned for nsample 1 and 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
degroup! (values.jl) strips separators with digit-adjacency validation in the integer part only; group widths stay lenient so Indian-style grouping parses. parsevalue's numeric forms take a per-(column x chunk) scratch buffer, so the grouped path never allocates per cell, the no-marks case costs one scan, and groupmark=off runs the exact pre-feature code. groupmark == delim composes through quoting naturally (the indexer already treats quoted marks as content). Detection and parsing share the degroup path, keeping parse-set = detect-set for grouped spellings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parsebigint: strict integer grammar through 18-digit chunks flushed with in-place GMP ops (one BigInt allocated per value; the allocating form measured slower than mpz_set_str, this form beats it). parsebigfloat: correctly rounded at any precision by pure integer scaling — all significant digits into a BigInt, then x5^q (exact) or divrem against 5^-q with the remainder as the sticky bit, one round-half-even of the top prec bits, assembled via exact BigFloat(BigInt)+ldexp so MPFR only stores the result (mpfr_strtofr never runs). Replaces a first-cut HPD bit-walk that was 150x slower than the oracle. Range gate |10^(q+ndig)| <= 1e65536 returns RC_OVERFLOW (upstream form gets power-of-ten jump tables). parseuuid: canonical dashed hex to UInt128, thin Base.UUID adapter in core. All three are user-typed columns only — the inference lattice is untouched. Oracle differentials: 109k adversarial + random spans vs Base.parse (mpfr_strtofr at 53/113/256/1000 bits, bit-exact incl. 400-digit halfway cases), plus 20k cross-pipeline checks (parsebigfloat@53 === parsefloat64). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pow_ui/mul!/mul_2exp!/tdiv_qr! + tstbit/scan1 rounding decisions replace ~15 BigInt temporaries per value with 4: 1379 -> 630 ns/value at 256 bits (mpfr_strtofr oracle: ~364). Suite stays bit-exact (1,338,143). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PooledColumn{KStr|Union{KStr,Missing}}: refs (0 = missing) + first-occurrence
levels as a KStrVector. Pooling is a stitch-time transformation over the
per-chunk StringColumn segments — parse loops, promotion, and re-parse
machinery are untouched, column-level stitch parallelism is preserved, and
level ids are deterministic (chunk order = row order). The intern table is
Dict{KStr,UInt32}: KStr hashing/equality are content-based and allocation-
free, so no custom table machinery. Policy pool=(ratio, cap) abandons the
moment levels exceed min(ratio*rows, cap) and falls back to the flat stitch —
a failed gamble costs one partial hashing pass, never a re-parse. Level
payloads viewing chunk-local extra buffers are copied into the pooled
column's own extra; buf-view and inline payloads pass through.
Measured (30 MiB, 2 pooled categorical cols + high-cardinality id + ints,
8T): pool=off 580 MiB/s, pool=on 613 MiB/s — within noise; the id column
abandons at the cap. pool=false (default) leaves every existing path
byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Use the full mantissa exponent for the prove-out bound and reject oversized exponents before GMP scaling. Extend differential coverage to low, odd, and boundary-shaped precisions. Co-Authored-By: Codex <codex@openai.com>
Reject the reserved NUL marker, return the documented status for marks outside the integer part, and route grouped BigInt and BigFloat columns through reusable scratch storage. Add allocation, inference, and validation coverage. Co-Authored-By: Codex <codex@openai.com>
Use the exact ratio bound, reject negative caps, and normalize extra-backed payload lengths before rebasing. Cover long escaped levels, fallback, promotion, determinism, empty input, and the AbstractVector contract. Co-Authored-By: Codex <codex@openai.com>
Verify that one colliding Bool spelling disables the full inference entry without changing explicit Bool parsing across sample sizes. Co-Authored-By: Codex <codex@openai.com>
The remaining gap to mpfr was allocation profile: three finalizer-registered GMP objects per value (mantissa BigInt, remainder BigInt, result BigFloat) vs mpfr's one. BigWork holds the two temporaries for reuse across a column loop (same pattern as the groupmark scratch); a precompile-time 5^q table (q <= 512) kills the per-value pow_ui; and assembly fuses into one in-place mpfr_set_z + mul_2si + neg (both stores exact — the single rounding remains ours). Base.GMP.MPZ internals are load-time guarded so a Julia that moves them fails loudly, never silently. At 256 bits vs Base.parse (mpfr_strtofr): 46-digit corpus 209 vs 357 ns/value (1.7x faster; was 3.8x slower at first cut), short-mantissa corpus 101 vs 238 (2.4x faster). Workspace statelessness pinned by an interleaved-shapes differential; full suite 1,368,401 bit-exact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e driver, two-phase masked parse
Driver gains four dependency-free hooks, all plain data:
select — Bool/Int/Symbol/String column selection; unselected columns are
never sampled, parsed, or stitched
limit — exact under any parallelism: chunks past the boundary are dropped
before value parsing, the boundary chunk stitches a prefix, and
problem reports beyond the limit are trimmed
rowmask — global row filter: excluded cells are never value-parsed, never
report problems, never pool; stitch gathers qualifying rows to
compact positions (chunk order, deterministic); type inference
samples only qualifying rows
index — reuse a prebuilt BufferIndex across parse calls
kernel/scan.jl implements the Tables.scan protocol on top (requires the
jq/scan branch of Tables.jl, dev'd into the kernel env): bind against the
extracted header, seed types from select items, exact limit/offset, and the
TWO-PHASE MASKED PARSE for filters — parse predicate columns, evaluate the
mask, parse remaining columns only where it is true. Every Scan axis is
consumed exactly, so the residual is always empty.
135 integration tests: pushdown ≅ generic Tables.finish across scan shapes,
chunk geometries, and parallelism; composition with pool (masked pooling) and
groupmark; one pinned deliberate divergence — masked inference means garbage
in excluded rows cannot degrade a qualifying column's type, which the
parse-everything-first generic path structurally cannot offer. 30 MiB demo
(temporal + pooled columns, 0.2%-selective filter): 1.6x faster, 1.5x fewer
allocations than the generic path; the multiplier scales with per-cell cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Advance compact pooled destinations across all-missing segments. Add multi-chunk serial and parallel coverage for missing, pooled, and escaped string cells. Co-Authored-By: Codex <codex@openai.com>
Suppress row and value diagnostics after the boundary limit before they enter the bounded problem log. Preserve the source-earliest problem for deterministic on_error behavior. Co-Authored-By: Codex <codex@openai.com>
Share header parsing with the driver, retain index-level quote problems, suppress duplicate phase-two structural reports, and enforce maxproblems once across all scan phases. Co-Authored-By: Codex <codex@openai.com>
Expose ParsedTable row counts for zero-column filters. Type-detect limit-excluded rows so pushdown keeps full-parse inference without parsing or reporting their values. Co-Authored-By: Codex <codex@openai.com>
Report an EOF quote problem only when the requested row limit reaches the malformed final data row. Co-Authored-By: Codex <codex@openai.com>
This was referenced Sep 15, 2026
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR replaces the CSV.jl parsing and writing internals for 1.0. A quote-aware structural index drives eager, lazy, row, chunk, transpose, and Tables.Scan reads. One column plan carries selection, types, missing-value intent, and field options. The writer renders bounded blocks in deterministic order. CSV keeps a small namespaced API and exports no names.
The rewrite uses released dependencies: Parsers 3, InlineStrings 2, Tables 1.14, DataStrings 1, and Durations 1.1. DataDecimals 1 is a weak dependency.
Implementation and behavior
File,read, andChunkscopy long cells into column-owned buffers. Chunk buffers are adopted by reference. A finished eager read releases the mapped input; changing a source byte vector or rewriting the file cannot change the table.Rowsandlazyremain documented views.fastindex=falseretains the scalar reference path. Generic CPU checks cover both ARM and x86.Pipe 3" long. Indexing detects this case and rebuilds under the field-start rule. Fuzz tests cover dialects and chunk boundaries.CSV.problems(file)and print one summary warning by default.on_error=:collectrecords silently;:errorthrowsCSV.ParseError.Chunkswarns once, andRowskeeps:collect.types=DateTimeandtypemapremain available. DeclaredUnion{Missing,T}string requests retain their union. Pooling is opt-in; unquoted empty fields are missing. Decimal types are explicit and require exact parsing.CSV.Chunksstreams under one header.RowWriter, transforms, custom quoting, date formats, and float formats preserve their existing contracts.The migration guide lists the changed and removed 0.10 options. Julia 1.10 remains the minimum supported runtime.
Performance
The release benchmark pass profiles CPU and allocations for all twelve approximately 200 MiB read inputs and all seven writer inputs, each at one and eight tasks: 38 cases. The final comparison loads each revision as a normal package in two balanced fresh-process pairs, with five warm calls per case per process. The table reports the geometric mean of the paired speed ratios for
ce58a6a→35bf7d8, on an Apple M3 Max and Julia 1.12.6. Higher is faster.Temporal writing removes about 1.25 GB of temporary allocation at either task count. Read timings range from 0.99× to 1.03×; no material reader gain is claimed. An unrelated one-core workload remained active, so small differences, including the threaded long-text result, are not treated as established gains.
The writer optimization is isolated in
23e7e5e;35bf7d8contains only the cleanup. The hosted cleanup-only comparison on AMD EPYC / Julia 1.13 covers 23 cases over four rounds: 22 minimum-run times are within 5%, and DataString writing takes 6.3% less time. Allocations are unchanged and all fingerprints match. This supports the code reduction, not a broad new throughput claim for the cleanup itself.Exploratory in-process prototypes used a nested candidate module. A same-source control exposed substantial loading-mode bias on some small cases, so those timings are not used for the final table. The general-float check covers uniform, normal, and broad-exponent random values; a larger custom-date check matches all values and stays within 1% at both task counts.
The investigation rejected general writer-buffer reuse, an extra reader reservation pass, explicit string-capacity state, extra quote-scanner branches, and bulk-copy escaping. Their isolated wins came with throughput or memory regressions elsewhere. Worker scheduling and string ownership remain unchanged.
Validation
Validated source:
35bf7d8.35bf7d8, including Linux ARM/x86/x64, Windows, macOS ARM, Julia 1.10/current/nightly, trim, quality, and docs. The manual performance job is intentionally skipped on PR CI.Gates before a 1.0 release
The version is already
1.0.0. This PR does not tag or register CSV.Contribution disclosure
Claude authored the rewrite and review fixes. Codex contributed review fixes, latency and performance work, adversarial tests, benchmark tooling, CPU guards, and release validation.
Co-authored by Codex