neo vs Swift · language

neo vs Swift.

The same input and the same kernels, in neo's C++ and in Swift. Across 5 measurements neo finished 1.85× faster in the middle and 2.76× at most; Swift was quicker on 1: load (parse + type + dictionary).

01 · The race

One kernel at a time

Each piece of work below has its own race, what it measures, how each side does it, the code that ran and every number taken for it. In a race both ships run for the same stretch: Swift completes the work once, and neo completes it as many times as the measured ratio says, turning around at each end.

  1. 01.1 · load the CSV, parse and type0.99×
  2. 01.2 · filter amount > 500, count2.76×
  3. 01.3 · sort, as a permutation1.18×
  4. 01.4 · sort into a new table2.73×
  5. 01.5 · sum by region1.85×

01.1 · kernel · 1,000,000 rows

load the CSV, parse and type

neo 3.59 ms · Swift 3.54 ms · 0.99× neo. Against every engine →

neo

Swift

0 runs

0 runs

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.

How neo does it

neo's parser finds the structural characters with SIMD, splits the file into blocks and parses them on every core above a megabyte, with results identical to the serial path. Typing and the dictionary happen once, at load, on the pool, so no kernel after it touches text. In the ledger's breakdown that is 2.8 ms of parse, 0.2 ms of typing and about nothing for the dictionary.

How Swift does it

The port's load is the whole program rather than one function, so it is not excerpted here: neon-cpp/bench/lang/swift/main.swift.

neo's side: the call and the contract

the harness, calling the engine · neon-cpp/bench/bench_engine_options.cpp:194
l.t = csv::parse(raw);
the parser's contract · neon-cpp/core/csv.h:55
Table parse(std::string_view raw);  // parallel above 1 MB, serial below; identical results

Every number taken for it

measurementneoSwift× neo
time, msThree loads to warm, then three timed whole loads; the median over every timed load of seven shuffled rounds.3.593.540.99×

Read it with this

  • Swift is ahead of neo on time, and is shown ahead.
  • A port builds one dictionary where neo builds three.
  • Taken on a shared machine. Both columns ran in the same shuffled rounds, so the multiple stands, but the times themselves read high.

01.2 · kernel · 1,000,000 rows

filter amount > 500, count

neo 0.017 ms · Swift 0.047 ms · 2.76× neo. Against every engine →

neo

Swift

0 runs

0 runs

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.

How neo does it

neo's filter produces a bitmap: one bit per row, 125 KB for a million rows, written at memory speed, with indices made only when something needs them. Below about a million values it runs on one thread, above that on the pool. The contract is the JavaScript node's: NaN never matches.

How Swift does it

Two passes on the pool. Each chunk counts its hits, a running total gives every chunk its own slice of the output, and each chunk then writes its row numbers into that slice.

filter amount > 500 · neon-cpp/bench/lang/swift/main.swift:816
/// Fills `out` (capacity n + 1: the branchless loop writes one slot past the last hit) with the selected row indices; returns the count.
func filterGt500(_ amt: UnsafePointer<Double>, _ n: Int, _ out: UnsafeMutablePointer<UInt32>, pool: Pool?, parts: Int) -> Int {
    guard let pool = pool, parts > 1 else { return filterRange(amt, 0, n, out) }
    let np = max(1, min(parts, n / 4096))
    let chunk = (n + np - 1) / np
    let counts = alloc(np + 1, Int.self), ends = alloc(np, Int.self)
    pool.run(np) { p in (counts[p], ends[p]) = countRange(amt, p * chunk, min(n, (p + 1) * chunk)) }   // pass 1: count
    var base = 0
    for p in 0..<np { let c = counts[p]; counts[p] = base; base += c }
    counts[np] = base
    pool.run(np) { p in _ = filterRange(amt, p * chunk, ends[p], out + counts[p]) }   // pass 2: write, stop after the last hit
    free(counts); free(ends)
    return base
}

neo's side: the call and the contract

the harness, calling the engine · neon-cpp/bench/bench_engine_options.cpp:337
const double f5 = timeIt([&] { Bitmap b = compareDoubles(amt.data(), n, Cmp::Gt, 500, &pool); sink = b.words[0]; }, 7);
the filter's contract · neon-cpp/core/selection.h:1
// selection.h — the result of a filter: one bit per row. 1M rows = 125 KB,
// written at memory speed; indices are produced only when something needs them.

