Chart · milliseconds

sum by region.

neo 0.020 ms in the language study, 0.020 ms in the library run. Closest: Zig at 1.40×. Farthest: Spark at 1,948×.

—

neo crosses once

neo

Every engine's multiple of neo

  1. neo
    1×
  2. Zig
    1.40× · 0.028 vs 0.020
  3. Swift
    1.85× · 0.037 vs 0.020
  4. Rust
    2.20× · 0.044 vs 0.020
  5. Go
    4.45× · 0.089 vs 0.020
  6. Polars
    77.3× · 1.55 vs 0.020
  7. DuckDB
    112× · 2.24 vs 0.020
  8. pandas
    612× · 12.2 vs 0.020
  9. Spark
    1,948× · 39.0 vs 0.020

Bars to scale, from zero to the slowest engine. The gray pair is the engine's measurement and neo's in the same run, in ms. The languages and the libraries were measured in separate runs, so neo has a number in each; every multiple is against neo in its own run.

What this measures

Total the amount column for each of five regions: a grouped sum, the kernel every dashboard runs.

Why neo is quick here

The region column was dictionary-encoded at load, so its ids are the group numbers and no string is hashed. The sum is taken on the pool in any order only when the column is whole numbers small enough that no addition can round; otherwise it is added in row order on one thread, because a floating-point sum depends on its order, and the contract is the JavaScript loop's result to the bit.

The code that ran, this kernel only

Zig1.40× neo · neo vs Zig

sum by region · neon-cpp/bench/lang/zig/main.zig:1515
fn aggMulti(pool: *Pool, amount: []const f64, ids: []const u32, ngroups: usize, partial: [][MAX_GROUPS]f64, out: []f64) void {
    const nk = partial.len;
    var j: AggJob = .{ .amount = amount, .ids = ids, .ngroups = ngroups, .partial = partial };
    pool.runChunks(&j, AggJob.run, nk);
    for (0..ngroups) |g| {
        var s: f64 = 0;
        for (0..nk) |k| s += partial[k][g]; // fixed chunk order -> deterministic sums
        out[g] = s;
    }
}

Swift1.85× neo · neo vs Swift

sum by region · neon-cpp/bench/lang/swift/main.swift:1125
func aggregateByRegion(_ ids: UnsafePointer<UInt32>, _ amt: UnsafePointer<Double>, _ n: Int, groups g: Int, pool: Pool?, parts: Int) -> [Double] {
    let np = (pool == nil || parts <= 1) ? 1 : max(1, min(parts, n / 4096))
    let chunk = (n + np - 1) / np
    let acc = alloc(np * 8 * g, Double.self)
    memset(acc, 0, np * 8 * g * 8)
    let body: (Int) -> Void = { p in aggRange(ids, amt, p * chunk, min(n, (p + 1) * chunk), groups: g, acc + p * 8 * g) }
    if let pool = pool, np > 1 { pool.run(np, body) } else { body(0) }
    var sums = [Double](repeating: 0, count: g)
    for p in 0..<np { for lane in 0..<8 { for k in 0..<g { sums[k] += acc[(p * 8 + lane) * g + k] } } }
    free(acc)
    return sums
}

Rust2.20× neo · neo vs Rust

sum by region · neon-cpp/bench/lang/rust/src/main.rs:1713
fn agg_mt(ids: &[u32], amt: &[f64], g: usize, pool: &Pool, parts: usize) -> Vec<f64> {
    let ranges = chunk_ranges(ids.len(), parts);
    let partials: Vec<Vec<f64>> = pool.map(parts, |p| {
        let (a, b) = ranges[p];
        agg_range(&ids[a..b], &amt[a..b], g)
    });
    let mut out = vec![0f64; g];
    for p in &partials {
        for j in 0..g {
            out[j] += p[j];
        }
    }
    out
}

Go4.45× neo · neo vs Go

