Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 160 additions & 9 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,96 @@ The central hypothesis is:
> ThinVec chose the right one-word owner. JackVec can improve its allocation
> representation and construction algorithms while preserving that final density.

## Active remaining work

This is the authoritative actionable roadmap. The experiment record and older
P0/P1/P2 proposal lists below are retained as an audit trail; their unchecked
boxes are historical hypotheses, not current commitments. New work starts here,
is pre-registered in the experiment record, and is removed or checked off when a
decision is reached.

### P0: land and complete trustworthy baselines

- [ ] Land the corrected Linux system-allocator matrix from `perf/iteration-audit`.
It replaces the uncontrolled tcmalloc report and includes the shared iteration
kernel, explicit allocator provenance, five rotations, and regenerated visuals.
- [ ] Capture the matching clean M4 macOS matrix at the same Git commit, Rust
1.97.0 compiler identity, schema, workload matrix, and explicit `system`
allocator policy. Do not pool platform measurements.
- [ ] Validate the Linux/macOS pair and extend the existing `LATEST.md` and SVGs
in one reporting change. Preserve each platform's absolute and relative values.
- [ ] Enable the already-configured CodSpeed ARM64 macro runner only after public
repository runner-group access is available; keep it a trend lane, not a
substitute for physical-host baselines.
- [ ] Accumulate repeated clean baselines before reconsidering the ±3% practical
equivalence band. Never tune the band to change a desired classification.

### P1: credible JackVec CPU gaps from the corrected Linux matrix

- [ ] Audit four-element append (`1.334x Vec`, `0.863x ThinVec`) first. Separate
unavoidable one-word-header setup from avoidable reserve, branch, and relocation
overhead using same-binary controls, assembly, fixed-work counters, allocation
parity, and code size. Preserve the accepted large append (`1.020x Vec`,
`0.313x ThinVec`).
- [ ] Audit `retain<u64>` (`1.245x Vec`, `0.972x ThinVec`) while preserving panic
repair, exact-once drops, and the accepted large-element improvement. Determine
whether predicate-loop/header publication overhead remains avoidable.
- [ ] Audit dedup only after retain: `u64` is `1.069x Vec` but `0.627x ThinVec`;
64-byte elements are `1.100x Vec` and `0.975x ThinVec`. Avoid trading the major
upstream gain for a small Vec-relative improvement.
- [ ] Audit reserved resize (`1.067x Vec`, `0.359x ThinVec`) for redundant checks
after reserve. Its large upstream win is a non-regression gate.
- [ ] Revisit reserved extend only if a repeated baseline converts its current
`1.035x Vec` inconclusive result into a stable loss. Do not optimize a point
estimate inside an uncertain interval.
- [ ] Explain why growing 1,024 elements beats Vec (`0.874x`) but trails ThinVec
(`1.098x`) before changing growth policy. Check capacity sequence, realloc moves,
allocator interaction, and code generation independently.

Long sequential iteration is closed: the corrected shared-kernel result is
`1.000x Vec` and equivalent across all five implementations. Do not reopen compact
header alignment without a new real-workload counterexample.

### P2: memory and allocator boundaries

- [ ] Add allocator-size-boundary cases only when they distinguish a proposed
representation or growth-policy decision. Always report owner, requested heap,
usable heap, allocation/reallocation counts, and moved versus in-place reallocs.
- [ ] Compare system allocators with mimalloc/tcmalloc only as separately labelled
situational studies. Never overwrite or merge them into the system baseline.
- [ ] Investigate canonical capacity classes or allocator-aware growth only after
measured histograms show retained-memory waste. Reject undocumented
`malloc_usable_size` capacity as a general safe policy.
- [ ] Preserve the one-word owner, one-word `Option`, empty singleton, contiguous
slice, reconstructable layout, and compact requested header. A universal
16-byte header is rejected unless new evidence outweighs its known density loss.

### P3: motivating sqlparsers workload

- [ ] Instrument `../../moderately-ai/sqlparsers` for final `(len, capacity)`
histograms, empty/small/large buckets, realloc movement, and mutation-after-parse
frequency before designing caller-informed policies.
- [ ] Re-evaluate a transient construction builder only from those distributions.
The previous generalized builder prototype was rejected; a successor must avoid
moving final elements, retain panic-safe partial initialization, and demonstrate
end-to-end parser value beyond current JackVec push.
- [ ] Integrate JackVec experimentally only after the standalone CPU work stabilizes.
Measure parse CPU, deterministic instructions, allocations, requested/usable and
retained bytes, traversal/rendering, compile cost, and pinned AST node sizes.
- [ ] Keep final inline storage out of AST nodes. Builder-only inline scratch is
situational and requires measured spill/copy wins without increasing final node
size.

