# reduce — benchmark (incremental sum on insert)

Workload: 10_000 rows; `reduce(add, remove, init)` running sum of `val`.
Tick inserts one new row at a fresh key (BI0 — the entry point reduce
was designed to be incremental for). Batch streams 1000 such inserts.
Generated by [comparisons/bench/operators/reduce.bench.ts](../../comparisons/bench/operators/reduce.bench.ts).

| Library | Setup (ms) | Single (ms) | vs data | Batch 1000 (ms) | vs data |
|---|---:|---:|---:|---:|---:|
| **data** | 3.18 | **0.005** | — | **1.44** | — |
| crossfilter | 1.27 | 0.015 | 3.0× | 27.06 | 18.8× |
| svelte-store | 0.953 | 0.253 | 50.6× | 697.69 | 485× |
| solid | 1.09 | 0.292 | 58.4× | 570.89 | 396× |
| rxjs | 1.30 | 0.304 | 60.8× | 594.17 | 413× |
| react | 1.38 | 0.363 | 72.6× | 604.94 | 420× |
| preact-signals | 1.41 | 0.653 | 131× | 875.12 | 608× |
| mobx | 139.88 | 8.05 | 1610× | 15339.95 | 10653× |
| vue-reactivity | 33.01 | 21.39 | 4278× | 42561.31 | 29556× |



data is fastest on both single (1.7x over crossfilter) and batch (19x
over crossfilter).

## How

3-arg `reduce(add, remove, init)` threads each BI0 through `add` in
O(Δ). Crossfilter's `groupAll.reduce(add, remove, init)` is the textbook
peer match and stays close on single. Other peers re-walk on every
emit. BU1 (a whole-slot overwrite) is also O(Δ) — the operator keeps a
per-key value cache so it can `remove(old)` + `add(new)`; only BU2 (a
nested in-place edit) falls back to rebuild, since the row reference is
unchanged and the cache holds the already-mutated row (CLAUDE.md notes
this).

Workload note: data's batch closes over a monotonic counter shared
across measure() reps so every insert hits a fresh key (BI0) — the path
this table measures. (Reusing keys would exercise the BU1 overwrite path
instead, which is now incremental too, so it no longer masks the
incremental path the way it did when BU1 rebuilt.)

Run `BENCH_OPS=reduce npm run bench:ops` to refresh.
