Sep .NET Library

repository·main·Indexed 23 days ago

https://github.com/nietras/sep

A high-performance, zero-allocation .NET library for reading and writing separated values (CSV, TSV, etc.), optimized for machine learning workloads. Built for .NET 7+ and C# 11+, it utilizes SIMD vectorization, csFastFloat, and Span<T> to ensure memory efficiency and speed. The library is fully trimmable and AOT/NativeAOT compatible, featuring a fluent API for reading and writing data with support for parallel enumeration and ValueTask-based async/await (C# 13.0+).

Tokens
10.6K
Snippets
13
Records
34
Agent score
31%

What's inside Sep

  1. Overview of Sep CSV Parser

    main

    Sep is a high-performance, zero-allocation .NET library for reading and writing separated values (CSV, TSV, etc.). It is designed for modern .NET (7+) and C# (11+) environments, specifically targeting machine learning use cases where speed and memory efficiency are critical.

    Key features include:

    • High Performance: Uses SIMD vectorization (AVX2, AVX-512, NEON) and csFastFloat for rapid parsing.
    • Zero Allocation: Intelligent memory management allows for zero allocations after warmup.
    • Modern .NET: Built using Span<T>, ref struct, ArrayPool<T>, and Generic Math (ISpanParsable<T>).
    • Concurrency: Supports highly efficient parallel parsing and ValueTask-based async/await support (requires C# 13.0+).
    • Compatibility: Fully trimmable and AOT/NativeAOT compatible with no reflection or dynamic code generation.
  2. Design philosophy of the Sep CSV Writer

    main

    Unlike many high-performance parsers that prioritize raw speed by writing columns directly upon definition, Sep's writer is designed for convenience and flexibility.

    Key characteristics:

    • Deferred Writing: Sep does not require the header to be defined upfront, nor does it require strict alignment between header column order and row value column order.
    • Row-Based Definition: Instead of writing columns directly, Sep defers the writing process until a new row has been fully defined and ended. This allows for more flexible data manipulation during the writing process at the cost of some raw throughput compared to specialized direct-write parsers.
  3. Performance advantages of Sep over Sylvan and CsvHelper

    main

    Sep provides significant performance improvements over other common .NET CSV parsers:

    • Vs Sylvan: Sep is >2x faster due to its integrated use of csFastFloat for high-speed floating-point parsing. When using the multi-threaded ParallelEnumerate implementation, Sep can be up to 23x faster than Sylvan.
    • Vs CsvHelper: Sep is >4x faster than CsvHelper in standard usage and up to 35x faster when using ParallelEnumerate. This is largely because CsvHelper must allocate a string for every column in every row, whereas Sep avoids these allocations.
    • Multi-threading: The ParallelEnumerate implementation provides substantial speedups even for relatively small files, as it efficiently utilizes multiple cores for parsing.
  4. The Sep type and namespace

    main

    In the nietras.SeparatedValues namespace, Sep is the primary entry point for the library. It is a readonly record struct that holds a validated char separator.

    Key characteristics:

    • The separator is validated upon construction to ensure it is within a supported range and is not a character like a quote (").
    • Sep can be used as a static entry point to build readers or writers.
    • If you need the separator to be inferred from the first row of a file, use Sep.Auto (which returns null).
    public readonly record struct Sep(char Separator);
  5. Sep's Approach to RFC-4180

    main

    Sep takes a pragmatic approach to CSV parsing, prioritizing performance and flexibility for use cases like machine learning pipelines over strict adherence to [RFC-4180].

    Key Differences from RFC-4180:

    • Line Endings: While RFC-4180 requires \r\n (CRLF), Sep supports \r\n, \n, and \r (similar to .NET). Environment.NewLine is used when writing.
    • Separators: RFC-4180 specifies only a comma (,) as a separator. Sep defaults to a semicolon (;) when writing and auto-detects supported separators when reading.
    • Quoting: RFC-4180 is strict about quoting. Sep supports matching pairs of quotes regardless of where they appear in a field, and allows characters like emojis which are restricted in the RFC's ABNF grammar.
    • Escaping: In escaped fields, Sep allows any pair of quotes, whereas the RFC specifically requires double-quotes ("") to escape a quote.

    Sep is designed for speed and real-world data rather than strict internet data exchange compliance.

  6. Understand Sep's parsing scopes and performance characteristics

    main

    Sep's performance can be evaluated across three distinct scopes, which help identify whether the bottleneck is the parsing logic or the data access/allocation:

    1. Row: Only the rows are enumerated (e.g., foreach (var row in reader) { }). This captures the cost of parsing both rows and columns without actually accessing the column data.
    2. Cols: Both rows and columns are enumerated. In Sep, this typically involves accessing columns as spans (e.g., var span = row[i].Span;). This measures the cost of column access.
    3. XYZ (Full Scope): The complete scenario is performed, such as parsing columns into specific class properties or strings.

    Additionally, Sep supports:

    • Multi-threaded parsing: Available via ParallelEnumerate (indicated by _MT in benchmarks).
    • Asynchronous support: Benchmarked with _Async to measure the overhead of the async code path.
  7. Efficiently parse multiple columns without repeated allocations

    main
    Sep is designed to handle multiple column access with minimal overhead. When parsing multiple columns, Sep internally manages a pool of arrays and returns Span<T> for them. This prevents repeated allocations of parsed types (like floating-point numbers) during row enumeration, making it significantly more efficient than libraries that allocate a new string for every column in every row (like CsvHelper).
  8. Efficiently enumerate rows with Enumerate and ParallelEnumerate

    main

    Because SepReader is not LINQ-compatible due to its ref struct design, Sep provides specialized extension methods for efficient enumeration.

    Enumerate: Converts the reader into an IEnumerable<T> by parsing rows into a target type T. This is the recommended way to bridge the gap between the high-performance ref struct API and standard LINQ/collection usage.

    ParallelEnumerate: Provides multi-threaded enumeration using a batching strategy. It is built on top of AsParallel().AsOrdered().

    Best Practices:

    • Use ParallelEnumerate for parsing: It is optimized for the CPU-intensive task of turning raw text into typed data.
    • Avoid expensive operations inside the delegate: If your per-row operation (like loading an image) takes more than 1ms, perform that work after the enumeration using standard LINQ AsParallel().
    • Avoid LINQ .Where on the result of Enumerate if possible: Filtering after parsing can be inefficient if you are parsing many rows that will be discarded. Instead, use the Enumerate overload that accepts a RowTryFunc<T> to filter and parse in a single step.
    // Efficiently filter and parse in one step using RowTryFunc
    var actual = reader.Enumerate((SepReader.Row row, out (string Key, double Value) kv) =>
    {
        var keyCol = row["Key"];
        if (keyCol.Span.StartsWith("B"))
        {
            kv = (keyCol.ToString(), row["Value"].Parse<double>());
            return true;
        }
        kv = default;
        return false;
    }).ToArray();
    
    // Using ParallelEnumerate for parsing, then AsParallel for expensive work
    var results = reader.ParallelEnumerate(ParseRow)
                        .AsParallel().AsOrdered()
                        .Select(LoadData) // Expensive load happens here
                        .ToList();
  9. Sep Naming and Terminology

    main

    Sep uses specific terminology tailored for machine learning and high-performance data processing. Understanding these terms is essential for using the API correctly:

    TermDescription
    SepShort for separator (also called delimiter). E.g., a comma (,) in a CSV.
    HeaderThe optional first row defining column names.
    RowA collection of columns (also called a record). A row may span multiple lines.
    ColShort for column (also called a field).
    LineA horizontal set of characters ending with \r\n, \r, or \n.
    Index0-based indexing. RowIndex 0 is the first row (or the header if present).
    Number1-based indexing. LineNumber 1 is the first line. Since rows can span multiple lines, a row has a From line number and a ToExcl line number (using C# range syntax [From..ToExcl]).
  10. Understand the Sep API pattern

    main

    The library follows a fluent API pattern where each step flows into the next:

    Reading Pattern: Sep/Spec $\rightarrow$ SepReaderOptions $\rightarrow$ SepReader $\rightarrow$ Row $\rightarrow$ Col(s) $\rightarrow$ Span/ToString/Parse

    Writing Pattern: Sep/Spec $\rightarrow$ SepWriterOptions $\rightarrow$ SepWriter $\rightarrow$ Row $\rightarrow$ Col(s) $\rightarrow$ Set/Format

    Important Performance Note: SepReader.Row and SepReader.Col are ref structs. They act as lightweight facades pointing to internal state to avoid allocations. Because they are ref structs:

    • You cannot use LINQ directly on the reader (e.g., reader.ToArray() is not possible).
    • You cannot store rows in arrays or lists.
    • To use LINQ, you must first parse/transform the rows into standard types (e.g., using yield return in an iterator or the provided Enumerate methods).
  11. Use asynchronous reading and writing with Sep

    main

    Sep provides efficient asynchronous support using ValueTask.

    Requirements

    Because SepReader.Row and SepWriter.Row are ref structs, using async/await requires C# 13.0+ (to support "ref and unsafe in iterators and async methods") and .NET 9.0+ (for IAsyncEnumerable<SepReader.Row> support via the allows ref struct annotation).

    Usage Patterns

    • SepReader: Creation methods like FromTextAsync are asynchronous because the reader must parse the first row to determine the separator and header columns. Use await when creating the reader. To iterate asynchronously, use await foreach.
    • SepWriter: Creation methods (e.g., ToText) are synchronous. However, because disposing a row may involve flushing data to the underlying TextWriter, you must use await using for the writer and the rows.
    • Cancellation: Many methods, including From*Async and NewRow, accept a CancellationToken to support cancellation.
    • Context Configuration: Both SepReaderOptions and SepWriterOptions include an AsyncContinueOnCapturedContext boolean option, which controls ConfigureAwait behavior.
    var text = """
               A;B;C;D;E;F
               Sep;🚀;1;1.2;0.1;0.5
               CSV;✅;2;2.2;0.2;1.5
               
               """; // Empty line at end is for line ending
    
    using var reader = await Sep.Reader().FromTextAsync(text);
    await using var writer = reader.Spec.Writer().ToText();
    await foreach (var readRow in reader)
    {
        await using var writeRow = writer.NewRow(readRow);
    }
    Assert.AreEqual(text, writer.ToString());
  12. Initialize a SepWriter with automatic or manual separator

    main

    You can create a SepWriter to output data to various destinations.

    Note: Unlike the reader, the writer cannot infer the separator; you must specify it.

    Usage: Use Sep.Writer() or Sep.New(char).Writer() followed by a destination method like .ToFile(path) or .ToText().

    Transferring settings: If you have an existing SepReader, you can use its Spec property to ensure the writer uses the same separator and CultureInfo:

    using var reader = Sep.Reader().FromFile("titanic.csv");
    using var writer = reader.Spec.Writer().ToFile("titanic-survivors.csv");