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 float columns, 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 -- verify runs each benchmark once and prints these values.
  • Sep reads with Unescape = true in 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 with Escape = true, Sylvan with \r\n line endings.
  • File scenarios use each library's own file API (Sep.Reader().FromFile/FromFileAsync, Sylvan's Create/CreateAsync with a path, CsvReader.FromFile).
  • The job pins the process to one performance core. It sets DOTNET_TC_CallCountingDelayMs=0 so 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 (Substring for records), and PadRight per 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)