### P4: release readiness after performance work

- [ ] Run the complete stable/MSRV/nightly/Miri/no-std/feature/docs/Clippy matrix
after each accepted unsafe or layout change and before release preparation.
- [ ] Reconcile public documentation, changelog/release notes, attribution to
Mozilla ThinVec, repository metadata, and the final MSRV/toolchain policy.
- [ ] Perform a final unsafe/layout audit and downstream compatibility pass before
considering crates.io publication. Publication remains explicitly out of scope
until separately authorized.

## Cross-implementation measurement system (2026-07-11)

- [x] Add the non-published `jack-vec-comparisons` workspace crate.
Expand All @@ -39,12 +129,13 @@ The central hypothesis is:
Linux report. Extend the same report rather than opening a separate PR when the
clean macOS half becomes available.
- [x] Add CodSpeed CPU simulation for pull requests and `main`.
- [ ] Enable public-repository access for the `moderately-ai` Actions runner
group, set `CODSPEED_MACRO_ENABLED=true`, and validate ARM64 Linux wall time.
- [ ] Capture and commit the first clean M4 macOS summary; the Ryzen 7950X3D
Linux summary is complete. Retain full raw rounds outside Git.
- [ ] Revisit the practical-equivalence band only after enough stable repeated
baselines exist; never tune it in response to a desired result.
- [x] Track public-repository access for the `moderately-ai` Actions runner in the
active roadmap; configuration is ready but organization runner-group access and
`CODSPEED_MACRO_ENABLED=true` remain external prerequisites.
- [x] Move the matching clean M4 macOS capture and pair validation into the active
roadmap; retain full raw rounds outside Git.
- [x] Move practical-equivalence-band review into the active roadmap with its
evidence threshold unchanged.

## Non-negotiable invariants

Expand All @@ -67,12 +158,12 @@ The central hypothesis is:

- Canonical repository: `https://github.com/moderately-ai/jack-vec`
- Historical fork: `https://github.com/tomsanbear/thin-vec`
- Canonical remote branch: `main` (`jackvec` remains the local working branch)
- Working branch: `benchmarks/allocator-usable-size`
- Canonical remote branch: `main`
- Working branch: `perf/iteration-audit`
- Initial benchmark commit: `5e4845a`
- Refined timing-boundary commit: `f8fa1e8`
- Persistent benchmark checkout: `catalyzed-builder:~/thin-vec`
- Benchmark toolchain: Rust 1.86
- Library MSRV: Rust 1.86; authoritative comparison toolchain: Rust 1.97.0
- Benchmark CPU: Ryzen 9 7950X3D, pinned to CPU 0 on the 96 MiB L3 CCD
- Benchmark OS: Ubuntu, Linux 5.15, glibc 2.35
- Allocator warning: the login environment preloads tcmalloc globally. Every CPU
Expand All @@ -81,6 +172,66 @@ The central hypothesis is:

## Experiment record

### Compact-header data alignment (`perf/iteration-audit`)

