Skip to content

Commit ec6b963

Browse files
dfa1claude
andcommitted
refactor(encoding): extract shared FastLanes layout + PType.bits to core
FL_CHUNK_SIZE/FL_ORDER + transposeIndex/iterateIndex/lanes were duplicated in the Delta encoder and decoder; the low-bit typeMask and the byteSize*8 width were also copy-pasted across Delta, Bitpacked and Patched. Pull the FastLanes layout into a shared core.encoding.FastLanes (CHUNK, transposeIndex, iterateIndex, lanes, lowMask) and add PType.bits() for the width. Cross-module (reader + writer) so the home is core, mirroring PTypeIO. Hot paths deliberately untouched: Bitpacked keeps its own FL_ORDER constant and the unrolled pack/unpack kernels are byte-identical — only the cold per-call typeMask / width setup now routes through FastLanes. Delta's transposeIndex/iterateIndex were already standalone static calls, so moving them across a class boundary does not change inlining. Pco is excluded (not FastLanes-family, perf-critical). Ground truth green both directions: RustWritesJavaReads (12), JavaWritesRustReads (213), JavaRoundTrip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ed6ebef commit ec6b963

7 files changed

Lines changed: 109 additions & 98 deletions

File tree

core/src/main/java/io/github/dfa1/vortex/core/PType.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ public int byteSize() {
4141
};
4242
}
4343

44+
/// Number of bits per element — [#byteSize()] times eight (8, 16, 32, or 64).
45+
///
46+
/// @return the bit width of this physical type
47+
public int bits() {
48+
return byteSize() * 8;
49+
}
50+
4451
/// Returns `true` for `F16`, `F32`, and `F64`.
4552
///
4653
/// @return `true` if this ptype is a floating-point type
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.github.dfa1.vortex.encoding;
2+
3+
import io.github.dfa1.vortex.core.PType;
4+
5+
/// Shared FastLanes layout constants and index math used by the bit-packing and delta encodings on
6+
/// both the read and write sides.
7+
///
8+
/// FastLanes processes values in fixed 1024-element chunks ([#CHUNK]) arranged into an interleaved
9+
/// lane order ([#ORDER]) so that the unpack inner loop is data-parallel. [#transposeIndex(int)] and
10+
/// [#iterateIndex(int, int)] map between the logical element order and that interleaved layout;
11+
/// [#lanes(PType)] is the lane count for a width and [#lowMask(int)] the low-`bits` value mask.
12+
///
13+
/// Mirrors the reference layout in `spiraldb/fastlanes` (`src/macros.rs`).
14+
public final class FastLanes {
15+
16+
/// Number of elements per FastLanes chunk.
17+
public static final int CHUNK = 1024;
18+
19+
/// The FastLanes transpose order — the lane permutation applied within each 8-row group.
20+
private static final int[] ORDER = {0, 4, 2, 6, 1, 5, 3, 7};
21+
22+
private FastLanes() {
23+
}
24+
25+
/// Maps a logical element index to its position in the transposed (interleaved-lane) layout.
26+
///
27+
/// @param idx logical element index within a chunk, in `[0, CHUNK)`
28+
/// @return the corresponding index in the transposed buffer
29+
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;
34+
}
35+
36+
/// Computes the logical element index visited at the given `row` and `lane` of the FastLanes
37+
/// iteration order — the inverse mapping used while packing or unpacking.
38+
///
39+
/// @param row the row within the chunk
40+
/// @param lane the lane within the row
41+
/// @return the logical element index
42+
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;
46+
}
47+
48+
/// Returns the FastLanes lane count for `ptype` — [#CHUNK] divided by the type's bit width.
49+
///
50+
/// @param ptype the physical type being packed
51+
/// @return the number of lanes
52+
public static int lanes(PType ptype) {
53+
return CHUNK / ptype.bits();
54+
}
55+
56+
/// Returns a mask selecting the low `bits` of a `long` (all ones when `bits == 64`).
57+
///
58+
/// @param bits the number of low bits to keep, in `[1, 64]`
59+
/// @return the low-`bits` mask
60+
public static long lowMask(int bits) {
61+
return bits == 64 ? -1L : (1L << bits) - 1;
62+
}
63+
}

