Benchmarks
This page contains benchmarks comparing the performance of FlameCsv with other popular CSV libraries.
The benchmarks use varied sample CSV datasets which can be downloaded from the repository. There are both x86 (AVX-2) and ARM64 (Neon) benchmarks, done on a desktop Ryzen machine and Mac Studio, respectively. x86 AVX-512 will be added if I can get my hands on a suitable CPU.
- Benchmark code: tools/Bench/Comparisons
- Data used: tools/Bench/Comparisons/Data
- Raw benchmark results: tools/Bench/BenchmarkResults
Details
- Memory allocations are on the right hand side of the bars in the charts.
- Parallel (multithreaded) versions are included when available, separated in the charts. Unordered processing is used where possible to get the best performance.
- "Reflection" and "SourceGen" refer to FlameCsv's type binding method.
- Some libraries lack async support for certain operations; these are absent in the charts.
Synchronous
Read objects
This benchmark enumerates 20,000 records (10 fields per record) from a stream wrapping a byte[].
- RecordParser uses hardcoded field indexes and manual parsing
- Sep uses manual parsing
Write objects
This benchmark writes the same 20,000 records from an array into a stream.
- Sep and RecordParser write objects manually
Enumerate all fields (unquoted)
This benchmark accesses all 14 fields from a 65,536 record CSV file, read from a stream wrapping a byte[].
Libraries that support accessing fields as a span into the underlying buffers are configured to do so.
Enumerate all fields (quoted)
This benchmark accesses all 12 fields from a 100,000 record CSV file, read from a stream wrapping a byte[].
Some of the fields require unescaping. Libraries that support accessing fields as a span into the underlying buffers are configured to do so.
Sum the value of one column
This benchmark parses a double from a single column of the 65,536 record dataset and sums the results.
The raw byte[] is passed to the libraries whenever possible. There is no async variant for this benchmark,
as the enumeration-benchmark above should already cover the differences.
Asynchronous
RecordParser doesn't support async, so it is absent from these results.
Read objects
Sep doesn't support asynchronous parallel.
Write objects
The async version of this benchmark uses a stream that always yields on async calls, to simulate real-world async I/O.
Enumerate all fields (unquoted)
Enumerate all fields (quoted)
Enums
FlameCsv provides a source generator for enum converters that generates highly optimized read/write operations specific to the enum.
Generating the enum converter at compile-time allows the enum to be analyzed, and specific optimizations to be made regarding different values and names. The generated converter especially excels at parsing numeric values from small enums that start from 0 without any gaps, or names that are ASCII-only. More complex configurations such as flags and non-ASCII display names are supported as well.
The benchmarks below are for the System.TypeCode-enum, in UTF8 (byte) and UTF16 (char).
Benchmarked on AMD Ryzen 7 3700X.
Parsing
The compares the throughput of Enum.TryParse, FlameCsv's reflection-based converter,
and the source-generated converter. Higher is better.
Click to view benchmark summary
| Parameter | Description |
|---|---|
| Bytes | Parsing from UTF8 (byte) or UTF16 (char) |
| IgnoreCase | Parsing is case-insensitive |
| ParseNumbers | Input is numeric and not enum names |
| Method | Bytes | IgnoreCase | ParseNumbers | Mean | StdDev | Ratio |
|---|---|---|---|---|---|---|
| TryParse | False | False | False | 582.33 ns | 3.088 ns | 1.00 |
| Reflection | False | False | False | 300.89 ns | 0.340 ns | 0.52 |
| SourceGen | False | False | False | 79.76 ns | 1.273 ns | 0.14 |
| TryParse | False | False | True | 185.49 ns | 2.101 ns | 1.00 |
| Reflection | False | False | True | 304.56 ns | 2.484 ns | 1.64 |
| SourceGen | False | False | True | 78.30 ns | 0.701 ns | 0.42 |
| TryParse | False | True | False | 661.59 ns | 6.298 ns | 1.00 |
| Reflection | False | True | False | 369.34 ns | 3.516 ns | 0.56 |
| SourceGen | False | True | False | 82.75 ns | 1.265 ns | 0.13 |
| TryParse | False | True | True | 186.26 ns | 1.584 ns | 1.00 |
| Reflection | False | True | True | 368.88 ns | 3.205 ns | 1.98 |
| SourceGen | False | True | True | 83.87 ns | 1.198 ns | 0.45 |
| TryParse | True | False | False | 726.99 ns | 15.936 ns | 1.00 |
| Reflection | True | False | False | 480.53 ns | 0.941 ns | 0.66 |
| SourceGen | True | False | False | 73.65 ns | 0.433 ns | 0.10 |
| TryParse | True | False | True | 326.83 ns | 0.540 ns | 1.00 |
| Reflection | True | False | True | 485.12 ns | 4.999 ns | 1.48 |
| SourceGen | True | False | True | 72.26 ns | 0.196 ns | 0.22 |
| TryParse | True | True | False | 785.22 ns | 1.791 ns | 1.00 |
| Reflection | True | True | False | 574.11 ns | 6.201 ns | 0.73 |
| SourceGen | True | True | False | 72.89 ns | 0.869 ns | 0.09 |
| TryParse | True | True | True | 327.22 ns | 3.023 ns | 1.00 |
| Reflection | True | True | True | 560.96 ns | 5.796 ns | 1.71 |
| SourceGen | True | True | True | 71.82 ns | 0.928 ns | 0.22 |
Formatting
The chart compares the throughput of Enum.TryFormat, FlameCsv's reflection-based converter,
and the source-generated converter. Higher is better.
Click to view benchmark summary
The table shows results for formatting directly using Enum.TryFormat, formatting using the reflection-based
converter in FlameCsv, and the source-generated converter.
| Parameter | Description |
|---|---|
| Numeric | Formatting as numbers and not enum names |
| Bytes | Formatting to UTF8 (byte) or UTF16 (char) |
| Method | Numeric | Bytes | Mean | StdDev | Ratio |
|---|---|---|---|---|---|
| TryFormat | False | False | 715.8 ns | 1.73 ns | 1.00 |
| Reflection | False | False | 275.2 ns | 1.63 ns | 0.38 |
| SourceGen | False | False | 188.4 ns | 0.27 ns | 0.26 |
| TryFormat | False | True | 1,296.3 ns | 1.33 ns | 1.00 |
| Reflection | False | True | 285.8 ns | 0.24 ns | 0.22 |
| SourceGen | False | True | 173.6 ns | 0.14 ns | 0.13 |
| TryFormat | True | False | 285.0 ns | 0.64 ns | 1.00 |
| Reflection | True | False | 298.5 ns | 0.24 ns | 1.05 |
| SourceGen | True | False | 151.6 ns | 0.43 ns | 0.53 |
| TryFormat | True | True | 861.6 ns | 0.81 ns | 1.00 |
| Reflection | True | True | 298.9 ns | 0.45 ns | 0.35 |
| SourceGen | True | True | 156.2 ns | 2.35 ns | 0.18 |
Why not NCsvPerf
While NCsvPerf benchmarks are commonly used for CSV library parsing comparisons, it has several limitations:
- String Conversion: All fields are converted to strings, which:
- Creates unnecessary transcoding and GC overhead
- Doesn't reflect modern libraries' ability to work with memory spans directly
- List Accumulation: Records are collected into a list before returning, which adds unnecessary overhead, and is not representative of how large datasets would be consumed
- Data Homogeneity: The test data lacks real-world complexity like quoted and escaped fields
NCsvPerf doesn't really stress the capabilities of modern CSV libraries effectively; with the speed of modern CSV libraries it's mostly a test of "how many strings can the .NET runtime create in a second".