diff --git a/valhalla-lab/reproducers/R11-observed.txt b/valhalla-lab/reproducers/R11-observed.txt new file mode 100644 index 0000000..c421fea --- /dev/null +++ b/valhalla-lab/reproducers/R11-observed.txt @@ -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. diff --git a/valhalla-lab/reproducers/R11_LayoutIsASchema.java b/valhalla-lab/reproducers/R11_LayoutIsASchema.java new file mode 100644 index 0000000..0bb7e61 --- /dev/null +++ b/valhalla-lab/reproducers/R11_LayoutIsASchema.java @@ -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."); + } + } +} diff --git a/valhalla-lab/reproducers/README.md b/valhalla-lab/reproducers/README.md index a3950b3..488b7e6 100644 --- a/valhalla-lab/reproducers/README.md +++ b/valhalla-lab/reproducers/README.md @@ -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.