reader/src/main/java/io/github/dfa1/vortex/reader/decode/BitpackedEncodingDecoder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public Array decode(DecodeContext ctx) {
6464
int bitWidth = meta.bit_width();
6565
int offset = meta.offset();
6666
PType ptype = ((DType.Primitive) ctx.dtype()).ptype();
67-
int typeBits = ptype.byteSize() * 8;
67+
int typeBits = ptype.bits();
6868
long rowCount = ctx.rowCount();
6969

7070
MemorySegment packed = ctx.buffer(0);

reader/src/main/java/io/github/dfa1/vortex/reader/decode/DeltaEncodingDecoder.java

Lines changed: 15 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.github.dfa1.vortex.core.PType;
55
import io.github.dfa1.vortex.core.VortexException;
66
import io.github.dfa1.vortex.encoding.EncodingId;
7+
import io.github.dfa1.vortex.encoding.FastLanes;
78
import io.github.dfa1.vortex.encoding.PrimitiveArrays;
89
import io.github.dfa1.vortex.encoding.PTypeIO;
910
import io.github.dfa1.vortex.proto.DeltaMetadata;
@@ -58,9 +59,9 @@ public Array decode(DecodeContext ctx) {
5859

5960
PType ptype = ((DType.Primitive) ctx.dtype()).ptype();
6061
long rowCount = ctx.rowCount();
61-
int typeBits = typeBits(ptype);
62-
int lanes = lanes(ptype);
63-
long mask = typeMask(ptype);
62+
int typeBits = ptype.bits();
63+
int lanes = FastLanes.lanes(ptype);
64+
long mask = FastLanes.lowMask(ptype.bits());
6465

6566
long deltasLen = meta.deltas_len();
6667
int offset = meta.offset();
@@ -76,32 +77,32 @@ public Array decode(DecodeContext ctx) {
7677
};
7778
}
7879

79-
long basesLen = (deltasLen / FL_CHUNK_SIZE) * lanes;
80+
long basesLen = (deltasLen / FastLanes.CHUNK) * lanes;
8081
DType dtype = ctx.dtype();
8182

8283
long[] basesAll = readLongs(ctx.decodeChildSegment(0, dtype, basesLen), (int) basesLen, ptype);
8384
long[] deltasAll = readLongs(ctx.decodeChildSegment(1, dtype, deltasLen), (int) deltasLen, ptype);
8485

85-
int numChunks = (int) (deltasLen / FL_CHUNK_SIZE);
86+
int numChunks = (int) (deltasLen / FastLanes.CHUNK);
8687
long[] decoded = new long[(int) deltasLen];
87-
long[] untransposedChunk = new long[FL_CHUNK_SIZE];
88+
long[] untransposedChunk = new long[FastLanes.CHUNK];
8889
long[] chunkBases = new long[lanes];
89-
long[] chunkDeltas = new long[FL_CHUNK_SIZE];
90-
long[] chunkUndelta = new long[FL_CHUNK_SIZE];
90+
long[] chunkDeltas = new long[FastLanes.CHUNK];
91+
long[] chunkUndelta = new long[FastLanes.CHUNK];
9192

9293
for (int chunk = 0; chunk < numChunks; chunk++) {
9394
int basesOff = chunk * lanes;
94-
int deltaOff = chunk * FL_CHUNK_SIZE;
95+
int deltaOff = chunk * FastLanes.CHUNK;
9596

9697
System.arraycopy(basesAll, basesOff, chunkBases, 0, lanes);
97-
System.arraycopy(deltasAll, deltaOff, chunkDeltas, 0, FL_CHUNK_SIZE);
98+
System.arraycopy(deltasAll, deltaOff, chunkDeltas, 0, FastLanes.CHUNK);
9899

99100
undeltaChunk(chunkDeltas, chunkBases, lanes, typeBits, mask, chunkUndelta);
100101

101-
for (int i = 0; i < FL_CHUNK_SIZE; i++) {
102-
untransposedChunk[transposeIndex(i)] = chunkUndelta[i];
102+
for (int i = 0; i < FastLanes.CHUNK; i++) {
103+
untransposedChunk[FastLanes.transposeIndex(i)] = chunkUndelta[i];
103104
}
104-
System.arraycopy(untransposedChunk, 0, decoded, deltaOff, FL_CHUNK_SIZE);
105+
System.arraycopy(untransposedChunk, 0, decoded, deltaOff, FastLanes.CHUNK);
105106
}
106107

107108
long[] result = new long[(int) rowCount];
@@ -121,7 +122,7 @@ private static void undeltaChunk(long[] deltas, long[] bases, int lanes, int typ
121122
for (int lane = 0; lane < lanes; lane++) {
122123
long prev = bases[lane] & mask;
123124
for (int row = 0; row < typeBits; row++) {
124-
int idx = iterateIndex(row, lane);
125+
int idx = FastLanes.iterateIndex(row, lane);
125126
long next = ((deltas[idx] & mask) + prev) & mask;
126127
out[idx] = next;
127128
prev = next;
@@ -149,34 +150,5 @@ private static long[] readLongs(MemorySegment buf, int count, PType ptype) {
149150
return out;
150151
}
151152

152-
private static final int FL_CHUNK_SIZE = 1024;
153-
154-
private static final int[] FL_ORDER = {0, 4, 2, 6, 1, 5, 3, 7};
155-
156-
private static int transposeIndex(int idx) {
157-
int lane = idx % 16;
158-
int order = (idx / 16) % 8;
159-
int row = idx / 128;
160-
return lane * 64 + FL_ORDER[order] * 8 + row;
161-
}
162-
163-
private static int iterateIndex(int row, int lane) {
164-
int o = row / 8;
165-
int s = row % 8;
166-
return FL_ORDER[o] * 16 + s * 128 + lane;
167-
}
168-
169-
private static int lanes(PType ptype) {
170-
return FL_CHUNK_SIZE / (ptype.byteSize() * 8);
171-
}
172-
173-
private static int typeBits(PType ptype) {
174-
return ptype.byteSize() * 8;
175-
}
176-
177-
private static long typeMask(PType ptype) {
178-
int bits = ptype.byteSize() * 8;
179-
return bits == 64 ? -1L : (1L << bits) - 1;
180-
}
181153

182154
}

writer/src/main/java/io/github/dfa1/vortex/writer/encode/BitpackedEncodingEncoder.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.github.dfa1.vortex.core.PType;
55
import io.github.dfa1.vortex.core.VortexException;
66
import io.github.dfa1.vortex.encoding.EncodingId;
7+
import io.github.dfa1.vortex.encoding.FastLanes;
78
import io.github.dfa1.vortex.encoding.PrimitiveArrays;
89
import io.github.dfa1.vortex.encoding.PTypeIO;
910
import io.github.dfa1.vortex.proto.BitPackedMetadata;
@@ -46,8 +47,8 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
4647
PType ptype = ((DType.Primitive) dtype).ptype();
4748
long[] longs = PrimitiveArrays.toLongs(data, ptype, EncodingId.FASTLANES_BITPACKED);
4849
int n = longs.length;
49-
int typeBits = ptype.byteSize() * 8;
50-
long typeMask = typeMask(typeBits);
50+
int typeBits = ptype.bits();
51+
long typeMask = FastLanes.lowMask(typeBits);
5152
boolean unsign = ptype.isUnsigned();
5253

5354
long signedMin = 0L;
@@ -198,7 +199,7 @@ private static MemorySegment packFastLanes(long[] values, int n, int bitWidth, i
198199
int lanes = 1024 / typeBits;
199200
int wordBytes = typeBits / 8;
200201
int blockCount = (n + 1023) / 1024;
201-
long typeMask = typeMask(typeBits);
202+
long typeMask = FastLanes.lowMask(typeBits);
202203
// Mask values to the chosen bit width so over-cap entries (handled separately as
203204
// patches) don't spill into the next row's region in the packed layout.
204205
long widthMask = bitWidth >= 64 ? -1L : (1L << bitWidth) - 1L;
@@ -239,9 +240,6 @@ private static MemorySegment packFastLanes(long[] values, int n, int bitWidth, i
239240
}
240241

241242

242-
private static long typeMask(int typeBits) {
243-
return typeBits == 64 ? -1L : (1L << typeBits) - 1L;
244-
}
245243

246244
private static byte[] statsBytes(PType ptype, long value) {
247245
if (ptype.isUnsigned()) {

writer/src/main/java/io/github/dfa1/vortex/writer/encode/DeltaEncodingEncoder.java

Lines changed: 16 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.github.dfa1.vortex.core.DType;
44
import io.github.dfa1.vortex.core.PType;
55
import io.github.dfa1.vortex.encoding.EncodingId;
6+
import io.github.dfa1.vortex.encoding.FastLanes;
67
import io.github.dfa1.vortex.encoding.PrimitiveArrays;
78
import io.github.dfa1.vortex.proto.DeltaMetadata;
89
import io.github.dfa1.vortex.proto.ScalarValue;
@@ -39,9 +40,9 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
3940
PType ptype = ((DType.Primitive) dtype).ptype();
4041
long[] longs = PrimitiveArrays.toLongs(data, ptype, EncodingId.FASTLANES_DELTA);
4142
int n = longs.length;
42-
int typeBits = typeBits(ptype);
43-
int lanes = lanes(ptype);
44-
long mask = typeMask(ptype);
43+
int typeBits = ptype.bits();
44+
int lanes = FastLanes.lanes(ptype);
45+
long mask = FastLanes.lowMask(ptype.bits());
4546
boolean unsign = ptype.isUnsigned();
4647

4748
long minVal = 0L;
@@ -60,34 +61,34 @@ public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
6061
}
6162
}
6263

63-
int numChunks = n == 0 ? 0 : (n + FL_CHUNK_SIZE - 1) / FL_CHUNK_SIZE;
64-
long paddedLen = (long) numChunks * FL_CHUNK_SIZE;
64+
int numChunks = n == 0 ? 0 : (n + FastLanes.CHUNK - 1) / FastLanes.CHUNK;
65+
long paddedLen = (long) numChunks * FastLanes.CHUNK;
6566
int basesLen = numChunks * lanes;
6667

6768
long[] basesAll = new long[basesLen];
6869
long[] deltasAll = new long[(int) paddedLen];
69-
long[] chunkBuf = new long[FL_CHUNK_SIZE];
70-
long[] transposed = new long[FL_CHUNK_SIZE];
70+
long[] chunkBuf = new long[FastLanes.CHUNK];
71+
long[] transposed = new long[FastLanes.CHUNK];
7172
long[] chunkBases = new long[lanes];
72-
long[] chunkDelta = new long[FL_CHUNK_SIZE];
73+
long[] chunkDelta = new long[FastLanes.CHUNK];
7374

7475
for (int chunk = 0; chunk < numChunks; chunk++) {
75-
int start = chunk * FL_CHUNK_SIZE;
76-
int end = Math.min(start + FL_CHUNK_SIZE, n);
76+
int start = chunk * FastLanes.CHUNK;
77+
int end = Math.min(start + FastLanes.CHUNK, n);
7778
for (int i = start; i < end; i++) {
7879
chunkBuf[i - start] = longs[i] & mask;
7980
}
80-
for (int i = end - start; i < FL_CHUNK_SIZE; i++) {
81+
for (int i = end - start; i < FastLanes.CHUNK; i++) {
8182
chunkBuf[i] = 0L;
8283
}
83-
for (int i = 0; i < FL_CHUNK_SIZE; i++) {
84-
transposed[i] = chunkBuf[transposeIndex(i)];
84+
for (int i = 0; i < FastLanes.CHUNK; i++) {
85+
transposed[i] = chunkBuf[FastLanes.transposeIndex(i)];
8586
}
8687
int basesOff = chunk * lanes;
8788
System.arraycopy(transposed, 0, basesAll, basesOff, lanes);
8889
System.arraycopy(basesAll, basesOff, chunkBases, 0, lanes);
8990
deltaChunk(transposed, chunkBases, lanes, typeBits, mask, chunkDelta);
90-
System.arraycopy(chunkDelta, 0, deltasAll, chunk * FL_CHUNK_SIZE, FL_CHUNK_SIZE);
91+
System.arraycopy(chunkDelta, 0, deltasAll, chunk * FastLanes.CHUNK, FastLanes.CHUNK);
9192
}
9293

9394
MemorySegment basesSeg = PrimitiveArrays.fromLongs(basesAll, ptype, ctx.arena());
@@ -109,7 +110,7 @@ private static void deltaChunk(long[] transposed, long[] bases, int lanes, int t
109110
for (int lane = 0; lane < lanes; lane++) {
110111
long prev = bases[lane] & mask;
111112
for (int row = 0; row < typeBits; row++) {
112-
int idx = iterateIndex(row, lane);
113+
int idx = FastLanes.iterateIndex(row, lane);
113114
long next = transposed[idx] & mask;
114115
out[idx] = (next - prev) & mask;
115116
prev = next;
@@ -124,35 +125,4 @@ private static byte[] statsBytes(PType ptype, long value) {
124125
return ScalarValue.ofInt64Value(value).encode();
125126
}
126127

127-
private static final int FL_CHUNK_SIZE = 1024;
128-
129-
private static final int[] FL_ORDER = {0, 4, 2, 6, 1, 5, 3, 7};
130-
131-
private static int transposeIndex(int idx) {
132-
int lane = idx % 16;
133-
int order = (idx / 16) % 8;
134-
int row = idx / 128;
135-
return lane * 64 + FL_ORDER[order] * 8 + row;
136-
}
137-
138-
private static int iterateIndex(int row, int lane) {
139-
int o = row / 8;
140-
int s = row % 8;
141-
return FL_ORDER[o] * 16 + s * 128 + lane;
142-
}
143-
144-
private static int lanes(PType ptype) {
145-
return FL_CHUNK_SIZE / (ptype.byteSize() * 8);
146-
}
147-
148-
private static int typeBits(PType ptype) {
149-
return ptype.byteSize() * 8;
150-
}
151-
152-
private static long typeMask(PType ptype) {
153-
int bits = ptype.byteSize() * 8;
154-
return bits == 64 ? -1L : (1L << bits) - 1;
155-
}
156-
157-
158128
}

writer/src/main/java/io/github/dfa1/vortex/writer/encode/PatchedEncodingEncoder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.github.dfa1.vortex.core.PType;
55
import io.github.dfa1.vortex.core.VortexException;
66
import io.github.dfa1.vortex.encoding.EncodingId;
7+
import io.github.dfa1.vortex.encoding.FastLanes;
78
import io.github.dfa1.vortex.encoding.PrimitiveArrays;
89
import io.github.dfa1.vortex.encoding.PTypeIO;
910
import io.github.dfa1.vortex.proto.PatchedMetadata;
@@ -140,8 +141,8 @@ static EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
140141
}
141142

142143
private static PatchedData computePatchedData(long[] longs, PType ptype, int n) {
143-
int typeBits = ptype.byteSize() * 8;
144-
long typeMask = typeBits == 64 ? -1L : (1L << typeBits) - 1L;
144+
int typeBits = ptype.bits();
145+
long typeMask = FastLanes.lowMask(typeBits);
145146
int elemBytes = ptype.byteSize();
146147

147148
int[] bitWidthFreq = new int[typeBits + 1];

0 commit comments

Comments
 (0)