Vorticity.RowEncoding 0.6.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Vorticity.RowEncoding --version 0.6.0
                    
NuGet\Install-Package Vorticity.RowEncoding -Version 0.6.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Vorticity.RowEncoding" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Vorticity.RowEncoding" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="Vorticity.RowEncoding" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Vorticity.RowEncoding --version 0.6.0
                    
#r "nuget: Vorticity.RowEncoding, 0.6.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Vorticity.RowEncoding@0.6.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Vorticity.RowEncoding&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=Vorticity.RowEncoding&version=0.6.0
                    
Install as a Cake Tool

Vorticity

Vorticity is a pure .NET implementation of the Vortex columnar file format, with no third-party dependency. It reads and writes Vortex files faster than Vortex's own Rust implementation, and its group bys run faster than DuckDB's.

Performance

Each figure below is a speedup: the other side's time divided by Vorticity's, so anything above 1.00x means Vorticity is faster. Both sides run as separate processes on the same machine, an Apple M4 Pro, are compiled ahead of time, and do the same work.

Against Vortex's Rust implementation, Vorticity wins every scenario, often by a factor of two. On a table of four columns and a million rows:

scenario speedup, one core speedup, 14 cores
read every column 1.99x 2.48x
read one column of four 2.31x 2.51x
filter, 1 % of the rows 1.45x 3.01x
filter, half the rows 2.08x 2.37x
take 1,000 scattered rows 1.64x 1.11x
write the table back out 2.37x 1.35x

Against DuckDB, Vorticity's group bys win all 13 queries we measured, on the same files and with the same number of threads:

DuckDB reading speedup, one thread speedup, 14 threads
its own table, loaded beforehand 1.43x to 6.15x 1.18x to 3.50x
the file, through its Vortex extension 1.58x to 5.65x 1.39x to 4.47x

The way these numbers are produced is described below, and every figure is on the pages against Rust, against DuckDB and on x64.

Features

  • Columns are declared by a [VortexRecord] type, and filters and aggregates are plain C# lambdas that the scan pushes down into the file. A filter that cannot be pushed down does not compile.
  • The core is built for throughput. It allocates next to nothing per batch on the hot paths, uses SIMD through System.Runtime.Intrinsics, and never copies data before decoding it: a mapped file is decoded where it lies, and an uncompressed column is a view of its bytes.
  • The API is async only. Every read and every write is awaited (ValueTask, await foreach), whatever the source, so there is no synchronous path to pick by mistake.
  • Files follow the published spec, VTXF version 1 up to the core2026.08.3 edition, and every file Vorticity writes is read back by the Rust reference in CI.

Beyond the base class library, the core references only System.IO.Hashing, a first-party package used for the write path's hashes and for Bloom filters, and two managed codecs from ManagedCompressions, a sibling project: ManagedCompressions.Zstd, which handles vortex.zstd and Parquet's ZSTD, and ManagedCompressions.Lz4, which handles Parquet's LZ4_RAW.

Parity with Rust means that Rust reads every file Vorticity writes and every value reads back equal. It does not mean byte parity. The two writers choose their encodings differently by design, so the same rows produce different bytes on each side.

The project is developed with the help of LLMs, and its API may still change in breaking ways before v1.0.

A first look

A record describes the columns of a file. It is a schema rather than a row: each member is a column, in declaration order and under its own name.

using Vorticity;

[VortexRecord]
public partial record struct Reading(int Day, double? Celsius, string City);

Writing records out and completing the file:

await using (VortexFileWriter writer = VortexSession.Default.CreateWriter<Reading>("readings.vortex"))
{
    await writer.WriteAsync<Reading>(readings);          // Reading[]: copied into the builder, column by column
    await writer.CompleteAsync();                        // disposed without it, the file is deleted
}

Reading the columns back, each batch is borrowed: its columns are ref structs that stay valid until the next batch, and the loop allocates next to nothing per batch.

await using VortexFile file = await VortexFile.OpenAsync("readings.vortex");

long hot = 0;
await foreach (var (_, celsius, _) in file.Scan<Reading>().Where(r => r.Day >= 900 && r.City == "Paris"))
{
    ReadOnlySpan<double> values = celsius.Values;        // the decoded buffer, no copy
    for (int i = 0; i < values.Length; i++)
    {
        if (celsius.IsValid(i) && values[i] > 30.0) hot++;
    }
}

The lambda given to Where runs once, when the scan is built, and what it records becomes the plan. The file statistics, the zone maps and the indexes then skip whatever they can before a block is decoded. Expressions such as r.Day >= "900" or r.Day % 2 == 0 do not compile.

The same question, and a few more, can be asked without a loop. The answer comes from the statistics when they settle it, from the encoded blocks otherwise, and never from a materialised batch.