struct Bitmap {
    std::vector<uint64_t> words;
    size_t bits = 0;
    bool test(size_t i) const { return (words[i >> 6] >> (i & 63)) & 1; }
    size_t count() const;
    std::vector<uint32_t> indices() const;  // ascending row indices of the set bits
};

// JS measurefilter semantics: NaN never matches. Sequential below ~1M values; the pool otherwise.
Bitmap compareDoubles(const double* v, size_t n, Cmp op, double threshold, Pool* pool);

Every number taken for it

measurementneoSwift× 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.0170.0472.76×

Read it with this

  • The two results differ in form: neo's figure is for a bitmap, the port's for a list of row numbers.
  • Taken on a shared machine. Both columns ran in the same shuffled rounds, so the multiple stands, but the times themselves read high.

01.3 · kernel · 1,000,000 rows

sort, as a permutation

neo 1.04 ms · Swift 1.23 ms · 1.18× neo. Against every engine →

neo

Swift

0 runs

0 runs

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.

How neo does it

neo turns each double into a sortable 64-bit key and runs an LSD radix sort on the pool: O(n), stable, with the JavaScript rule that NaN sorts as negative infinity and ties keep their order. The ledger's design levers show the step from std::stable_sort at 84 ms, to radix at 14 ms, to the pool at 1 ms.

How Swift does it

The keys are partitioned on their top 16 bits in parallel, then each bucket is sorted on its low 48 bits on its own: insertion sort when tiny, LSD radix otherwise, the largest buckets first.

