Topic 11 notes — execution models
Baseline (provided lane, Apple M3 Pro, measured 2026-07-28)
cargo run --release --bin exec_bench — 50 M rows,
SELECT k, SUM(v) WHERE f < t GROUP BY k, best of 3.
| selectivity | volcano | vectorized | kernels |
|---|---|---|---|
| 5% | 0.386 s / 129.4 M rows/s | stub | stub |
| 50% | 0.484 s / 103.3 M rows/s | stub | stub |
| 95% | 0.669 s / 74.7 M rows/s | stub | stub |
103 M rows/s at 50% selectivity is the tuple-at-a-time ceiling on this machine, and note which direction it moves. Cost rises monotonically with selectivity (74.7 M rows/s at 95%) because in a Volcano pipeline every surviving row pays the full per-tuple interpretation cost — a virtual call per operator per row — on its way to the aggregate. The filter is not the expensive part; passing the filter is.
Predict before implementing: at 50% selectivity, how much of the gap between 103 M rows/s and memory bandwidth (topic 12 measures ~50 GB/s = 6.2 G u64/s on this box) does batching close, and what does the remainder consist of?
Predictions (fill BEFORE implementing vectorized.rs / kernels.rs)
Measured baseline (provided volcano, release, 50M rows, sel 50%), from the Baseline block above: 0.484 s = 103.3 M rows/s — already fast, ~9.7 ns/row including two virtual calls per tuple. Modern branch predictors eat stable indirect calls; the Volcano tax on an M-series core is NOT mostly call overhead. Where will the vectorized win actually come from? (SIMD, ILP, no per-row branch.) Predict accordingly:
| engine | predicted M rows/s | predicted ratio vs volcano | actual | actual ratio |
|---|---|---|---|---|
| volcano (sel 50) | — | 1× | 103.3 | 1× |
| vectorized (sel 50) | ||||
| kernel (sel 50) |
| question | prediction | actual |
|---|---|---|
| does selectivity 5 vs 95 change volcano most or vectorized most? | ||
| X100 U-curve: vectorized at BATCH_SIZE 64 / 1024 / 65536 | ||
does the kernel’s branchless mask beat a branchy if at sel 50? (topic 0 says: yes, hugely) | ||
| kernels.rs: does autovec emit NEON for the fused loop? | ||
| multi-accumulator ILP trick: helps or not with random 64-slot destination? |
Implementation log
- vectorized.rs: batches + selection vector + flat group array; tests green
- kernels.rs: fused branchless pass; negative-values test green (mask sign extension!)
- exec_bench full run recorded above
- BATCH_SIZE sweep recorded (64 / 1024 / 65536)
- flamegraph of volcano run — where does time actually go? (dispatch vs branch miss vs agg)
- look at kernels.rs asm — NEON? record instruction mix
Surprises / dead ends:
- (provided-baseline surprise, already found) LLVM DEVIRTUALIZED the
Box<dyn>operator chain when the tree was statically known — 202 M rows/s before black_box, 180 after. A compiler will happily turn your Volcano engine into a compiled engine if you let it. Real engines can’t (trees built from plans at runtime).
Questions from the reading guides
DuckDB execution (reading-duckdb-execution.md)
- Why 2048 not 64K (chunk bytes vs L2):
- CONSTANT vector trace for
2 * price: - Which operators need HAVE_MORE_OUTPUT and why:
- Salt trick: fraction of non-matches still chasing pointers, for k salt bits:
- M11 Expand as HAVE_MORE_OUTPUT — state between calls:
Postgres executor (reading-postgres-executor.md)
- Indirect branches per tuple for
SELECT sum(x) WHERE y > 10vs per 2048 in DuckDB: - Why computed goto helps (predictor entry per opcode site):
- Other “first call replaces itself” patterns:
- eval.rs: linear steps or closure tree; 3 Cypher shapes worth peepholing:
Rust execution stack (reading-rust-execution-stack.md)
- Async operators vs hand-rolled state machines for M11 (one-threadpool decision from topic 7):
- Which Cypher results are order-sensitive (MorselSeq equivalent needed?):
- Select-in-lanes vs compact-then-compute — selectivity threshold guess:
- HT probes saved by intern-then-index with 4 aggregates:
- Which operator contract fits SpMV-produces-a-matrix best:
X100 (reading-x100.md)
- Vector length that keeps 3 ops × 8 cols in M-series L1:
- Full-column materialization: GB moved for Q1, seconds at ~100 GB/s:
- Rust monomorphization of primitives — compile time/binary size cost:
- What autovec gives free vs what needs std::simd (answer from kernels.rs asm):
Compiled vs vectorized (reading-compiled-vs-vectorized.md)
- Why vectorized probing overlaps misses and fused loops don’t (MLP):
- Why prefetching is easy vectorized, contorted compiled:
- Wide-pipeline load/store count per row, both models:
- Prediction for kernels.rs vs vectorized.rs on THIS workload:
- Graph workloads’ scorecard column → JIT priority for M19:
Morsel-driven parallelism (reading-morsel-parallelism.md)
- What bounds morsel size below and above:
- Thread-local HTs + merge vs shared lock-free HT: 64 groups / 64M groups:
- Restoring order after morsel pulling — cost:
- P/E cores: NUMA-shaped or skew-shaped problem:
- M11 parallelism paragraph: morselize the frontier? natural morsel for SpMV:
Cross-topic threads
- Vector size is a CACHE parameter — topic 0’s ladder decides it.
- Selection vectors = filter-without-copying = the same don’t-materialize discipline as late materialization (topic 12 next).
- Salt-in-pointer in the join HT = bit-smuggling ledger entry #6.
- Vectorized probes win by MLP = lookup_shootout’s HashMap flatline.
- Morsel pulling vs static partitions = topic 9’s scaling.rs skew story.
M11 log (vectorized runtime: batch.rs / vectorized.rs / eval.rs)
- batch size chosen by measurement (sweep on M-series, not DuckDB’s 2048 by faith)
- Batch type: node ids + property columns + selection vector
- operator contract:
next(&mut self, out: &mut Batch)+ a HAVE_MORE_OUTPUT-style result enum (Expand explodes) - eval.rs: linear-step interpreter over batches, peephole the 3 hot shapes
- boundary decision: GraphBLAS matrix ops ↔ row-ish batches — where does Expand hand off?
- bench vs M10’s naive interpreter; flamegraph both
Done when
- All three engines agree + full bench table filled + U-curve recorded.
- The compiled-vs-vectorized scorecard argued for M11 with a decision.
- M11 parallelism paragraph written (morsel design for graph queries).