01 · The race
One kernel at a time
Each piece of work below has its own race, what it measures, what neo is held to, how Zig does it and every number taken for it. In a race both ships run for the same stretch: Zig completes the work once, and neo completes it as many times as the measured ratio says, turning around at each end.
- 01.1 · load the CSV, parse and type1.39×
- 01.2 · filter amount > 500, count1.67×
- 01.3 · sort, as a permutation1.34×
- 01.4 · sort into a new table1.46×
- 01.5 · sum by region1.20×
01.1 · kernel · 1,000,000 rows
load the CSV, parse and type
neo 2.89 ms · Zig 4.03 ms · 1.39× neo. Against every engine →
What this measures
Read a 46 MB CSV of a million rows and six columns into a table: split the bytes into fields, parse the amount column into numbers, and dictionary-encode the region column. Each engine's typed read, best of two, with the first read of a fresh process kept separately.
What neo is held to
The same table the app loads, with the amount column typed and the region column encoded at load, so that no kernel after it touches text. Before a time is taken every engine prints its table as one number and its checks, and the line under the table says whether they agreed.
How Zig does it
Written in-house to beat the C++, on every core, the better of sixteen and eighteen threads; what it did better went back into the engine. It is part of the same body of work and stays with it. How it was timed and checked is beside every number below.
neo's side is confidential. What it is held to is above; why it is kept is under 03.
Every number taken for it
| measurement | neo | Zig | × neo |
|---|
| time, msThree loads to warm, then three timed whole loads; the median over every timed load of seven shuffled rounds. | 2.89 | 4.03 | 1.39× |
Read it with this
- neo's load does more than a port's, so that nothing after it touches text; the load multiple carries that work.
01.2 · kernel · 1,000,000 rows
filter amount > 500, count
neo 0.018 ms · Zig 0.030 ms · 1.67× neo. Against every engine →
What this measures
Count the rows whose amount is over 500: a million comparisons on a typed column, the answer one integer. Each library's count is written the way its users write it.
What neo is held to
The count every other engine prints, under the JavaScript node's rule that NaN never matches. What comes out is a selection the next kernel reads from, not a copy of the rows.
How Zig does it
Written in-house to beat the C++, on every core, the better of sixteen and eighteen threads; what it did better went back into the engine. It is part of the same body of work and stays with it. How it was timed and checked is beside every number below.
neo's side is confidential. What it is held to is above; why it is kept is under 03.
Every number taken for it
| measurement | neo | Zig | × neo |
|---|
| time, msBest of three in one process, then the median over seven rounds, every engine run once a round in a shuffled order. | 0.018 | 0.030 | 1.67× |
Read it with this
- The two results differ in form: the port's is a list of row numbers, neo's a selection the next kernel reads from. The count checked is the same.
01.3 · kernel · 1,000,000 rows
sort, as a permutation
neo 1.04 ms · Zig 1.39 ms · 1.34× neo. Against every engine →
What this measures
Produce the stable ascending order of the amount column as a permutation of row indices, without moving any other column. SQL engines have no such form, so DuckDB and Spark sit this one out.
What neo is held to
A stable ascending order under the JavaScript rule that NaN sorts first and ties keep their place. The first sorted values are printed and compared with every other engine's.
How Zig does it
Written in-house to beat the C++, on every core, the better of sixteen and eighteen threads; what it did better went back into the engine. It is part of the same body of work and stays with it. How it was timed and checked is beside every number below.
neo's side is confidential. What it is held to is above; why it is kept is under 03.
Every number taken for it
| measurement | neo | Zig | × neo |
|---|
| time, msBest of three in one process, then the median over seven rounds, every engine run once a round in a shuffled order. | 1.04 | 1.39 | 1.34× |
01.4 · kernel · 1,000,000 rows
sort into a new table
neo 2.85 ms · Zig 4.17 ms · 1.46× neo. Against every engine →
What this measures
Sort by amount and build the whole sorted table, every column reordered.
What neo is held to
The whole table in that order, every column, with the same first values as every other engine.
How Zig does it
Written in-house to beat the C++, on every core, the better of sixteen and eighteen threads; what it did better went back into the engine. It is part of the same body of work and stays with it. How it was timed and checked is beside every number below.
neo's side is confidential. What it is held to is above; why it is kept is under 03.
Every number taken for it
| measurement | neo | Zig | × neo |
|---|
| time, msBest of three in one process, then the median over seven rounds, every engine run once a round in a shuffled order. | 2.85 | 4.17 | 1.46× |
01.5 · kernel · 1,000,000 rows
sum by region
neo 0.020 ms · Zig 0.024 ms · 1.20× neo. Against every engine →
What this measures
Total the amount column for each of five regions: a grouped sum, the kernel every dashboard runs.
What neo is held to
The JavaScript loop's result to the bit. A floating-point sum depends on the order it is taken in, and neo is held to the row order's answer whatever it does to get there.
How Zig does it
Written in-house to beat the C++, on every core, the better of sixteen and eighteen threads; what it did better went back into the engine. It is part of the same body of work and stays with it. How it was timed and checked is beside every number below.
neo's side is confidential. What it is held to is above; why it is kept is under 03.
Every number taken for it
| measurement | neo | Zig | × neo |
|---|
| time, msBest of three in one process, then the median over seven rounds, every engine run once a round in a shuffled order. | 0.020 | 0.024 | 1.20× |
02 · Every number
The same kernels, in other languages
1,000,000 rows · ms, lower is better · 2026-10-15 08:22
| kernel | neo | Zig | × neo |
|---|
| load (parse + type + dictionary) | 2.89 | 4.03 | 1.39× |
| filter amount > 500 | 0.018 | 0.030 | 1.67× |
| sort, as a permutation | 1.04 | 1.39 | 1.34× |
| sort into a new table | 2.85 | 4.17 | 1.46× |
| sum by region | 0.020 | 0.024 | 1.20× |
The kernels were ported to Go, Rust, Swift and Zig, each told to beat the C++; what they did better was then taken into the C++. Milliseconds, 1,000,000 rows. Where a port is still ahead, it is shown ahead. These are the million-row figures of the study in the next card, the same record: A round runs every engine once, in an order shuffled each round, and a figure is the median over the rounds (7 at 1,000,000 rows, 5 at 10,000,000 and 3 at 100,000,000). The load is one loop in every binary: three loads first, then three timed whole loads, the previous result still alive during each; its figure is the median over every timed load of every round. Filter, sort and sum are each binary's best of three in a process, and the figure the median of those. Threads: a port is run on 16 and 18 (every core) and its figure for a kernel is the better of the two; neo runs as it ships, on sixteen, and on every core for a load of 256 MB and more. Before a time is taken every engine prints its table as one number, an FNV over every cell, and after it its checks; the line under the table says whether they agreed. What still differs: a port is a benchmark kernel written for this comparison, not a library; each is built as its own run.sh builds it — the Rust and Zig ports for this processor, the Rust port with link-time optimisation, the Swift port with its checks off — and neo at -O3 with none of that; a port builds one dictionary where neo builds three; neo's filter figure is a bitmap where a port's is a list of row numbers; and the file is one synthetic shape (six columns, no field over sixteen bytes).
The same kernels, at a hundred million rows
100,000,000 rows · ms, lower is better · 2026-10-15 08:50
| kernel | neo | Zig | × neo |
|---|
| load (parse + type + dictionary) | 326 | 416 | 1.28× |
| first load in a fresh process (median of 3) | 410 | 469 | 1.14× |
| filter amount > 500 | 3.00 | 6.84 | 2.28× |
| sort, as a permutation | 101 | 151 | 1.49× |
| sort into a new table | 759 | 1,238 | 1.63× |
| sum by region | 4.20 | 4.96 | 1.18× |
| all five, end to end | 1,194 | 1,817 | 1.52× |
The same kernels in neo's C++ and in the four ports, Go, Rust, Swift and Zig, in one study, from a million rows to 100,000,000; beyond that neo alone, up to a billion rows (a 49 GB file), which a port's suite does not fit in memory. Milliseconds. A round runs every engine once, in an order shuffled each round, and a figure is the median over the rounds (7 at 1,000,000 rows, 5 at 10,000,000 and 3 at 100,000,000). The load is one loop in every binary: three loads first, then three timed whole loads, the previous result still alive during each; its figure is the median over every timed load of every round. Filter, sort and sum are each binary's best of three in a process, and the figure the median of those. Threads: a port is run on 16 and 18 (every core) and its figure for a kernel is the better of the two; neo runs as it ships, on sixteen, and on every core for a load of 256 MB and more. Before a time is taken every engine prints its table as one number, an FNV over every cell, and after it its checks; the line under the table says whether they agreed. The row under the load is the first load a fresh process makes, one a round. The last row is the five added up: neo builds dictionaries while it loads, which the load row pays for and the sort into a new table gets back. Every size shows them all at once: neo's time in each cell, and under it the quickest port there. What still differs: a port is a benchmark kernel written for this comparison, not a library; each is built as its own run.sh builds it — the Rust and Zig ports for this processor, the Rust port with link-time optimisation, the Swift port with its checks off — and neo at -O3 with none of that; a port builds one dictionary where neo builds three; neo's filter figure is a bitmap where a port's is a list of row numbers; and the file is one synthetic shape (six columns, no field over sixteen bytes).
03 · Both sides
Zig's side
Zig is a small systems language with manual memory management, no hidden control flow and a reputation for tight binaries. The port runs on every core. The port was written in-house to beat the C++ and given the C++ numbers to beat; what it did better went back into the engine. It is part of the same body of work and stays with it. How it was built, run, timed and checked is under 04, and every number it produced is above.
neo's side: confidential
neo's kernels, the ports written in-house to beat them, and the techniques behind both stay with the engine. Nothing about how the numbers were made is withheld: the method, the checks every engine had to pass, every number and every place neo is behind are on this page. The engine itself we keep confidential, because we don't want it getting into the wrong hands. The binaries run on request and reproduce every number here.