sort, as a permutation · neon-cpp/bench/lang/swift/main.swift:920 · show all 104 lines
/// MSD partition on the top 16 bits (parallel: per-part histograms → absolute write positions,
/// part-major inside each bucket keeps it stable), then buckets sorted independently on the low
/// 48 bits (insertion sort when tiny, LSD radix otherwise), largest buckets scheduled first.
func sortPermMSD(_ amt: UnsafePointer<Double>, _ n: Int, _ perm: UnsafeMutablePointer<UInt32>, pool: Pool?, parts: Int) {
    let np = pool == nil ? 1 : max(1, min(parts, n / 4096))
    let chunk = (n + np - 1) / np
    let kBuckets = 1 << 16
    let keys = alloc(n, UInt64.self)
    let hist = alloc(np * kBuckets, UInt32.self)        // [p * kBuckets + b]
    // A: keys + per-part histograms of the top 16 bits
    let phaseA: (Int) -> Void = { p in
        let h = hist + p * kBuckets
        memset(h, 0, kBuckets * 4)
        var i = p * chunk
        let hi = min(n, i + chunk)
        while i < hi { let k = sortKey(amt[i]); keys[i] = k; h[Int(k >> 48)] &+= 1; i += 1 }
    }
    if let pool = pool { pool.run(np, phaseA) } else { phaseA(0) }
    // B: turn histograms into absolute write positions (bucket-major, part-major within a bucket)
    let bucketStart = alloc(kBuckets + 1, Int.self)
    let ranges = max(1, min(64, np * 4))
    let perRange = (kBuckets + ranges - 1) / ranges
    let rangeTotal = alloc(ranges + 1, Int.self)
    let phaseB1: (Int) -> Void = { r in
        var total = 0
        var b = r * perRange
        let be = min(kBuckets, b + perRange)
        while b < be { var p = 0; while p < np { total += Int(hist[p * kBuckets + b]); p += 1 }; b += 1 }
        rangeTotal[r] = total
    }
    if let pool = pool { pool.run(ranges, phaseB1) } else { for r in 0..<ranges { phaseB1(r) } }
    var running = 0
    for r in 0..<ranges { let t = rangeTotal[r]; rangeTotal[r] = running; running += t }
    rangeTotal[ranges] = running
    let phaseB2: (Int) -> Void = { r in
        var pos = rangeTotal[r]
        var b = r * perRange
        let be = min(kBuckets, b + perRange)
        while b < be {
            bucketStart[b] = pos
            var p = 0
            while p < np { let c = Int(hist[p * kBuckets + b]); hist[p * kBuckets + b] = UInt32(truncatingIfNeeded: pos); pos += c; p += 1 }
            b += 1
        }
    }
    if let pool = pool { pool.run(ranges, phaseB2) } else { for r in 0..<ranges { phaseB2(r) } }
    bucketStart[kBuckets] = n
    // C: scatter (key, idx) into bucket order
    let part = alloc(n, KV.self)
    let phaseC: (Int) -> Void = { p in
        let h = hist + p * kBuckets
        var i = p * chunk
        let hi = min(n, i + chunk)
        while i < hi {
            let k = keys[i]
            let b = Int(k >> 48)
            let pos = Int(h[b]); h[b] &+= 1
            part[pos] = KV(key: k, idx: UInt64(i))
            i += 1
        }
    }
    if let pool = pool { pool.run(np, phaseC) } else { phaseC(0) }
    // D: sort each bucket on the low 48 bits. Tasks = runs of consecutive buckets, ~n/(4·parts) rows each.
    var taskLo: [Int] = [], taskHi: [Int] = [], taskSize: [Int] = []
    let target = max(1024, n / max(1, 4 * np))
    var b = 0
    while b < kBuckets {
        let lo = b
        var size = 0
        while b < kBuckets && (size == 0 || size + (bucketStart[b + 1] - bucketStart[b]) <= target) {
            size += bucketStart[b + 1] - bucketStart[b]; b += 1
        }
        if size > 0 { taskLo.append(lo); taskHi.append(b); taskSize.append(size) }
    }
    let order = (0..<taskLo.count).sorted { taskSize[$0] > taskSize[$1] }
    let phaseD: (Int) -> Void = { t in
        let ti = order[t]
        var bk = taskLo[ti]
        let bend = taskHi[ti]
        var maxBucket = 0
        var x = bk
        while x < bend { maxBucket = max(maxBucket, bucketStart[x + 1] - bucketStart[x]); x += 1 }
        let tmp = maxBucket > 32 ? alloc(maxBucket, KV.self) : nil
        let scratch = maxBucket > 32 ? alloc(768, Int.self) : nil
        while bk < bend {
            let s = bucketStart[bk], e = bucketStart[bk + 1]
            let cnt = e - s
            if cnt > 1 {
                if cnt <= 32 { insertionSort(part + s, cnt) }
                else {
                    let r = lsdRadix(part + s, tmp!, cnt, shift0: 0, shift1: 48, scratch!, scratch! + 256, scratch! + 512)
                    if r != part + s { memcpy(part + s, r, cnt * 16) }
                }
            }
            var i = s
            while i < e { perm[i] = UInt32(truncatingIfNeeded: part[i].idx); i += 1 }
            bk += 1
        }
        if let t = tmp { free(t) }
        if let s = scratch { free(s) }
    }
    if let pool = pool { pool.run(order.count, phaseD) } else { for t in 0..<order.count { phaseD(t) } }
    free(keys); free(hist); free(bucketStart); free(rangeTotal); free(part)
}

neo's side: the call and the contract

the harness, calling the engine · neon-cpp/bench/bench_engine_options.cpp:372
const double s4 = timeIt([&] { sortIndexByDouble(amt.data(), n, perm, &pool); sink = perm[0]; }, 3);
the sort's contract · neon-cpp/core/sort.h:1
// sort.h — stable O(n) sorting of row indices by a double key (parallel radix).
// JS numeric sort semantics: NaN sorts as -Infinity; ties keep input order.

// Fills `idx` with the stable ascending order of keys[0..n). pool may be null.
void sortIndexByDouble(const double* keys, size_t n, std::vector<uint32_t>& idx, Pool* pool);

Every number taken for it

measurementneoSwift× 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.041.231.18×

Read it with this

  • Taken on a shared machine. Both columns ran in the same shuffled rounds, so the multiple stands, but the times themselves read high.

01.4 · kernel · 1,000,000 rows

sort into a new table

neo 2.88 ms · Swift 7.86 ms · 2.73× neo. Against every engine →

neo

Swift

0 runs

0 runs

What this measures

Sort by amount and build the whole sorted table, every column reordered.

How neo does it

