This directory is intentionally standalone. It is not referenced by the top-level build, so comparison benchmarking remains opt-in and cannot slow normal developer or CI builds.
These benchmarks were run on:
- Windows 11 Home 25H2 (
OS build 26200.8246) - Visual Studio 2026 / MSVC 19.50
- Windows SDK
10.0.26100.0 - C++20
- AVX2 enabled
CSV_ENABLE_THREADS=ON- Google Benchmark 1.8.5
- 12th Gen Intel(R) Core(TM) i5-12400
- 12 logical CPUs at ~2.5 GHz
- Samsung 990 EVO SSD storage
The short version:
fast-cpp-csv-parserremains a strong single-parser-worker baseline, especially on smaller raw read loops.- With positional access and 4+ parser workers,
csv-parserovertakesfast-cpp-csv-parseron clean and quoted read workloads. csv-parserbeats Glaze's CSV reader on the mixed string/numeric/quoted workloads shown below, including the no-background-thread path.csv-parserwins the materialization and materialize+aggregation ETL benchmarks shown below, while also supporting multiline CSV thatfast-cpp-csv-parsercannot parse.csv-parserbeatsrapidcsvfor both DataFrame load and edited load+save workflows at 500K and 5M rows.
All tables below use median real time from the Google Benchmark output.
csv_parser_read_bench: read benchmarks for this library, using the filename constructor and therefore the native mmap parser where supported.csv_parser_multi_pass_bench: single-parser-worker materialization and multi-pass ETL-style benchmarks using reusableCSVRowobjects.csv_parser_fast_cpp_read_bench: one-binary positional-read comparison between this library,fast-cpp-csv-parser, and Glaze. It labels scheduling explicitly:csv-parserno-background-thread,csv-parserSPSC producer/consumer,csv-parserspeculative parallel parsing, andfast-cpp-csv-parserSPSC-style background I/O.fast_cpp_csv_parser_read_bench: read-focused benchmarks forfast-cpp-csv-parser.fast_cpp_csv_parser_multi_pass_bench: single-parser-worker materialization and multi-pass ETL-style benchmarks forfast-cpp-csv-parser, materializing into named-field row structs before running repeated passes.glaze_csv_read_bench: read-focused benchmarks for Glaze's CSV reader, materializing into reflected row structs throughglz::read<glz::opts_csv>.glaze_csv_multi_pass_bench: single-parser-worker materialization and multi-pass ETL-style benchmarks for Glaze, using reflected row structs with native numeric deserialization for the aggregation fields.dataframe_rapidcsv_roundtrip_bench: table/round-trip-oriented benchmarks comparing this library'sDataFrameworkflow withrapidcsv, including load-only and edited full round-trip cases.
The rapidcsv round-trip benchmark uses rapidcsv::Document::Save() for the
rapidcsv write side. The csv-parser benchmark writes through this library's
CSVWriter, so benchmark names call that out explicitly.
From the repository root:
cmake -S benchmarks -B build/benchmarks -DCMAKE_BUILD_TYPE=Release
cmake --build build/benchmarks --config ReleaseBy default, CMake uses FetchContent for Google Benchmark and the comparison
libraries if they are not already available. For offline or pinned dependency
work, provide local include directories instead:
cmake -S benchmarks -B build/benchmarks `
-DFAST_CPP_CSV_PARSER_INCLUDE_DIR=C:/src/fast-cpp-csv-parser `
-DGLAZE_INCLUDE_DIR=C:/src/glaze/include `
-DRAPIDCSV_INCLUDE_DIR=C:/src/rapidcsv/srcThe Glaze benchmark targets are compiled with C++23 flags because current Glaze headers use C++23 library facilities. The rest of the benchmark project remains configured as C++20.
The benchmark programs expect a CSV file with this 8-column schema:
id,city,state,category,amount,quantity,flag,note
Three payload profiles are supported:
clean: deterministic scalar fields with no embedded commas, quotes, or line breaks.quoted: quoted commas and doubled quotes in thenotecolumn, but no embedded line breaks.multiline: quoted commas, doubled quotes, and embedded line breaks in thenotecolumn.
Two row shapes are supported:
standard: the original compact schema valueswide: longer string values across the same 8 columns
Use enough rows to cross csv-parser's 10 MB chunk boundary when measuring reader behavior. At least 500K rows is the project baseline for large-file coverage.
Example run:
build/benchmarks/Release/csv_parser_read_bench.exe --benchmark_format=json data/bench_8col_500k.csv
build/benchmarks/Release/csv_parser_multi_pass_bench.exe --benchmark_format=json data/bench_8col_500k.csv
build/benchmarks/Release/csv_parser_fast_cpp_read_bench.exe --benchmark_format=json data/bench_8col_500k.csv
build/benchmarks/Release/fast_cpp_csv_parser_read_bench.exe --benchmark_format=json data/bench_8col_500k.csv
build/benchmarks/Release/fast_cpp_csv_parser_multi_pass_bench.exe --benchmark_format=json data/bench_8col_500k.csv
build/benchmarks/Release/glaze_csv_multi_pass_bench.exe --benchmark_format=json data/bench_8col_500k.csv
build/benchmarks/Release/dataframe_rapidcsv_roundtrip_bench.exe --benchmark_format=json data/bench_8col_500k.csvOr use the helper script, which generates the default 500K-row dataset, builds the standalone benchmark tree, and runs any benchmark executables that were available in the build:
benchmarks/scripts/run_benchmarks.ps1For Python reader comparisons against stdlib csv.reader, build the Python
binding and run:
python python/benchmarks/compare_readers.py path/to/input.csvThe Python helper prints one tab-separated line per available variant with file
path, file size, rows, columns, elapsed seconds, MiB/s, and rows/s. The matrix
compares stdlib csv.reader, csvpy.reader, stdlib csv.DictReader, and
csvpy.DictReader, with separate csvpy string-only and cast=True runs.
The script writes JSON results to
benchmarks/results/<row-count>_rows/<profile>/<benchmark-name>.json and also
copies the exact input CSV locally to
benchmarks/results/<row-count>_rows/<profile>/benchmark_input.csv.
By default it runs clean, quoted, and multiline payloads at 500K rows and
5M rows. The helper skips the fast-cpp-csv-parser benchmarks on multiline
payloads because that library does not support quoted line breaks.
There are two very different stories here:
-
Raw parsing throughput
- Count rows
- Read rows
- Minimal downstream work
-
Actual ETL / aggregation work
- Materialize rows into a reusable row structure
- Run repeated passes over the data
- Pay the cost of turning bytes into something you can work with
fast-cpp-csv-parser is excellent at the first category. These benchmarks are
included because that speed is real and worth acknowledging.
The second category is where csv-parser pulls ahead in these runs. It is
faster on the materialize and materialize+multi-pass workloads shown below, and
it supports multiline CSV files that fast-cpp-csv-parser cannot parse.
One more important point: the materialization and multi-pass ETL comparisons
below are intentionally single-parser-worker runs to stay as apples-to-apples
as possible.
In real use, fast-cpp-csv-parser stops at parsing. The caller owns row
materialization, repeated-pass analysis, and any thread-pool or chunk-parallel
orchestration.
csv-parser ships those higher-level pieces out of the box, including
chunk_parallel_apply(), so the benchmark is conservative with respect to the
actual library surface each user gets.
Median real time, 8-column datasets.
| Dataset | csv-parser count | fast-cpp count | csv-parser read | fast-cpp read |
|---|---|---|---|---|
| 500K clean | 146.5 ms | 81.8 ms | 164.9 ms | 82.8 ms |
| 500K quoted | 197.2 ms | 118.8 ms | 223.5 ms | 136.2 ms |
| 5M clean | 547.6 ms | 881.5 ms | 1,087 ms | 944.2 ms |
| 5M quoted | 665.4 ms | 1,153 ms | 1,140 ms | 1,164 ms |
That is the honest "bytes in, rows out" picture. fast-cpp-csv-parser wins
the smaller raw throughput tests on the inputs it supports, while the mmap path
is faster on the current 5M-row count runs.
It is also worth noting that csv-parser supports a more dynamic row model by
default. If users choose positional access, which is much closer to the access
pattern fast-cpp-csv-parser effectively pushes users toward, the mmap path
changes the picture. In this table, csv-parser SPSC means one background
producer/parser worker feeding the foreground consumer through the reader queue;
spec-N means speculative parallel parsing with N parser workers.
fast-cpp-csv-parser SPSC means its default background block reader plus one
foreground parser.
New runs of csv_parser_fast_cpp_read_bench also include
csv_parser_no_background_thread_read_8col, which disables the background
CSVReader worker at runtime.
The no-background-thread result is useful for constrained environments and extremely cheap caller loops. If user code does almost nothing per row, the background producer/consumer layer has little downstream work to hide and may not improve raw throughput. The SPSC-style mode is still the better default for general use because it decouples parser progress from caller-side work, and it helps speculative parsing keep the parser side busy while the foreground consumer performs conversions, filtering, aggregation, or other row handling.
| Dataset | csv-parser no background | csv-parser SPSC | csv-parser spec-2 | csv-parser spec-4 | csv-parser spec-8 | fast-cpp SPSC read |
|---|---|---|---|---|---|---|
| 500K clean | 155.4 ms | 154.5 ms | 123.9 ms | 76.6 ms | 77.7 ms | 88.0 ms |
| 500K quoted | 192.9 ms | 196.3 ms | 117.6 ms | 89.3 ms | 83.1 ms | 119.1 ms |
| 5M clean | 1,390 ms | 1,442 ms | 975.8 ms | 702.2 ms | 605.0 ms | 915.1 ms |
| 5M quoted | 1,712 ms | 1,889 ms | 1,331 ms | 977.0 ms | 819.9 ms | 1,320 ms |
That is the core performance story for modern csv-parser: positional reads
scale with worker threads and overtake fast-cpp-csv-parser by 4 threads on
these clean and quoted runs.
This table compares the one-pass row materialization benchmarks:
csv-parser:materialize_csvrow_8colfast-cpp-csv-parser:materialize_struct_8col
Lower is better.
| Dataset | csv-parser | fast-cpp-csv-parser | Winner |
|---|---|---|---|
| 500K clean | 156.8 ms | 218.8 ms | csv-parser |
| 500K quoted | 211.1 ms | 301.9 ms | csv-parser |
| 500K multiline | 207.4 ms | unsupported | csv-parser |
| 5M clean | 739.6 ms | 2,105 ms | csv-parser |
| 5M quoted | 930.6 ms | 2,489 ms | csv-parser |
| 5M multiline | 827.1 ms | unsupported | csv-parser |
This table compares the closest apples-to-apples ETL benchmark:
csv-parser:materialize_and_multi_pass_csvrow_8colfast-cpp-csv-parser:materialize_and_multi_pass_struct_8col
Lower is better.
| Dataset | csv-parser | fast-cpp-csv-parser | Winner |
|---|---|---|---|
| 500K clean | 215.4 ms | 237.1 ms | csv-parser |
| 500K quoted | 262.9 ms | 280.7 ms | csv-parser |
| 500K multiline | 287.5 ms | unsupported | csv-parser |
| 5M clean | 1,251 ms | 2,252 ms | csv-parser |
| 5M quoted | 1,517 ms | 3,090 ms | csv-parser |
| 5M multiline | 1,372 ms | unsupported | csv-parser |
fast-cpp-csv-parseris still a strong single-parser-worker raw-read baseline on the inputs it supports.csv-parserwins the positional-read comparison once 4 parser workers are available.csv-parserwins the materialization and ETL-style tables above.csv-parserhandles quoted line breaks;fast-cpp-csv-parserdoes not.
Glaze was added because its reflected struct API is a natural comparison point for users who want typed CSV deserialization. Its CSV reader is much smaller and simpler than csv-parser's parser core, which is a reasonable tradeoff for a general serialization library. These results measure a mixed workload with strings, quoted fields, numeric fields, and row-oriented access. A dense number-heavy CSV may move the relative numbers, but this mixed workload is a realistic target for csv-parser.
This table comes from the same csv_parser_fast_cpp_read_bench bundle used in
the fast-cpp comparison. It reports positional string access for csv-parser,
caller-owned strings for fast-cpp-csv-parser, and reflected row structs for
Glaze.
| Dataset | csv-parser no background | csv-parser SPSC | csv-parser spec-8 | Glaze read |
|---|---|---|---|---|
| 500K clean | 155.4 ms | 154.5 ms | 77.7 ms | 243.0 ms |
| 500K quoted | 192.9 ms | 196.3 ms | 83.1 ms | 302.5 ms |
| 5M clean | 1,390 ms | 1,442 ms | 605.0 ms | 2,418 ms |
| 5M quoted | 1,712 ms | 1,889 ms | 819.9 ms | 3,122 ms |
This table compares one-pass materialization into each library's natural row representation:
csv-parser: reusableCSVRowobjects backed by parser-owned row data- Glaze: reflected row structs through
glz::read<glz::opts_csv>
| Dataset | csv-parser | Glaze | Winner |
|---|---|---|---|
| 500K clean | 156.8 ms | 209.8 ms | csv-parser |
| 500K quoted | 211.1 ms | 267.1 ms | csv-parser |
| 500K multiline | 207.4 ms | 282.9 ms | csv-parser |
| 5M clean | 739.6 ms | 1,972 ms | csv-parser |
| 5M quoted | 930.6 ms | 2,930 ms | csv-parser |
| 5M multiline | 827.1 ms | 2,492 ms | csv-parser |
Both libraries use their own numeric conversion/deserialization paths for the numeric fields in this benchmark.
| Dataset | csv-parser | Glaze | Winner |
|---|---|---|---|
| 500K clean | 215.4 ms | 232.6 ms | csv-parser |
| 500K quoted | 262.9 ms | 283.1 ms | csv-parser |
| 500K multiline | 287.5 ms | 287.7 ms | effectively tied |
| 5M clean | 1,251 ms | 2,084 ms | csv-parser |
| 5M quoted | 1,517 ms | 3,381 ms | csv-parser |
| 5M multiline | 1,372 ms | 2,649 ms | csv-parser |
The benchmark edits two columns before save paths, so the test is not merely "load a file and dump it back unchanged." Both libraries are asked to do small, realistic table mutation work.
These rapidcsv comparisons are single-threaded table load/save benchmarks. They
do not include a speculative parallel CSVReader, DataFrameExecutor,
column_parallel_apply(), or chunk_parallel_apply(); using those primitives
would likely widen the gap for real ETL workloads.
Median real time, lower is better.
| Dataset | csv-parser load | rapidcsv load | csv-parser load+save | rapidcsv load+save |
|---|---|---|---|---|
| clean | 156.7 ms | 244.7 ms | 310.5 ms | 683.0 ms |
| quoted | 211.6 ms | 330.1 ms | 409.8 ms | 890.5 ms |
| multiline | 220.4 ms | 325.2 ms | 412.7 ms | 905.8 ms |
Median real time, lower is better.
| Dataset | csv-parser load | rapidcsv load | csv-parser load+save | rapidcsv load+save |
|---|---|---|---|---|
| clean | 666.3 ms | 2,587 ms | 2,070 ms | 7,140 ms |
| quoted | 951.6 ms | 3,450 ms | 2,910 ms | 10,425 ms |
| multiline | 771.1 ms | 3,494 ms | 2,515 ms | 8,987 ms |
Some representative median ratios:
| Workflow | Dataset | Result |
|---|---|---|
| Load | 500K clean | csv-parser 1.56x faster |
| Load+save | 500K quoted | csv-parser 2.17x faster |
| Load | 5M multiline | csv-parser 4.53x faster |
| Load+save | 5M clean | csv-parser 3.45x faster |
| Load+save | 5M quoted | csv-parser 3.58x faster |
- These are Windows/MSVC results. The relative ordering may shift on different compilers, CPUs, or storage setups.
- Google Benchmark adds its own harness overhead, so absolute runtimes here are not the same as a minimal hand-timed production loop. Real-world throughput is often somewhat better, especially for the shortest benchmarks.
- The thread-count comparisons are Google Benchmark tests too. They are useful for relative comparisons, but a dedicated application loop will usually give a cleaner view of peak throughput.
- Some row-count benchmarks showed noisy count-only means on the 5M datasets, so the tables above use median real time instead of means.
- The
multilineprofile matters. Many CSV libraries look great until quoted line breaks appear. - "Faster parser" and "faster ETL tool" are not the same claim. This directory intentionally measures both.
Raw JSON outputs for the current benchmark runs live under:
benchmarks/results/
The repository tracks the benchmark JSON files. The generated CSV inputs are kept out of git because they are large and reproducible.
Each profile directory may contain:
benchmark_input.csv: the exact generated input used for the run, ignored by gitcsv_parser_read_bench.jsoncsv_parser_multi_pass_bench.jsoncsv_parser_fast_cpp_read_bench.jsonwhere applicable; includes the side-by-side csv-parser, fast-cpp-csv-parser, and Glaze read comparison when those dependencies are availablefast_cpp_csv_parser_read_bench.jsonwhere applicablefast_cpp_csv_parser_multi_pass_bench.jsonwhere applicableglaze_csv_read_bench.jsonwhere applicableglaze_csv_multi_pass_bench.jsonwhere applicabledataframe_rapidcsv_roundtrip_bench.json