- Status: benchmark corrected; no representation change justified
- Baseline: authoritative Linux comparison commit `4943262`; Rust 1.97.0 on
pinned CPU 0 of the Ryzen 7950X3D V-cache CCD.
- Finding: the compact 8-byte header places ordinary `u64` data at 8 modulo 16
under the measured allocator. Both JackVec and Vec folds vectorize, but a
10,000,000-iteration fixed-work control measured 3.93 billion cycles for
JackVec, 3.86 billion for an intentionally one-`u64`-offset Vec slice, and 2.93
billion for aligned Vec. Instructions were effectively equal; IPC fell from
6.2 to 4.6--4.7. This proves alignment can dominate that specific fixed-work
kernel; it does not prove a generalized JackVec iteration loss.
- Harness finding: a seven-round two-binary 16-byte-header control was invalidated
by its unchanged Vec control: Vec/1024 shifted from about 102 ns in the baseline
binary to 59 ns in the candidate, while JackVec shifted in the opposite
direction. The generic benchmark emitted a separately optimized fold kernel per
implementation, making instruction/code placement part of the apparent vector
comparison (and explaining the implausible spilled SmallVec4/SmallVec8 split).
The permanent benchmark must call one shared, non-inlined slice kernel so real
data alignment remains measured without comparing five different loop bodies.
- Environment finding: the builder globally injects tcmalloc through
`LD_PRELOAD`, but the first matrix runner neither cleared nor recorded it. The
published Linux measurements are valid only as an uncontrolled tcmalloc result,
not as the intended system-allocator baseline. The runner now requires an
explicit allocator policy, records inherited/effective injection variables,
clears both Linux and macOS injection variables for `system`, and rejects a
cross-platform pair without that policy. Replace—not relabel—the Linux baseline
with a clean system-allocator run.
- Hypothesis: a reconstructable layout that preserves 16-byte data alignment can
recover vectorized traversal and other bulk-loop throughput. A universal
16-byte header is only a diagnostic control because it gives back JackVec's
eight requested-byte header saving.
- CPU gates: reproduce the iteration recovery in a same-binary fixed-work test
and the cross-vector benchmark; check nested traversal, push, append, retain,
dedup, extend, and resize for both gains and regressions. Do not infer a broad
win from iteration alone.
- Memory gates: report owner, requested, and allocator-usable bytes independently
for every existing allocation scenario. Nested sparse/small requested-byte
regressions are first-class costs even where glibc size-class rounding leaves
usable bytes unchanged.
- Safety gates: allocation and deallocation layouts must remain reconstructable;
empty singleton pointers must satisfy alignment without adding a dynamic
capacity branch to ordinary `data_raw`; over-aligned and ZST behavior, boundary
rejection, strict-provenance Miri, and all existing lanes must pass.
- Decision: reject a universal policy that merely trades the compact header away.
Accept only a clear overall representation improvement or an explicit,
caller-selected performance mode whose tradeoff is visible in the type/API.
- Corrected result: after routing all implementations through one shared,
non-inlined slice kernel and clearing injected tcmalloc, 1,024-element JackVec,
Vec, ThinVec, SmallVec4, and SmallVec8 iteration are equivalent; JackVec/Vec is
1.000x. The compact header therefore has no demonstrated generalized traversal
regression. Retain the fixed-work alignment finding as situational codegen
evidence, but do not change the representation from it.
- Replacement matrix: authoritative system-allocator Linux report at benchmark
commit `2da08ee`, Rust 1.97.0, five rotations, 100% minimum pinned-core idle,
maximum audited one-minute load 1.18, and no host issues. JackVec records eight
confidence-qualified wins, five equivalents, three inconclusive results, and
six losses versus Vec. The next credible CPU target is four-element append,
followed by `retain<u64>`.

### Guarded `Splice` fill (`perf/splice-fill-guard`)

- Status: accepted; temporary benchmark removed
Expand Down
10 changes: 7 additions & 3 deletions comparisons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ An authoritative physical-host run requires a clean commit and performs five
Latin-square registration rotations:

