Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Columnar-list spike evidence

This accompanies the columnar-list proposal. Sources and reproduction commands live in experiments/columnar-list/README.md in the repository. These are bounded feasibility experiments, not a production container, Plenty performance results, or a backend comparison.

This page records the original 2026-10-23 experiment. The subsequent generated Plenty implementation spike tests real runtime failure paths, borrowing, and Cranelift-generated scans separately.

Layout experiment

The dependency-free Rust program compares an array of 64-byte records (eight u64 fields), eight flat columns, and groups of 1,024 or 4,096 rows with one allocation per field. Every layout contains identical generated values. The kernels sum one field, filter on one field and sum another, mix all eight fields in sequential row order, and perform the same whole-row work in an odd-stride permutation. That permutation visits every row once; it is a repeatable dispersed-access workload, not a model of all random workloads.

All arithmetic deliberately wraps. This is equivalent across the experiment, but does not model Plenty’s default overflow traps. Checked integer and floating-point kernels need separate end-to-end validation before adoption. Timed paths allocate nothing; construction is outside the scan measurements. Append and vacuum are correctness experiments, not performance measurements.

On 2026-10-23: AMD Ryzen 7 7840HS, Linux 7.2.11, rustc 1.99.0 (b940084d7), LLVM 23.1.1, native x86-64 target, CPU affinity 4. Per-core L1d is 32 KiB, L2 is 1 MiB, and shared L3 is 16 MiB. Each layout has 32 KiB, 2 MiB or 64 MiB of row payload at the three tested lengths; the single selected column has 4 KiB, 256 KiB or 8 MiB. All four layouts coexist.

Release builds use -C target-cpu=native; a second build also disables loop and SLP vectorization. Seven samples rotate layout order; each sample traverses 4,194,304 rows in total. References and outputs cross black_box, every result is checked, and individual scan functions are not inlined into the timed repetition. These are repeated warm scans with no cache flush. Frequency is not locked and the machine is shared: load average was approximately 8.1–9.7 on 16 logical CPUs. Treat close rankings and exact ratios as provisional.

Large input: median nanoseconds per row

1,048,576 rows; lower is better. “Scalar” means both LLVM vectorizers disabled, not a simulation of Cranelift. Native may use all supported host instructions.

Build / workloadAoSFlat SoAChunk 1,024Chunk 4,096
Scalar: sum one field2.4630.2510.4460.334
Scalar: filter two fields1.3630.3180.8170.513
Scalar: all fields, sequential1.9043.5223.0062.626
Scalar: all fields, permuted24.14647.17958.05752.378
Native: sum one field4.2170.0780.2030.154
Native: filter two fields6.8950.2920.7360.458
Native: all fields, sequential10.2852.2542.7902.385
Native: all fields, permuted22.90449.42656.29857.228

Sample ranges matter: scalar AoS sequential whole-row work ranged 1.707–2.325 ns/row and flat SoA 3.453–4.638. Native permuted work ranged 21.686–27.734 for AoS and 45.205–64.939 for flat SoA. This is enough to identify tradeoffs, not to choose final tuning parameters.

Size sensitivity: one-field scan

RowsScalar AoSScalar flat SoANative AoSNative flat SoA
5120.4030.2400.5600.040
32,7680.8100.2560.7350.064
1,048,5762.4630.2514.2170.078

The large single-column working set fits shared L3 while the complete AoS does not. Cache residency and reduced traffic are part of the benefit, but these warm-scan numbers must not be advertised as sustained DRAM bandwidth or cold-scan speedups. No hardware cache-miss counters were collected.

Code generation changes the story

Inspection of emitted assembly found vpgatherqq in the native AoS narrow, filter and sequential whole-row kernels. Flat SoA uses contiguous vector loads/arithmetic (vpaddq, and vprolq for whole-row mixing), without those gathers. With both vectorizers disabled, these kernel bodies contain no vector instructions. Chunk scan functions call the corresponding SoA kernels.

Thus the striking native ratios combine layout, cache residency and a poor AoS gather strategy on this compiler/CPU. Scalar results still favor columns for narrow work, but the sequential whole-row ranking reverses. Do not claim that all full-object access is slower in SoA or that LLVM always improves a loop. The experiment also charges flat SoA’s generated indexed row access its bounds checks and chunked random access its directory/division work. Future Plenty lowering may optimize those differently.

Flat columns won the measured narrow scans. Fixed chunks did not provide a scan-speed reason to adopt them here; growth limits, metadata pruning and deletion behavior need separate measurements. This supports starting flat, without foreclosing chunked storage later.

Ownership and metadata experiments

Four Rust tests check:

  • Equal row values and scan results at lengths 0, 1, 2, 7, 1,023, 1,024, 1,025 and 4,097, including chunk tails.
  • Failure before either required column reserve: no partial live row, both owned fields returned intact, then destroyed exactly once. Real try_reserve errors share this path. Injection is a deterministic gate, not process-wide allocator failure instrumentation.
  • Every deletion mask of eight rows: owners destroyed at deletion, repeat deletion harmless, stable in-place vacuum preserves row alignment/order, repeat vacuum harmless and no duplicate destruction. Safe Option slots model holes; production bitmap/uninitialized storage is not implemented.
  • Conservative min-bound pruning over identical sorted/permuted values: a < 64 query visits 1/8 groups when sorted and 8/8 when dispersed, returning the same sum. Statistics are rebuilt for the test, not maintained under mutation. This is a correctness demonstration, not a timing result.

These tests reduce implementation risk but leave real runtime allocation failure, destructor ordering, chunk relocation, and borrowing diagnostics for the production gate. There is no benchmark of noncopyable/string fields, concurrent updates, growth, tombstone scan density or memory reclamation.

Current Plenty feasibility

Using a compiler freshly built from 45798945, columns.plenty creates two ordinary lists, reserves both before appending, and zips borrowed columns into a fold. It prints 90. This is checked language code with ordinary collection storage and no generated schema support. It establishes that a concrete SoA wrapper need not wait for clist, not that its elements are packed native words.

reject-row-reference.plenty fails with references cannot be stored in class fields. reject-clist.plenty fails with `clist` is not a generic function. The rejection fixtures document the baseline only, not new defects. The implementation inspected in plenty-runtime/src/entries.rs stores collection rows in u128 words; the borrowed-list row helpers in plenty-runtime/src/aggregates.rs expose contiguous row addresses. A columnar source needs new field/projection lowering and must not masquerade as that ABI.

All four native tests pass, including in release mode; formatting and Clippy pass for the isolated crate. The Plenty checks are reproducible through check_plenty.py. Existing language/reference behavior is unchanged.