The permutation from the radix sort drives a parallel gather of every column, with the dictionary column moved as ids. The ledger times the gather alone at under three milliseconds for a million rows.

How Swift does it

Every column is gathered through the permutation on the pool, into a fresh arena of bytes and a fresh array of offsets.

sort into a new table · neon-cpp/bench/lang/swift/main.swift:1025 · show all 78 lines
// ───────────────────────────── materialise the sorted table ─────────────────────────────
/// Gathers every column of `t` by `perm` into fresh arenas + offsets (same total bytes).
func materialize(_ t: Table, _ perm: UnsafePointer<UInt32>, pool: Pool?, parts: Int) -> [Column] {
    let n = t.rows
    var out: [Column] = []
    for c in t.cols {
        let off = alloc(n + 1, UInt32.self)
        off[0] = 0
        out.append(Column(arena: allocBytes(c.bytes + 64), offsets: off, bytes: c.bytes))
    }
    let ncols = t.cols.count
    @inline(__always) func copyCell(_ dst: UnsafeMutableRawPointer, _ src: UnsafeRawPointer, _ len: Int) {
        dst.storeBytes(of: src.loadUnaligned(as: V16.self), as: V16.self)
        if len > 16 { var k = 16; while k < len { (dst + k).storeBytes(of: (src + k).loadUnaligned(as: V16.self), as: V16.self); k += 16 } }
    }
    guard let pool = pool, parts > 1 else {
        for c in 0..<ncols {
            let src = t.cols[c], dst = out[c]
            var cursor = 0
            var i = 0
            while i < n {
                let r = Int(perm[i])
                let s = Int(src.offsets[r]), len = Int(src.offsets[r + 1]) - s
                copyCell(UnsafeMutableRawPointer(dst.arena + cursor), UnsafeRawPointer(src.arena + s), len)
                cursor += len
                dst.offsets[i + 1] = UInt32(truncatingIfNeeded: cursor)
                i += 1
            }
        }
        return out
    }
    let np = max(1, min(parts, n / 4096))
    let chunk = (n + np - 1) / np
    let partBytes = alloc(ncols * (np + 1), Int.self)   // [c * (np+1) + p]
    // 1. lengths (into the new offsets) and bytes per (column, part)
    pool.run(ncols * np) { task in
        let c = task % ncols, p = task / ncols
        let src = t.cols[c], dst = out[c]
        var total = 0
        var i = p * chunk
        let hi = min(n, i + chunk)
        while i < hi {
            let r = Int(perm[i])
            let len = Int(src.offsets[r + 1]) - Int(src.offsets[r])
            dst.offsets[i + 1] = UInt32(truncatingIfNeeded: len)
            total += len
            i += 1
        }
        partBytes[c * (np + 1) + p] = total
    }
    for c in 0..<ncols {
        var running = 0
        for p in 0..<np { let b = partBytes[c * (np + 1) + p]; partBytes[c * (np + 1) + p] = running; running += b }
        partBytes[c * (np + 1) + np] = running
    }
    // 2. copy with known destinations. The 16-byte vector copy may spill past a cell; inside a
    //    part that is overwritten by the next cell, but the last ≤16 bytes of a part belong to
    //    another thread, so cells that close to the part end are copied exactly.
    pool.run(ncols * np) { task in
        let c = task % ncols, p = task / ncols
        let src = t.cols[c], dst = out[c]
        var cursor = partBytes[c * (np + 1) + p]
        let safeEnd = partBytes[c * (np + 1) + p + 1] - 16
        var i = p * chunk
        let hi = min(n, i + chunk)
        while i < hi {
            let r = Int(perm[i])
            let len = Int(dst.offsets[i + 1])
            let d = UnsafeMutableRawPointer(dst.arena + cursor), sp = UnsafeRawPointer(src.arena + Int(src.offsets[r]))
            if cursor + len <= safeEnd { copyCell(d, sp, len) } else { memcpy(d, sp, len) }
            cursor += len
            dst.offsets[i + 1] = UInt32(truncatingIfNeeded: cursor)
            i += 1
        }
    }
    free(partBytes)
    return out
}

neo's side: the call and the contract

the sort's contract · neon-cpp/core/sort.h:1
// sort.h — stable O(n) sorting of row indices by a double key (parallel radix).
// JS numeric sort semantics: NaN sorts as -Infinity; ties keep input order.

