Skip to content

Commit 089b6e3

Browse files
dfa1claude
andcommitted
perf(encoding): precompute FastLanes transpose/iterate index tables
transposeIndex and iterateIndex computed per-element % / / plus an ORDER[] indirection. In the delta transpose and (un)delta hot loops that dependency chain (div -> ORDER load -> mul) serializes scatter address generation, throttling how many scatter misses stay in flight. Replace with permutation tables built once in a static initializer: - TRANSPOSE[CHUNK] for transposeIndex - ITERATE_BASE[64] for iterateIndex (lane added per call) Public API unchanged. JMH (Apple M5, long[], FastLanesTransposeBenchmark) across L1 -> DRAM working sets: - transpose: 3.4x (L1) ... 1.7x (256 MB) - undelta: 1.6x (L1) ... 1.4x (256 MB) Win persists when memory-bound: same dst indices = same traffic, so the gain is memory-level parallelism, not bandwidth. Shift-reduction control variants in the benchmark show strength reduction alone recovers only part of it (~1.5x transpose, ~1.08x undelta) - the dominant cost is the dependent ORDER[] load, which only the table removes. Also drops the now-completed FastLanes optimization item from TODO.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7af0af2 commit 089b6e3

3 files changed

Lines changed: 214 additions & 11 deletions

File tree

TODO.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111

1212
- [ ] Performance tests must be peer reviewed
1313
- [ ] Run performance tests on other machines (I have access only to Apple M5)
14-
- [ ] **Optimize `FastLanes.transposeIndex(int)` / `iterateIndex(int, int)`** — per-element `%`/`/`
15-
violate the hot-loop rule; called once per element in the delta transpose loops
16-
(`DeltaEncodingDecoder`, `DeltaEncodingEncoder`). Divisors are power-of-two constants (16/8/128);
17-
replace with shifts/masks or a precomputed permutation table. Profile first, benchmark both.
1814
- [ ] **Vector API adoption** — deferred; see [ADR-0005](docs/adr/0005-vector-api-adoption.md) for adoption criteria and candidate loops.
1915

2016
## Security

