Performance Tuning - dathere/qsv GitHub Wiki
Tier: Advanced
Note
Canonical reference: docs/PERFORMANCE.md and the TL;DR docs/PERFORMANCE_TLDR.md. Benchmarks: docs/BENCHMARKS.md and the live dashboard at qsv.dathere.com/benchmarks.
This page is a workflow guide to performance โ when to reach for which knob, in what order. Numbers quoted on this page link to BENCHMARKS.md as the source of truth.
If you have five minutes to make qsv faster on your workload, in order:
-
Index your file.
qsv index data.csvโ one-time cost (~14s on 15 GB). Speeds up 9 commands (and improvessniff's inference). -
Pre-populate the stats cache.
qsv stats --stats-jsonl data.csvโ makesfrequency,schema,validate,pragmastat,pivotp,describegpt,sqlp, andscoresqlsmarter. -
Set
QSV_AUTOINDEX_SIZE. Auto-creates the index for any file above the threshold; auto-refreshes stale ones. -
For Polars commands, generate a Polars schema.
qsv schema --polars data.csvโ written todata.pschema.json, picked up automatically bysqlp/joinp/pivotp. -
For files > RAM, use
extsort/extdedupinstead ofsort/dedup.
Most users stop here. The rest of this page is for the remaining 1%.
Tip
Not sure where to start? Let qsv profile your machine for you โ see Machine profiling with qsv-tune below.
New in 21.1.0, the qsv-tune script (scripts/qsv-tune.sh for Linux/macOS, scripts/qsv-tune.ps1 for Windows) inspects your machine's RAM, CPU cores, and disk type โ plus the qsv binary's compiled-in memory allocator โ and emits a tuned set of QSV_* environment variables in .env format.
It is dry-run by default: with no arguments it prints an annotated, ready-to-use .env block to stdout and writes nothing.
# Print recommended settings (writes nothing):
scripts/qsv-tune.sh
# Capture the suggestion:
scripts/qsv-tune.sh > my-qsv.env
# Merge the QSV_* block into ./.env (idempotent, with a .bak backup):
scripts/qsv-tune.sh --write# Windows (best-effort):
.\scripts\qsv-tune.ps1
.\scripts\qsv-tune.ps1 -WriteAllocator tuning (jemalloc/mimalloc) is reported separately as export lines, not written into .env โ allocators read their config at process start, before qsv loads .env, so those variables must live in the real shell environment.
The recommended values are heuristic starting points โ always benchmark against your own data shapes (see docs/PERFORMANCE.md). Implements issue #2829.
qsv builds a <file>.idx sidecar that lets commands skip parsing rows they don't need.
| Command | Without index | With index |
|---|---|---|
count |
scans the file | instantaneous |
sample (reservoir) |
scans the file | random I/O โ no full scan |
slice |
parses rows up to the slice | parses only the sliced rows |
stats |
streaming, single-threaded | streaming, multithreaded |
frequency |
single-threaded | multithreaded |
split |
single-threaded | multithreaded |
schema |
single-threaded | multithreaded |
luau |
sequential mode only | unlocks random-access mode + LASTROW
|
search |
sequential | parallel (per-chunk) |
sniff |
samples the first N rows | distributed sample โ better type/date inference |
The sniff win is about accuracy, not just speed: instead of inferring the
schema from only the first --sample rows, an indexed file is sampled across its
whole length โ the first & last 5 rows, 5 rows each around the 25th/50th/75th
percentiles, and the rest random โ so column types and dates that only appear late
in the file are detected. Falls back to the contiguous first-N sample when the file
is not indexed.
This also sharpens stats date type inference. stats --infer-dates uses its
default --dates-whitelist sniff, which runs sniff to decide which columns to
treat as Date/DateTime before computing their summary stats. On an indexed file
that sniff step uses the distributed sample, so date columns whose date values
only appear late in the file are correctly typed (and get date-aware Min/Max) โ
whereas the first-N sample would miss them and leave the column inferred as text or
a number.
Index size is typically ~1-2% of the source file. A 15 GB CSV indexes in ~14 seconds and the index is ~27 MB.
qsv index huge.csv
# Or โ auto-index everything above 100 MB:
export QSV_AUTOINDEX_SIZE=100000000See Indexing, Compression & Diff โ index.
Note
stats got faster and leaner in 21.1.0 โ roughly ~40% faster unindexed, ~42% faster indexed --everything, and ~43% less peak memory (#3970). Pre-populating the stats cache below is still the single most impactful thing you can do, since it benefits every "smart" command, not just stats itself.
Running qsv stats --stats-jsonl <file> writes two sidecar files:
-
<file>.stats.csvโ human-readable -
<file>.stats.csv.data.jsonlโ machine-readable (the file other commands actually consume)
Every "smart" command (๐ช in the README legend) automatically picks up the JSONL and uses it:
-
frequencyโ short-circuits all-unique columns (would otherwise blow memory) -
schemaโ skips redundant type inference -
validateโ faster generated-schema runs -
pragmastatโ date-aware mode requires the cache -
pivotpโ smart aggregation auto-selection -
tojsonlโ smart JSON type inference -
sqlp scoresqlโ query plan analysis -
describegptโ deterministic statistical context for the LLM -
sample --systematic/--weighted/--clusterโ extra checks against cardinality
Set QSV_STATSCACHE_MODE=force to automatically create the cache whenever a smart command runs on a file that doesn't have one yet. See Stats Cache & Caching.
qsv detects logical CPUs and uses all of them by default. Cap with QSV_MAX_JOBS=N if you want to share CPU with other workloads.
Always-multithreaded (๐): apply, applydp, blake3, datefmt, dedup, diff, excel, extsort, geocode, joinp, jsonl, replace, snappy, sort, sqlp, template, to, tojsonl, validate.
Multithreaded with an index (๐๏ธ): frequency, sample, schema, search, searchset, split, stats.
For the full mapping, see the README legend.
Commands marked ๐คฏ load the entire file into memory:
-
dedup(unless--sorted) pragmastat-
reverse(unless an index is present โ then constant memory) -
sortโ for files > RAM useextsort -
statsโ for--cardinality/--quartiles/--medianmodes table-
transpose(unless--multipassor--long)
Commands marked ๐ฃ use memory proportional to column cardinality:
frequencyschematojsonl
For huge files where you can't avoid memory mode, set the safety env vars:
export QSV_MEMORY_CHECK=1 # opt into pre-check
export QSV_FREEMEMORY_HEADROOM_PCT=10 # reserve 10% of free RAMWith QSV_MEMORY_CHECK=1, qsv computes available memory (including swap), applies a platform multiplier (1.3ร macOS, 1.15ร Linux, 1.0ร Windows), subtracts the headroom, and aborts if the file won't fit.
stats and frequency can switch to Apache DataSketches for constant-memory operation:
# stats: approximate quartiles (t-digest) and cardinality (HyperLogLog);
# under --everything, the cap is what bounds the mode/cardinality tables
qsv stats --everything --cardinality-method approx --quantile-method approx \
--mode-cardinality-cap 10000 data.csv
# frequency: top-K with bounded error
qsv frequency --sketch-method frequent_items --sketch-map-size 16384 data.csvBoth add ~1.5% error in exchange for O(1) memory per column. Use when exact methods OOM.
Note
When modes are computed (--everything, --mode), stats keeps the exact table of distinct values regardless of --cardinality-method, so only --mode-cardinality-cap bounds that memory; approx then supplies the cardinality for columns that pass the cap. In 24.0.0, approx without a cap under --everything was slower than exact and replaced exact counts with estimates; fixed in 25.0.0 (#4806). See Aggregation & Statistics.
New in 24.0.0: parallel stats runs warn when exact mode & cardinality tracking may not fit in memory (estimated > 80% of available). Those tables hold every distinct value of every column, and each chunk keeps its own copy, so --everything on a 1M-row, 44-column NYC 311 file peaks at 607 MB with --jobs 1 but 1.2 GB with --jobs 16. The fix the warning suggests: cap the tables, keeping approximate cardinalities.
qsv stats --everything --mode-cardinality-cap 10000 --cardinality-method approx data.csvOn that file a cap of 10000 cut peak memory by ~65% and runtime by half. Defaults and output are unchanged.
Also new in 24.0.0: frequency is 1.5ร faster by default (2ร with --weight) with lower peak memory, and stats --infer-dates (1.43ร) and sniff (1.81ร) no longer contend for shared regex locks across threads.
As of 21.1.0, the prebuilt qsv binaries on native targets are Profile-Guided-Optimization (PGO) tuned โ the compiler optimizes hot paths based on real execution profiles, so you get extra performance out of the box with no action required (#1448). Just download a prebuilt binary.
24.0.0 exception โ Apple Silicon. The 24.0.0
aarch64-apple-darwinprebuilt was built without PGO (#4740); see the note on Benchmarks. With Rust 1.99, the PGO-instrumented binary crashed beforemain(). The cause was thecccrate: it passed Rust's PGO flags on to Apple clang, whose profile format (v10) didn't match Rust 1.99's (v11, LLVM 23), so C dependencies such as jemalloc crashed the profiling runtime. 25.0.0: Apple Silicon PGO is restored. #4757 first worked around it, andcc1.8.0 then fixed it upstream by no longer forwarding the flags (rust-lang/cc-rs#1986). Withcc1.8.0 inCargo.lock, the workaround was removed (#4795).Building your own PGO binary on macOS (with
scripts/build-pgo.shorcargo pgo): make sureCargo.lockhascc1.8.0 or newer. With an oldercc, the instrumented binary segfaults beforemain()whenever Apple clang's LLVM major version differs from rustc's.
Through 23.0.x, x86_64 prebuilts targeted the baseline instruction set to avoid SIGILL on older CPUs. New in 24.0.0: they target x86-64-v3 โ AVX2/FMA/BMI2 โ so a native build mainly adds newer extensions such as AVX-512. If you build from source on your own machine, opt into your CPU's full instruction set:
CARGO_BUILD_RUSTFLAGS='-C target-cpu=native' \
cargo build --release --locked --bin qsv -F all_featuresApple Silicon, Windows-on-ARM, IBM Power, and IBM Z prebuilds always have CPU optimizations enabled โ no need to rebuild on those platforms.
qsv ships a nightly-toolchain prebuilt with extra optimizations enabled. See docs/PERFORMANCE.md#nightly-release-builds.
qsv builds with mimalloc by default. For huge-file workloads, jemalloc is sometimes faster. Both have their own environment variables โ see the mimalloc options and jemalloc options docs.
To see which allocator your binary uses: qsv --version shows it in the version string.
export QSV_RDR_BUFFER_CAPACITY=131072 # reader buffer, default 128 KB
export QSV_WTR_BUFFER_CAPACITY=524288 # writer buffer, default 512 KBBump up on slow disks (network mounts, spinning disks). Bump down for memory-constrained environments.
Polars-powered commands (๐ปโโ๏ธ): color, count, joinp, lens, pivotp, prompt, schema, scoresql, sqlp. They respond to Polars's env vars:
export POLARS_VERBOSE=1 # log plan + execution to stderr
export POLARS_PANIC_ON_ERR=1 # panic instead of returning an error
export POLARS_BACKTRACE_IN_ERR=1 # include backtrace in errorsFor the full set, see Polars env vars.
The single biggest Polars optimization is the Polars schema (qsv schema --polars data.csv). It tells Polars the column types without an inference scan.
-
qsv index <file>(orQSV_AUTOINDEX_SIZE) -
qsv stats --stats-jsonl <file>(orQSV_STATSCACHE_MODE=force) -
qsv schema --polars <file>(for Polars commands) - Replace
sortโextsort,dedupโextdedupfor files > RAM - For huge cardinalities:
--cardinality-method approx/--quantile-method approx - For huge files:
--sketch-method frequent_itemsinfrequency - Set
QSV_MAX_JOBSto share CPU;QSV_MEMORY_CHECK=1for safety - Build with
target-cpu=nativeif compiling from source - Point
extsort/extdedupat a fast spill location (NVMe preferred) with their tmp-dir flag (extsort --tmp-dir,extdedup --temp-dir), or the OSTMPDIR/TEMPenv var - If on x86_64 and getting SIGILL, build from source for your own CPU (expected on CPUs without AVX2 since 24.0.0) โ the portable
qsvpvariant was retired in 22.0.1
-
docs/PERFORMANCE.mdโ canonical reference -
docs/PERFORMANCE_TLDR.mdโ TL;DR -
docs/BENCHMARKS.mdโ methodology - qsv.dathere.com/benchmarks โ live benchmark dashboard
- Environment Variables โ every QSV_* var
- Stats Cache & Caching
- Indexing, Compression & Diff
- Recipe: Larger-than-RAM CSV
- Binary Variants โ which binary to build or download