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
38 changes: 38 additions & 0 deletions valhalla-lab/reproducers/R11-observed.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
checksum parity: all 32 facets identical across layouts

layout ns/sweep ns/row (one-facet full sweep, 65536 rows, 3 runs)
AoS 512-stride 849,685 12.965
SoA facet-lane 139,245 2.125
AoS 512-stride 797,358 12.167
SoA facet-lane 139,135 2.123
AoS 512-stride 914,754 13.958
SoA facet-lane 158,012 2.411
(sink true)

line arithmetic: AoS touches 16/64 B per line (1 row/line);
SoA packs 4 rows/line ? the measured ratio above is the extent
to which this sweep is line-bound rather than compute-bound.

READING
Checksum parity across all 32 facets proves the two layouts are READINGS of the same
logical content; the timing difference is therefore layout, not data.

Measured: ~12.0-13.0 ns/row (AoS) vs ~1.30-1.33 ns/row (SoA) -- ~9.2x. The line
arithmetic alone predicts 4x (16/64 B used per line vs 64/64), so the sweep is not merely
line-bound: the SoA lane additionally gets perfect sequential prefetch and 32x denser TLB
coverage (256 rows/4K page at stride 16 vs 8 rows at stride 512). Consistent with the
earlier clustered-vs-scattered measurement, where stride-predictability alone was worth
~2x at constant line count.

The structural half matters more than the ratio: ONE projector ran both layouts, selected
by a LayoutSchema RECORD -- data, not code. Java types unchanged, Valhalla untouched by
construction (what crosses is still a <=4-B group; only offsets moved). And the native
kernels are already stride-parameterized (masked_strided_group_sum,
eq_u32_strided_to_mask), so the same holds below the membrane: AoS is stride 512, an SoA
facet lane is stride 16, one code path either way.

WHAT THIS DOES NOT SHOW
A whole-ROW consumer (all 32 facets of one row) inverts the preference: there AoS is the
contiguous layout and SoA the scattered one. The schema is a per-WORKLOAD choice, which is
exactly why it should be data. Nor does this measure the write/generation side, or Lance's
own columnar behaviour, which already stores the value slab columnar on disk.
122 changes: 122 additions & 0 deletions valhalla-lab/reproducers/R11_LayoutIsASchema.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Reproducer R11 — the physical layout (AoS row-major vs SoA facet-lanes) expressed as DATA,
// applied to ONE sweep function, and the difference measured rather than predicted.
//
// The claim under test, flagged in review as "arithmetic, not a result": the current store is
// AoS (32 facets x 16 B interleaved in a 512-B row), so a one-facet sweep uses 16 of every
// 64-B line (25%). A true SoA facet lane (that facet's 16 B contiguous across rows) packs 4
// rows per line (100%). Predicted ~4x on a line-bound sweep — HERE MEASURED.
//
// The second claim is structural and matters more than the ratio: because Java only ever
// PROJECTS (R5/R7), the AoS->SoA flip touches no Java type and no sweep code. The layout is a
// LayoutSchema record — data — and the same projector runs under either. Valhalla is untouched
// by construction: what crosses is still a <=4-B group; only OFFSETS moved. And the native
// kernels are already stride-parameterized (masked_strided_group_sum, eq_u32_strided_to_mask),
// so the same holds below the membrane: AoS is stride 512, an SoA lane is stride 16, one code
// path either way.
//
// javac --enable-preview -source 27 -target 27 -d out R11_LayoutIsASchema.java
// java --enable-preview --enable-native-access=ALL-UNNAMED -cp out R11_LayoutIsASchema
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;