long hotDays = await file.Scan<Reading>()
    .Where(r => r.Day >= 900 && r.City == "Paris" && r.Celsius > 30.0)
    .CountAsync();

double? mean = await file.Scan<Reading>().AverageAsync(r => r.Celsius);

Spread spread = await file.Scan<Reading>()
    .Where(r => r.Day >= 900)
    .AggregateAsync<Spread>(a => (a.Min(r => r.Celsius), a.Max(r => r.Celsius), a.CountDistinct(r => r.City)));

[VortexRecord]
public partial record struct Spread(double? Min, double? Max, long Cities);   // several answers go into a record

A group by is a query too, and its result is a scan of a record, read in batches like a file:

await foreach (var (city, readings, mean) in file.Scan<Reading>()
    .GroupBy(r => r.City)
    .Select(g => (g.Key, g.Count(), g.Average(r => r.Celsius)))
    .As<CityMean>())
{
    ReadOnlySpan<long> counts = readings.Values;          // one value per city, no copy
}

[VortexRecord]
public partial record struct CityMean(string City, long Readings, double? Mean);

When rows are really what you need, ask for them and pay for them: ToRecordsAsync() yields one Reading per row, and LINQ takes over from there. A file whose schema is unknown at compile time is read by column name, file.Scan("Day", "City").Where($"Day >= {min}"), with each hole of the filter passed as a typed value. That is the surface vxdump is built on.

Requirements

The library targets net11.0, and global.json pins the SDK to a .NET 11 release candidate with allowPrerelease. Until .NET 11 is generally available you need that SDK installed: a machine with only .NET 10 is refused by global.json before anything builds. That SDK is all dotnet build and dotnet test need. Comparing against the Rust reference (the cross-check, the ratio gates of the bench and the corpus generator) also requires a Rust toolchain.

The library ships on nuget.org for projects that target net11.0. The generator is a build-time dependency, and PrivateAssets keeps it out of whatever your project packs:

<PackageReference Include="Vorticity" Version="0.5.1" />
<PackageReference Include="Vorticity.Generators" Version="0.5.1" PrivateAssets="all" />

Vorticity.Dataset and Vorticity.RowEncoding are both experimental. They are referenced the same way, at the same version as the core.

Documentation

document contents
docs/guide/ how to use the library: pages named after what you want to do, each backed by a program the samples project compiles and runs
docs/guide/benchmarks.md every figure the bench publishes: Vorticity against the Rust implementation, and each kernel against its baseline
docs/guide/benchmarks-x64.md the same page measured on an x64 machine, a Zen 4 processor under Windows, against a Rust built there
docs/guide/benchmarks-duckdb.md our group bys against DuckDB's on the same files, through its Vortex reader and from its own table, on one thread and on fourteen
docs/design/ why the library is shaped this way: scope, byte layout, engine, public API

How performance is measured

Vorticity is measured against Vortex's Rust implementation on the same files and the same machine, both built with Native AOT, on one core and on all of them. The bench covers a table read and written, every encoding decoded, taken from and written, and each hot loop against its baseline. Both sides do the same work: each maps the file and reads it where it lies, decodes every value it returns, and hands what it writes to a sink that keeps nothing. On one core, Rust is timed with the faster of its two ways of splitting a scan. The speedup is Rust's time over Vorticity's, so above 1.00x Vorticity is faster. The table at the top of this page reads the file Vorticity writes.

Per encoding, Vorticity is well ahead on nested, text and compressed integer columns (struct 6.92x, varbin 6.08x, map 3.28x, pco 3.06x). It is level with Rust or behind by at most a tenth on 10 files: plain booleans (0.90x), onpair (0.95x), decimal (0.96x), masked columns (0.96x to 1.00x), patched bit-packed integers (0.98x), alprd and sequence (1.00x). Its files are at most 5 % larger than Rust's, or smaller, on 44 of 56 encodings. With both readers warmed up in one process, Rust is faster on 1 case out of 19, a prefix filter on FSST strings (0.93x).

Every figure is on the benchmark page, each section with the machine and the commit it was measured on, and its x64 twin holds the same sections measured on x64. docs/design/05-benchmarks.md explains what is compared and how, and bench/README.md how to run each instrument. bench/gate.sh runs everything that gates a change before it is pushed.

Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Vorticity.RowEncoding:

Package Downloads
Vorticity.Dataset

A versioned dataset over an object store: immutable data and commit objects, a prolly tree of leaves, and the store seam an S3 library implements. The format is experimental and this repository is the first to write it.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.7.1 45 10/17/2026
0.7.0 56 10/16/2026
0.6.0 61 10/11/2026
0.5.1 90 10/9/2026
0.5.0 87 10/9/2026
0.4.1 97 10/4/2026
0.4.0 98 10/4/2026
0.3.0 95 10/2/2026
0.2.0 99 9/30/2026
0.1.0 96 9/29/2026