// Fills `idx` with the stable ascending order of keys[0..n). pool may be null.
void sortIndexByDouble(const double* keys, size_t n, std::vector<uint32_t>& idx, Pool* pool);

Every number taken for it

measurementneoSwift× 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.887.862.73×

Read it with this

  • Taken on a shared machine. Both columns ran in the same shuffled rounds, so the multiple stands, but the times themselves read high.

01.5 · kernel · 1,000,000 rows

sum by region

neo 0.020 ms · Swift 0.037 ms · 1.85× neo. Against every engine →

neo

Swift

0 runs

0 runs

What this measures

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

How neo does it

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.

How Swift does it

Each chunk of rows adds into its own small array of group totals on the pool, and the chunks' totals are then added together in a fixed order.

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
}

neo's side: the call and the contract

the harness, calling the engine · neon-cpp/bench/bench_engine_options.cpp: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);

Every number taken for it

measurementneoSwift× 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.0200.0371.85×

Read it with this

  • Taken on a shared machine. Both columns ran in the same shuffled rounds, so the multiple stands, but the times themselves read high.

02 · Every number

The same kernels, in other languages

1,000,000 rows · ms, lower is better · 2026-10-12 06:38 · shared machine

kernelneoSwift× neo
load (parse + type + dictionary)3.593.540.99×
filter amount > 5000.0170.0472.76×
sort, as a permutation1.041.231.18×
sort into a new table2.887.862.73×
sum by region0.0200.0371.85×

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).

03 · The code that ran

Swift's side, in full

Swift is Apple's language, compiled with LLVM and tuned for the same Apple silicon this was measured on. The port runs on every core. The port was written to beat the C++ and given the C++ numbers to beat. Its code is beside each race above, and nothing of it is left out here.

neo's side: the calls and the contracts

neo's kernels are the product, so their bodies stay in the engine. What is published is what a reader needs to judge the comparison: the harness calling the engine, and each kernel's declaration with the guarantees it is held to, down to the order a floating-point sum is taken in. Each contract is beside its race above; the harness lines together, and whatever no race showed, are here. The binaries run on request, and the scripts named below reproduce every number on this page.

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;
how energy is read · neon-cpp/bench/energy.h:1
// energy.h — what a process has used, as the operating system's own ledger has it: the energy it was billed,
// its processor time, the wake-ups it caused. The one reader of that ledger, for two questions:
//   what a kernel costs — the Evals page's Energy view (bench_engine_options.cpp reads it around each kernel;
//     tools/energy/energy.cpp checks the counter and reads it for a command it runs;
//     bench/lang/python/yardstick.py reads the same field; PHD.md section 33)
//   what a process costs at rest — the idle studies (scripts/idle_cost.sh and scripts/idle_server.py take
//     `neon_energy --pid <pid>` twice and subtract; PHD.md section 31.1)
//
// macOS on Apple silicon: proc_pid_rusage(RUSAGE_INFO_V6). ri_energy_nj — nanojoules billed to the process's
// threads for the processor cores they ran on, since it started. Not the memory, the disk or the screen, and
// not the machine at rest: what running this code added. (Not ri_billed_energy: against half a second of a
// busy loop that field read 0.08 mJ where ri_energy_nj read 1,887.) Beside it, processor time in nanoseconds
// — `ps` has hundredths of a second, more than a resting window uses in a minute — and the wake-ups. No
// privilege is needed, and any process of the same user can be read, so a tree of processes can be summed.
// Linux: /proc/<pid>/stat and /proc/<pid>/status — clock ticks and context switches. No energy: RAPL is one
// figure for the whole machine and root's to read.
// Elsewhere: nothing is read (ok stays false).
//
// A reading is cumulative; a measurement is the difference of two. The counter moves in steps (a thread's
// share is settled when it leaves a core or enters the kernel), so a window should be a quarter of a second
// or more: `neon_energy --check` prints how far apart the steps are on the machine it runs on.

04 · Diligence

What both sides were held to

Scripts: scripts/scale_study.sh · measured 2026-10-12 · the numbers on this page come from the same ledger as the app's own Evals page, written by neon-cpp/scripts/results_md.py.

More comparisons