```console
uv run --project comparisons/tools --locked python comparisons/tools/run_matrix.py --toolchain 1.97.0 --output-name macos-aarch64
uv run --project comparisons/tools --locked python comparisons/tools/run_matrix.py --toolchain 1.97.0 --cpu 0 --output-name linux-x86_64
uv run --project comparisons/tools --locked python comparisons/tools/run_matrix.py --toolchain 1.97.0 --allocator system --output-name macos-aarch64
uv run --project comparisons/tools --locked python comparisons/tools/run_matrix.py --toolchain 1.97.0 --allocator system --cpu 0 --output-name linux-x86_64
uv run --project comparisons/tools --locked python comparisons/tools/validate_pair.py \
comparisons/benchmark-results/macos-aarch64.json \
comparisons/benchmark-results/linux-x86_64.json
Expand All @@ -51,7 +51,11 @@ The exact toolchain argument is mandatory and must match across hosts. The Linux
host is expected to pin CPU 0 on the Ryzen 7950X3D's 96 MiB V-cache
CCD, leave sibling CPU 16 idle, use the performance governor during measurement,
and restore its prior governor afterwards. The runner records but does not mutate
power-management policy. macOS runs should be on AC power with Low Power Mode
power-management policy. The allocator policy is also mandatory: `system` clears
`LD_PRELOAD` and `DYLD_INSERT_LIBRARIES` for every build and measurement while
recording their inherited and effective values. Environment-injected allocator
runs are explicitly recorded but cannot form an authoritative cross-platform
pair. macOS runs should be on AC power with Low Power Mode
disabled and without other sustained work.

The runner resolves and verifies the requested `rustc` release, records its
Expand Down
16 changes: 8 additions & 8 deletions comparisons/benches/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,17 +170,17 @@ fn push_preallocated(c: &mut Criterion) {
group.finish();
}

#[inline(never)]
fn sum_u64_slice(values: &[u64]) -> u64 {
values
.iter()
.fold(0_u64, |sum, value| sum.wrapping_add(*value))
}

fn bench_iteration<V: BenchVector<u64>>(group: &mut BenchmarkGroup<'_, WallTime>, len: usize) {
let values = build_reserved::<V>(len);
group.bench_function(BenchmarkId::new(V::LABEL, len), |bencher| {
bencher.iter(|| {
black_box(
values
.as_slice()
.iter()
.fold(0_u64, |sum, value| sum.wrapping_add(*value)),
)
});
bencher.iter(|| black_box(sum_u64_slice(black_box(values.as_slice()))));
});
}

Expand Down
39 changes: 20 additions & 19 deletions comparisons/benchmark-results/LATEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,20 @@ implementation and scenario is retained, and platforms are never pooled.
## What this baseline says

- JackVec is not an across-the-board faster `Vec`: it has
6 confidence-qualified wins and
7 losses versus `Vec` in this matrix.
8 confidence-qualified wins and
6 losses versus `Vec` in this matrix.
- Its intended nested-density advantage is substantial: requested memory for the
empty and sparse nested workloads is
0.333× and
0.526× Vec,
respectively, while each collection owner remains one machine word.
- The optimized large append path reaches
1.013× Vec and
0.322× upstream
1.020× Vec and
0.313× upstream
ThinVec. This is a large targeted improvement, not a universal CPU claim.
- The clearest JackVec CPU losses versus Vec include 1,024-element sequential
iteration (1.265×),
`u64` retain (1.246×), and
four-element append (1.437×).
They are retained here as investigation targets.
- JackVec's three largest median CPU gaps versus Vec are `append_preallocated/4` (1.334×), `retain_mixed/u64` (1.245×), `retain_mixed/64_byte` (1.101×). They are
retained here as investigation targets; confidence-aware classifications remain
authoritative over point-estimate ordering.
- Against the inline candidates, JackVec wins most measured CPU medians, while
SmallVec avoids heap allocation when values fit inline. Neither representation
dominates every workload.
Expand All @@ -40,20 +38,20 @@ silently counted as equality.

| Implementation | Wins | Equivalent | Inconclusive | Losses |
|---|---:|---:|---:|---:|
| JackVec | 6 | 4 | 5 | 7 |
| ThinVec | 7 | 2 | 3 | 10 |
| SmallVec4 | 1 | 1 | 3 | 17 |
| SmallVec8 | 1 | 3 | 3 | 15 |
| JackVec | 8 | 5 | 3 | 6 |
| ThinVec | 8 | 6 | 1 | 7 |
| SmallVec4 | 5 | 3 | 1 | 13 |
| SmallVec8 | 5 | 2 | 1 | 14 |

For direct context, this simpler head-to-head table compares median CPU times
using the same ±3% practical band. It does not replace the confidence-aware table.

| JackVec compared with | JackVec faster | Within ±3% | JackVec slower |
|---|---:|---:|---:|
| Vec | 8 | 5 | 9 |
| ThinVec | 8 | 7 | 7 |
| SmallVec4 | 16 | 2 | 4 |
| SmallVec8 | 15 | 3 | 4 |
| Vec | 9 | 6 | 7 |
| ThinVec | 8 | 10 | 4 |
| SmallVec4 | 13 | 4 | 5 |
| SmallVec8 | 13 | 4 | 5 |

## Memory outcomes

Expand All @@ -75,11 +73,14 @@ and spill counts.

## Run provenance

- Commit: `1f6e5f8abac7152d6ef3523baaed63096ac88b0a`
- Commit: `2da08eea786c9755fa6ac883026ebfc8eeeb904e`
- Compiler: `1.97.0` (`2d8144b7880597b6e6d3dfd63a9a9efae3f533d3`)
- Allocator policy: `system`; inherited injection:
`{'DYLD_INSERT_LIBRARIES': None, 'LD_PRELOAD': '/usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4'}`; effective injection:
`{'DYLD_INSERT_LIBRARIES': None, 'LD_PRELOAD': None}`
- CPU rounds: 5; CPU rows: 110; allocation rows: 60
- Minimum pinned-core idle audit: 100.0%
- Maximum audited one-minute load: 1.12
- Maximum audited one-minute load: 1.18

The performance profile reports the fraction of workloads within each factor of
the fastest implementation for that workload. It is an aggregate view, not a
Expand Down
Loading
Loading