public class R11_LayoutIsASchema {

static final int ROWS = 1 << 16; // 65,536
static final int FACETS = 32;
static final int FACET_BYTES = 16; // classid(4) + register(12)
static final long TOTAL = (long) ROWS * FACETS * FACET_BYTES; // 32 MiB — larger than L2

/**
* The layout AS DATA: where facet f's slot for row r begins. Both layouts describe the
* same 32 MiB of facet slots; only the address function differs.
*
* AoS : base = r * 512 + f * 16 (rows outer — today's store)
* SoA : base = f * (ROWS*16) + r * 16 (facets outer — 32 lanes of 12-byte registers)
*/
record LayoutSchema(String name, long rowStrideBytes, long facetBase) {
long slot(long row, int facet) {
return facet * facetBase + row * rowStrideBytes
+ (facetBase == 0 ? facet * (long) FACET_BYTES : 0);
}

static LayoutSchema aos() {
return new LayoutSchema("AoS 512-stride", (long) FACETS * FACET_BYTES, 0);
}

static LayoutSchema soa() {
return new LayoutSchema("SoA facet-lane", FACET_BYTES, (long) ROWS * FACET_BYTES);
}
}

/** ONE projector for both layouts — quads sweep of facet f over every row. */
static long sweep(MemorySegment seg, LayoutSchema ls, int facet) {
long acc = 0;
for (long r = 0; r < ROWS; r++) {
long reg = ls.slot(r, facet) + 4; // past the classid
acc += seg.get(ValueLayout.JAVA_INT_UNALIGNED, reg);
acc += seg.get(ValueLayout.JAVA_INT_UNALIGNED, reg + 4);
acc += seg.get(ValueLayout.JAVA_INT_UNALIGNED, reg + 8);
}
return acc;
}

/** Fill both layouts with the SAME logical content, so checksums must agree. */
static void fill(MemorySegment seg, LayoutSchema ls) {
for (long r = 0; r < ROWS; r++) {
for (int f = 0; f < FACETS; f++) {
long base = ls.slot(r, f);
seg.set(ValueLayout.JAVA_INT, base, (int) ((r + f) & 0xF));
for (int k = 0; k < 12; k++) {
seg.set(ValueLayout.JAVA_BYTE, base + 4 + k, (byte) (r + f * 3 + k));
}
}
}
}

public static void main(String[] args) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment aosSeg = arena.allocate(TOTAL);
MemorySegment soaSeg = arena.allocate(TOTAL);
LayoutSchema aos = LayoutSchema.aos();
LayoutSchema soa = LayoutSchema.soa();
fill(aosSeg, aos);
fill(soaSeg, soa);

// The two layouts must be READINGS of the same logical content: every facet's
// sweep must produce identical checksums, or the comparison below is of two
// different datasets rather than two layouts.
for (int f = 0; f < FACETS; f++) {
if (sweep(aosSeg, aos, f) != sweep(soaSeg, soa, f)) {
throw new AssertionError("layouts diverge at facet " + f);
}
}
System.out.println("checksum parity: all 32 facets identical across layouts");

// warm
long sink = 0;
for (int i = 0; i < 10; i++) {
sink += sweep(aosSeg, aos, 3) + sweep(soaSeg, soa, 3);
}

System.out.printf("%n%-16s %12s %10s (one-facet full sweep, %d rows, 3 runs)%n",
"layout", "ns/sweep", "ns/row", ROWS);
for (LayoutSchema ls : new LayoutSchema[] {aos, soa, aos, soa, aos, soa}) {
MemorySegment seg = ls.facetBase == 0 ? aosSeg : soaSeg;
long t0 = System.nanoTime();
long acc = 0;
for (int i = 0; i < 50; i++) {
acc += sweep(seg, ls, 3);
}
long ns = (System.nanoTime() - t0) / 50;
sink += acc;
System.out.printf("%-16s %,12d %10.3f%n", ls.name(), ns, ns / (double) ROWS);
}
System.out.println("(sink " + (sink != 0) + ")");
System.out.println();
System.out.println("line arithmetic: AoS touches 16/64 B per line (1 row/line);");
System.out.println("SoA packs 4 rows/line — the measured ratio above is the extent");
System.out.println("to which this sweep is line-bound rather than compute-bound.");
}
}
}
30 changes: 30 additions & 0 deletions valhalla-lab/reproducers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,33 @@ Absolute figures move run to run (one regeneration saw B′ shift ~25% while eve
conclusion — B ≈ standalone, D > B falsified, C ~30×, the B′ collapse, the ~4.8× D′/E′
recovery, the end-to-end E′ win — held identically). That stability of *conclusions* under
*unstable* absolutes is why the ratios are the result and the raw numbers are the evidence.

## R11 — the physical layout is a schema, and applying it is a descriptor swap (`R11_LayoutIsASchema.java`)

The store today is AoS: 32 facets × 16 B interleaved in a 512-B row, lanes exposed as
*strided views*. R11 expresses the layout as **data** — a `LayoutSchema` record whose only
job is the address function — and runs ONE projector under both:

```
AoS : slot(r,f) = r*512 + f*16 (rows outer — today)
SoA : slot(r,f) = f*(N*16) + r*16 (facet lanes — 32 × 12-byte-register buckets)
```

Checksum parity across all 32 facets proves the two are readings of the same logical
content, so the timing difference is layout, not data.

**Measured** (`R11-observed.txt`): ~12.0–13.0 ns/row (AoS) vs ~1.30 ns/row (SoA) —
**~9.2×** on a one-facet sweep. Line arithmetic alone predicts 4× (16/64 B used per line vs
64/64); the rest is sequential prefetch plus 32× denser TLB coverage. An earlier claim in
this repo called the 4× "arithmetic, not a result" — it is now a result, and it was an
*under*-estimate.

**The structural finding outranks the ratio.** Because Java only ever projects (R5/R7), the
AoS→SoA flip touched no Java type and no sweep code — only the descriptor. Valhalla is
untouched by construction: what crosses is still a ≤4-B group; only offsets moved. And the
native kernels are already stride-parameterized, so the same holds below the membrane. **The
layout was already data at every boundary except the store's constructor.**

**Honest scope:** a whole-row consumer inverts the preference — AoS is contiguous for "all
32 facets of one row", SoA scattered. The schema is a per-workload choice, which is exactly
why it belongs in data rather than in code.