sum by region · neon-cpp/bench/lang/go/main.go:1104
func aggregate(ids []uint32, amt []float64, groups int, p *pool) []float64 {
	n := len(ids)
	C := p.chunks()
	partial := make([]float64, C*groups)
	p.run(C, n, func(c, lo, hi int) {
		acc := make([]float64, 4*groups)
		a0, a1, a2, a3 := acc[:groups], acc[groups:2*groups], acc[2*groups:3*groups], acc[3*groups:]
		i := lo
		for ; i+4 <= hi; i += 4 {
			a0[ids[i]] += amt[i]
			a1[ids[i+1]] += amt[i+1]
			a2[ids[i+2]] += amt[i+2]
			a3[ids[i+3]] += amt[i+3]
		}
		for ; i < hi; i++ {
			a0[ids[i]] += amt[i]
		}
		out := partial[c*groups : (c+1)*groups]
		for g := 0; g < groups; g++ {
			out[g] = a0[g] + a1[g] + a2[g] + a3[g]
		}
	})
	sums := make([]float64, groups)
	for c := 0; c < C; c++ {
		for g := range sums {
			sums[g] += partial[c*groups+g]
		}
	}
	return sums
}

Polars77.3× neo · neo vs Polars

Polars, this kernel · neon-cpp/bench/lang/python/yardstick.py:218
timed("agg", lambda: df.group_by("region").agg(pl.col("amount").sum()))

DuckDB112× neo · neo vs DuckDB

DuckDB, this kernel · neon-cpp/bench/lang/python/yardstick.py:237
timed("agg", lambda: con.execute("SELECT region, sum(amount) FROM t GROUP BY region").fetchall())

pandas612× neo · neo vs pandas

pandas, this kernel · neon-cpp/bench/lang/python/yardstick.py:197
timed("agg", lambda: df.groupby("region")["amount"].sum())

Spark1,948× neo · neo vs Spark

Spark, this kernel · neon-cpp/bench/lang/python/yardstick.py:255
timed("agg", lambda: df.groupBy("region").agg(F.sum("amount")).collect())

neo

The kernel's body stays in the engine. Its declaration and the guarantees it is held to are below, with the harness line that calls it.

the harness, calling the engine · neon-cpp/bench/bench_engine_options.cpp
 194  l.t = csv::parse(raw);
 337  const double f5 = timeIt([&] { Bitmap b = compareDoubles(amt.data(), n, Cmp::Gt, 500, &pool); sink = b.words[0]; }, 7);
 372  const double s4 = timeIt([&] { sortIndexByDouble(amt.data(), n, perm, &pool); sink = perm[0]; }, 3);
 454  const double a5 = G <= 16 ? timeIt([&] { groupSumAnyOrder(amtP, idP, n, G, &pool, pairTotal); sink = static_cast<size_t>(pairTotal[0]); }, 9) : 0;
the group sum's contract · neon-cpp/core/groupsum.h:1
// groupsum.h — a typed column's sum, count and extremes per group: what the aggregate node's measures need
// once the column is doubles (toDoubles) and every row has a group number.
//
// The results are the ones a loop over the rows in order gives — `if (isFinite(v)) { sum += v; n++;
// if (v < min) min = v; if (v > max) max = v; }` a group, which is what the JavaScript does — to the bit:
//   • counts and extremes are taken a part of the rows at a time on the pool and the parts joined in row
//     order (a strict comparison keeps the earlier of two equal values, as the loop does);
//   • a sum is added in row order on one thread, because a floating-point sum depends on its order —
//     unless the column is whole numbers small enough that no addition can round (wholeNumbers below),
//     where every order gives the same bits and the rows are summed on the pool, two a step.
// bench/bench_agg_node.cpp times each path against the node and checks it; PHD.md §30.

// The sum alone, any order, two rows a step; for tests and the benchmark (groupStats calls it when it may).
// False, and nothing written, when there are more groups than it is built for.
bool groupSumAnyOrder(const double* x, const uint32_t* id, size_t n, size_t groups, Pool* pool, double* sums);

Measured on Apple M5 Max, 18 cores, 26.4.1, 2026-10-11 and 2026-10-12. The language study ran on a shared machine; its columns stand beside each other, but its times read high. Every number here is also on the comparison page of its engine, with the whole file it came from.

All evalsEvery comparisonStart a conversation