core/src/main/java/io/github/dfa1/vortex/encoding/FastLanes.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,51 @@ public final class FastLanes {
1919
/// The FastLanes transpose order — the lane permutation applied within each 8-row group.
2020
private static final int[] ORDER = {0, 4, 2, 6, 1, 5, 3, 7};
2121

22+
/// Precomputed logical-to-transposed permutation for one chunk (see [#transposeIndex(int)]).
23+
private static final int[] TRANSPOSE = new int[CHUNK];
24+
25+
/// Precomputed per-row base offsets for [#iterateIndex(int, int)]; the lane is added at use.
26+
/// Sized to the maximum row count (a 64-bit type has 64 rows per chunk).
27+
private static final int[] ITERATE_BASE = new int[64];
28+
29+
static {
30+
for (int idx = 0; idx < CHUNK; idx++) {
31+
int lane = idx % 16;
32+
int order = (idx / 16) % 8;
33+
int row = idx / 128;
34+
TRANSPOSE[idx] = lane * 64 + ORDER[order] * 8 + row;
35+
}
36+
for (int row = 0; row < ITERATE_BASE.length; row++) {
37+
ITERATE_BASE[row] = ORDER[row / 8] * 16 + (row % 8) * 128;
38+
}
39+
}
40+
2241
private FastLanes() {
2342
}
2443

2544
/// Maps a logical element index to its position in the transposed (interleaved-lane) layout.
2645
///
46+
/// The mapping is precomputed into a per-chunk table; the lookup avoids the per-element
47+
/// division and `ORDER` indirection that would otherwise serialize address generation in the
48+
/// transpose hot loop.
49+
///
2750
/// @param idx logical element index within a chunk, in `[0, CHUNK)`
2851
/// @return the corresponding index in the transposed buffer
2952
public static int transposeIndex(int idx) {
30-
int lane = idx % 16;
31-
int order = (idx / 16) % 8;
32-
int row = idx / 128;
33-
return lane * 64 + ORDER[order] * 8 + row;
53+
return TRANSPOSE[idx];
3454
}
3555

3656
/// Computes the logical element index visited at the given `row` and `lane` of the FastLanes
3757
/// iteration order — the inverse mapping used while packing or unpacking.
3858
///
59+
/// The row-dependent part is precomputed; only the lane is added per call, keeping the
60+
/// pack/unpack inner loop free of division and `ORDER` indirection.
61+
///
3962
/// @param row the row within the chunk
4063
/// @param lane the lane within the row
4164
/// @return the logical element index
4265
public static int iterateIndex(int row, int lane) {
43-
int o = row / 8;
44-
int s = row % 8;
45-
return ORDER[o] * 16 + s * 128 + lane;
66+
return ITERATE_BASE[row] + lane;
4667
}
4768

4869
/// Returns the FastLanes lane count for `ptype` — [#CHUNK] divided by the type's bit width.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package io.github.dfa1.vortex.performance;
2+
3+
import org.openjdk.jmh.annotations.Benchmark;
4+
import org.openjdk.jmh.annotations.BenchmarkMode;
5+
import org.openjdk.jmh.annotations.Fork;
6+
import org.openjdk.jmh.annotations.Level;
7+
import org.openjdk.jmh.annotations.Measurement;
8+
import org.openjdk.jmh.annotations.Mode;
9+
import org.openjdk.jmh.annotations.OutputTimeUnit;
10+
import org.openjdk.jmh.annotations.Param;
11+
import org.openjdk.jmh.annotations.Scope;
12+
import org.openjdk.jmh.annotations.Setup;
13+
import org.openjdk.jmh.annotations.State;
14+
import org.openjdk.jmh.annotations.Warmup;
15+
import org.openjdk.jmh.infra.Blackhole;
16+
17+
import java.util.Random;
18+
import java.util.concurrent.TimeUnit;
19+
20+
/// Head-to-head micro-benchmark for the FastLanes index math used by the delta encoding's
21+
/// transpose and undelta loops, comparing the original per-element arithmetic
22+
/// (`%`/`/` + `ORDER[]` lookup) against the precomputed permutation tables now shipped in
23+
/// `FastLanes`.
24+
///
25+
/// Both kernels permute chunk-by-chunk directly into a large destination array, so the
26+
/// working set scales with `size`: at `size == 1024` it is a single L1-resident chunk
27+
/// (pure index-math cost), and at large `size` the scatter spans L2/SLC/DRAM (memory-bound,
28+
/// where the win shrinks but persists because faster address generation keeps more scatter
29+
/// misses in flight). The crossover is the whole point of the sweep.
30+
///
31+
/// Run: ./bench FastLanesTransposeBenchmark
32+
@State(Scope.Benchmark)
33+
@BenchmarkMode(Mode.AverageTime)
34+
@OutputTimeUnit(TimeUnit.MICROSECONDS)
35+
@Warmup(iterations = 3, time = 2)
36+
@Measurement(iterations = 5, time = 2)
37+
@Fork(1)
38+
public class FastLanesTransposeBenchmark {
39+
40+
private static final int CHUNK = 1024;
41+
private static final int[] ORDER = {0, 4, 2, 6, 1, 5, 3, 7};
42+
43+
/// Precomputed logical-to-transposed permutation for one chunk.
44+
private static final int[] TRANSPOSE = new int[CHUNK];
45+
/// Precomputed `iterateIndex` base per row for the 64-bit (typeBits == 64) path.
46+
private static final int[] ITERATE_BASE = new int[64];
47+
48+
static {
49+
for (int i = 0; i < CHUNK; i++) {
50+
int lane = i % 16;
51+
int order = (i / 16) % 8;
52+
int row = i / 128;
53+
TRANSPOSE[i] = lane * 64 + ORDER[order] * 8 + row;
54+
}
55+
for (int row = 0; row < 64; row++) {
56+
ITERATE_BASE[row] = ORDER[row / 8] * 16 + (row % 8) * 128;
57+
}
58+
}
59+
60+
/// Working-set sizes: 8 KB (L1) -> 256 MB (DRAM) in `long` elements.
61+
@Param({"1024", "32768", "262144", "2097152", "8388608", "33554432"})
62+
private int size;
63+
64+
private long[] src;
65+
private long[] dst;
66+
private int numChunks;
67+
68+
@Setup(Level.Trial)
69+
public void setup() {
70+
numChunks = size / CHUNK;
71+
src = new long[size];
72+
dst = new long[size];
73+
Random random = new Random(42);
74+
for (int i = 0; i < size; i++) {
75+
src[i] = random.nextLong();
76+
}
77+
}
78+
79+
private static int transposeIndex(int idx) {
80+
int lane = idx % 16;
81+
int order = (idx / 16) % 8;
82+
int row = idx / 128;
83+
return lane * 64 + ORDER[order] * 8 + row;
84+
}
85+
86+
private static int transposeIndexShift(int idx) {
87+
int lane = idx & 15;
88+
int order = (idx >> 4) & 7;
89+
int row = idx >> 7;
90+
return lane * 64 + ORDER[order] * 8 + row;
91+
}
92+
93+
@Benchmark
94+
public void transposeArithmetic(Blackhole bh) {
95+
for (int chunk = 0; chunk < numChunks; chunk++) {
96+
int base = chunk * CHUNK;
97+
for (int i = 0; i < CHUNK; i++) {
98+
dst[base + transposeIndex(i)] = src[base + i];
99+
}
100+
}
101+
bh.consume(dst);
102+
}
103+
104+
@Benchmark
105+
public void transposeTable(Blackhole bh) {
106+
for (int chunk = 0; chunk < numChunks; chunk++) {
107+
int base = chunk * CHUNK;
108+
for (int i = 0; i < CHUNK; i++) {
109+
dst[base + TRANSPOSE[i]] = src[base + i];
110+
}
111+
}
112+
bh.consume(dst);
113+
}
114+
115+
@Benchmark
116+
public void transposeShift(Blackhole bh) {
117+
for (int chunk = 0; chunk < numChunks; chunk++) {
118+
int base = chunk * CHUNK;
119+
for (int i = 0; i < CHUNK; i++) {
120+
dst[base + transposeIndexShift(i)] = src[base + i];
121+
}
122+
}
123+
bh.consume(dst);
124+
}
125+
126+
@Benchmark
127+
public void undeltaArithmetic(Blackhole bh) {
128+
int lanes = 16;
129+
int typeBits = 64;
130+
for (int chunk = 0; chunk < numChunks; chunk++) {
131+
int base = chunk * CHUNK;
132+
for (int lane = 0; lane < lanes; lane++) {
133+
long prev = src[base + lane];
134+
for (int row = 0; row < typeBits; row++) {
135+
int o = row / 8;
136+
int s = row % 8;
137+
int idx = ORDER[o] * 16 + s * 128 + lane;
138+
long next = src[base + idx] + prev;
139+
dst[base + idx] = next;
140+
prev = next;
141+
}
142+
}
143+
}
144+
bh.consume(dst);
145+
}
146+
147+
@Benchmark
148+
public void undeltaShift(Blackhole bh) {
149+
int lanes = 16;
150+
int typeBits = 64;
151+
for (int chunk = 0; chunk < numChunks; chunk++) {
152+
int base = chunk * CHUNK;
153+
for (int lane = 0; lane < lanes; lane++) {
154+
long prev = src[base + lane];
155+
for (int row = 0; row < typeBits; row++) {
156+
int o = row >> 3;
157+
int s = row & 7;
158+
int idx = ORDER[o] * 16 + (s << 7) + lane;
159+
long next = src[base + idx] + prev;
160+
dst[base + idx] = next;
161+
prev = next;
162+
}
163+
}
164+
}
165+
bh.consume(dst);
166+
}
167+
168+
@Benchmark
169+
public void undeltaTable(Blackhole bh) {
170+
int lanes = 16;
171+
int typeBits = 64;
172+
for (int chunk = 0; chunk < numChunks; chunk++) {
173+
int base = chunk * CHUNK;
174+
for (int lane = 0; lane < lanes; lane++) {
175+
long prev = src[base + lane];
176+
for (int row = 0; row < typeBits; row++) {
177+
int idx = ITERATE_BASE[row] + lane;
178+
long next = src[base + idx] + prev;
179+
dst[base + idx] = next;
180+
prev = next;
181+
}
182+
}
183+
}
184+
bh.consume(dst);
185+
}
186+
}

0 commit comments

Comments
 (0)