Guide
Benchmarks
CsvHelper against Sep and Sylvan.Data.Csv, measured with BenchmarkDotNet.
CsvHelper against Sep 0.17.1 and Sylvan.Data.Csv 1.4.4, measured with BenchmarkDotNet 0.15.8 on .NET 10.0.12, Windows 11, AMD Ryzen 9 9950X3D (one core, AVX-512). Times are means per operation; lower is better. Sep is the baseline. CsvHelper runs with its default options, except where a row says otherwise.
Data sets (deterministic, generated by DataGenerator, 50,000 records each):
- PackageAssets (16 MB, LF): 25 short text columns modelled on the NuGet package assets data set; one column is quoted.
- Floats (14.7 MB, LF): 3 text columns and 25
floatcolumns, like a machine learning feature file. - Quoted (7.1 MB, CRLF): 7 columns; the 4 text columns are quoted and contain separators, escaped quotes and line breaks.
Scenarios: Rows finds the records only. Cols touches every field as a span, without creating strings.
Records builds an object with 25 string properties per record and keeps them all. Strings creates a string
for every field. Floats parses the 25 floats of each record and sums them. Write writes the same records, and
every library produces byte-identical RFC 4180 output. Reads come from a MemoryStream unless the data column says
otherwise. The Async variants use each library's async API.
Reading
| Data | Scenario | Sep | CsvHelper | Sylvan | CsvHelper vs Sep |
|---|---|---|---|---|---|
| PackageAssets | Rows | 2.80 ms | 2.01 ms | 4.79 ms | 1.40x faster |
| PackageAssets | RowsAsync | 3.04 ms | 2.02 ms | 5.23 ms | 1.51x faster |
| PackageAssets | Cols | 3.75 ms | 3.02 ms | 6.73 ms | 1.24x faster |
| PackageAssets | Records | 28.7 ms | 26.9 ms | 30.6 ms | 1.07x faster |
| PackageAssets | RecordsAsync | 28.4 ms | 26.5 ms | 31.2 ms | 1.07x faster |
| PackageAssets | Records, both pooling strings ¹ | 27.1 ms | 19.3 ms | 1.40x faster | |
PackageAssets, from a string |
Cols | 3.41 ms | 3.05 ms | 6.24 ms | 1.12x faster |
PackageAssets, from a string |
Records | 28.0 ms | 27.4 ms | 30.2 ms | 1.02x faster |
| PackageAssets, from a file | Cols | 9.12 ms | 4.05 ms | 8.95 ms | 2.25x faster |
| PackageAssets, from a file | ColsAsync | 22.4 ms | 5.15 ms | 13.4 ms | 4.35x faster |
| Floats | Floats | 19.9 ms | 12.2 ms | 48.3 ms | 1.62x faster |
| Floats | FloatsAsync | 19.7 ms | 13.6 ms | 48.4 ms | 1.45x faster |
| Quoted | Rows | 2.63 ms | 1.46 ms | 6.90 ms | 1.80x faster |
| Quoted | Strings | 6.17 ms | 5.60 ms | 11.4 ms | 1.10x faster |
| Quoted | StringsAsync | 6.43 ms | 4.79 ms | 11.9 ms | 1.34x faster |
Writing
| Data | Scenario | Sep | CsvHelper | Sylvan | CsvHelper vs Sep |
|---|---|---|---|---|---|
| PackageAssets | Write | 27.5 ms | 8.64 ms | 12.7 ms | 3.18x faster |
| PackageAssets | WriteAsync | 44.7 ms | 6.77 ms | 15.1 ms | 6.60x faster |
| PackageAssets | WriteRecords ² | 8.66 ms | |||
| PackageAssets | WriteRecordsAsync ² | 9.18 ms | |||
| Floats | Write | 86.3 ms | 23.7 ms | 62.5 ms | 3.64x faster |
| Quoted | Write | 11.0 ms | 5.28 ms | 5.80 ms | 2.08x faster |
¹ Sep with its per-column string pool (SepToString.PoolPerCol()), CsvHelper with PoolStrings = true.
² Objects of a [CsvRecord] type with 25 string fields: CsvHelper's WriteRecords with its generated record writer,
which is 2% slower than writing the fields by hand (8.48 ms). Sep and Sylvan do not write objects.
Throughput: CsvHelper finds records at 8.0 GB/s (Sep 5.7 GB/s) and reads every field of PackageAssets at 5.3 GB/s (Sep 4.3 GB/s). It writes 1.9 GB/s of text records (Sep 0.58 GB/s) and 0.62 GB/s of floats (Sep 0.17 GB/s).
Records keeps 1.25 million strings in memory (62 MB allocated per run, the same for every library), so garbage
collection takes most of its time, and the libraries end up close. Pooling strings (PoolStrings = true) cuts that to
18.4 MB (Sep's pool: 28.4 MB). It is off by default because for strings that are used and dropped (Strings) a pool
lookup costs more than it saves. The writer allocates under 400 bytes per run (Sep 7–10 KB).
How the comparison is kept fair:
- All three libraries return the same data: the same record count and content hash, the same sum of floats,
byte-identical written files (in the PackageAssets Cols scenarios Sep's total is larger because its spans keep the
quotes, see below).
dotnet run -c Release -- verifyruns each benchmark once and prints these values. - Sep reads with
Unescape = truein the Records and Strings scenarios and on the Quoted data, so every library returns the same unquoted values. In the PackageAssets Rows and Cols scenarios it uses its defaults, so in Cols its spans of the one quoted column still include the quotes, which is slightly less work. Sep writes withEscape = true, Sylvan with\r\nline endings. - File scenarios use each library's own file API (
Sep.Reader().FromFile/FromFileAsync, Sylvan'sCreate/CreateAsyncwith a path,CsvReader.FromFile). - The job pins the process to one performance core. It sets
DOTNET_TC_CallCountingDelayMs=0so that benchmarks taking a long time per call reach optimized (Tier-1) code before measurement, which applies to every library alike.
Other parsing modes
The parsing modes other than RFC 4180, on the PackageAssets records (50,000, 25 columns), on the same machine. Sep has no escape mode, and none of the other libraries reads or writes fixed widths, so each row names its own baseline.
- Escape (16.1 MB): a backslash before every comma and backslash (framework monikers, Windows paths), nothing
quoted. Sylvan with
CsvStyle.Escaped. - NoEscape (16.0 MB): tab separated, nothing quoted or escaped. Sep with
DisableQuotesParsing; Sylvan reads it in its standard mode. - FixedWidth (29.0 MB): every column padded with spaces to its longest value. The baseline is the usual
hand-written loop:
StreamReader.ReadLine, then a trimmed slice per column (Substringfor records), andPadRightper value for writing.
| Mode | Scenario | Baseline | CsvHelper | Others | CsvHelper vs baseline |
|---|---|---|---|---|---|
| Escape | Rows | Sylvan 15.1 ms | 2.18 ms | 6.92x faster | |
| Escape | Cols | Sylvan 19.5 ms | 4.95 ms | 3.94x faster | |
| Escape | Records | Sylvan 42.3 ms | 22.1 ms | 1.92x faster | |
| Escape | Write | Sylvan 14.9 ms | 12.5 ms | 1.19x faster | |
| NoEscape | Rows | Sep 2.56 ms | 1.45 ms | Sylvan 2.97 ms | 1.77x faster |
| NoEscape | Cols | Sep 3.53 ms | 2.27 ms | Sylvan 4.93 ms | 1.55x faster |
| NoEscape | Records | Sep 27.7 ms | 19.2 ms | Sylvan 28.8 ms | 1.45x faster |
| NoEscape | Write | Sep 15.9 ms | 8.48 ms | 1.87x faster | |
| FixedWidth | Rows | ReadLine 3.39 ms | 1.91 ms | 1.77x faster | |
| FixedWidth | Cols | ReadLine 13.6 ms | 11.7 ms | 1.17x faster | |
| FixedWidth | Records | ReadLine 70.0 ms | 35.3 ms | 1.99x faster | |
| FixedWidth | Write | PadRight 16.7 ms | 11.8 ms | 1.42x faster |
Every library returns the same results here too, and every writer produces byte-identical output.
Separators of several characters
The PackageAssets and Quoted data with || in place of every comma (in the Quoted data, the commas inside quoted fields
become || too), read through CsvSeparatorStream. Sep and Sylvan do not read such separators. The baseline is
CsvHelper on the same data with commas, so the difference is the cost of the stream.
| Data | Scenario | CsvHelper, , |
CsvHelper, \|\| |
|---|---|---|---|
| PackageAssets | Rows | 2.06 ms | 3.82 ms |
| PackageAssets | Strings | 7.65 ms | 9.29 ms |
| Quoted | Rows | 1.52 ms | 2.51 ms |
| Quoted | Strings | 4.97 ms | 6.08 ms |
The stream alone replaces separators at 9.6 GB/s on PackageAssets and 6.9 GB/s on the Quoted data with AVX-512 VBMI2 (one VPCOMPRESSB per 64-byte block), and at 5.8 and 4.6 GB/s with the code for AVX2 and SSSE3 on the same machine (a byte shuffle per 8 bytes, which for a separator of two bytes also writes the replacement). It allocates about 300 bytes per stream.
Dynamic records
The PackageAssets data read as dynamic records, without a record type (see
Dynamic records). Dynamic reads 3 of the 25 fields of every record by name
(record.Id, record.Version, record.FileName); DynamicToList keeps every record in a list. They are compared
with the usual way to get dynamic records: an ExpandoObject per record, with a string per field added by header name
(filled by CsvHelper's reader, so the difference is the record).
| Scenario | CsvHelper | ExpandoObject |
CsvHelper vs ExpandoObject |
|---|---|---|---|
| Dynamic | 5.05 ms | 82.1 ms | 16.3x faster |
Dynamic, from a byte[] |
3.87 ms | ||
| DynamicToList | 5.41 ms | 119 ms | 22.0x faster |
CsvHelper reads dynamic records at 3.2 GB/s (4.1 GB/s from a byte[]). It allocates 29.7 MB per run in Dynamic
(14.3 MB from a byte[], whose records reference the input in place; most of the rest are the 150,000 strings of the
fields read), against 136 MB for the ExpandoObjects. Kept in a list, its 50,000 records take 23.6 MB and the
ExpandoObjects 137 MB. The ExpandoObject times vary more than CsvHelper's (a standard deviation of 9 ms in
Dynamic).
Writing on several threads
ParallelWriteRecords against WriteRecords on the same [CsvRecord] objects (see
Writing from several threads), with
byte-identical output. Unlike the other benchmarks, these run on all 32 threads of the same machine (16 cores). The
data are the PackageAssets records, the Floats records (objects with 25 float fields) and Orders (9.5 MB): 12
fields of dates, a decimal, a Guid, numbers and text. 50,000 records each. The 4, 8 and 16 thread columns set
maxDegreeOfParallelism; 32 threads is the default, one per processor.
| Data | WriteRecords |
4 threads | 8 threads | 16 threads | 32 threads |
|---|---|---|---|---|---|
| PackageAssets | 8.23 ms | 2.84 ms (2.9x) | 1.71 ms (4.8x) | 1.84 ms (4.5x) | 1.32 ms (6.2x) |
| Floats | 23.6 ms | 7.16 ms (3.3x) | 3.97 ms (5.9x) | 2.31 ms (10.2x) | 1.62 ms (14.6x) |
| Orders | 6.50 ms | 2.29 ms (2.8x) | 1.29 ms (5.1x) | 0.90 ms (7.2x) | 0.85 ms (7.7x) |
To a file (CsvWriter.ToFile, on a WD Blue SN5000 NVMe drive), with 32 threads:
| Data | WriteRecords |
ParallelWriteRecords |
|---|---|---|
| PackageAssets | 12.0 ms | 5.59 ms (2.2x) |
| Floats | 28.2 ms | 5.13 ms (5.5x) |
| Orders | 8.78 ms | 3.54 ms (2.5x) |
To memory, the parallel writer reaches 12.1 GB/s on PackageAssets, 9.1 GB/s on Floats and 11.3 GB/s on Orders. To a
file, all three end up at 2.7 to 2.9 GB/s: writing the file is then the limit, so the records that cost the most to
format gain the most. A call allocates about 42 KB with 32 workers (their writers and output buffers), against 416
bytes for WriteRecords.
Run them with:
cd bench/CsvHelper.Benchmarks
dotnet run -c Release -- --filter *PackageAssetsRead.* *PackageAssetsReadFromString.* *PackageAssetsReadFile.* *FloatsRead.* *QuotedRead.* *PackageAssetsWrite.* *FloatsWrite.* *QuotedWrite.*
dotnet run -c Release -- --filter *.EscapeRead.* *.NoEscapeRead.* *.FixedWidthRead.* *.EscapeWrite.* *.NoEscapeWrite.* *.FixedWidthWrite.*
dotnet run -c Release -- --filter *SeparatorPackageAssetsRead.* *SeparatorQuotedRead.* --anyCategories Rows Strings
dotnet run -c Release -- --filter *ParallelWrite.* # on every core
dotnet run -c Release -- verify # check that all libraries return the same results
dotnet run -c Release -- quick QuotedRead # quick Stopwatch runner for tuning (not for published numbers)