Skip to content

Commit df6530c

Browse files
dfa1claude
andcommitted
perf: hot-path optimizations in ALP/Dict/Bitpacked codecs
- AlpCodec: precompute single factor (1 FP mult/elem vs 2), strength-reduce loop offset, Math.toIntExact for 2GB safety - DictCodec: loop-unswitch codePType outside expand loop; three type-specific expandU8/U16/U32 with running outOff counter; Math.toIntExact - BitpackedCodec: running blockByteOff counter; hoist per-row invariants (o, s, baseIdx, wordBase, hiBase, loMask, hiMask) above lane loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a380a41 commit df6530c

3 files changed

Lines changed: 70 additions & 28 deletions

File tree

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,17 @@ private Array decodeF64(DecodeContext ctx, EncodingProtos.ALPMetadata meta, int
9090
DType encodedDtype = new DType.Primitive(PType.I64, false);
9191
Array encoded = decodeChildAs(ctx, 0, encodedDtype, n);
9292

93-
double f10 = F10_F64[expF];
94-
double if10 = IF10_F64[expE];
93+
// Precompute single factor — avoids 2 FP mults per element in the hot loop.
94+
double factor = F10_F64[expF] * IF10_F64[expE];
9595

96-
byte[] outBytes = new byte[(int) (n * 8)];
96+
byte[] outBytes = new byte[Math.toIntExact(n * 8)];
9797
ByteBuffer out = ByteBuffer.wrap(outBytes).order(ByteOrder.LITTLE_ENDIAN);
9898
var srcLayout = ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
9999
MemorySegment src = encoded.buffer(0);
100-
for (long i = 0; i < n; i++) {
101-
long encVal = src.get(srcLayout, i * 8);
102-
double dec = (double) encVal * f10 * if10;
100+
// Strength-reduce: running byte offset instead of i*8 per iteration.
101+
for (long off = 0, end = n * 8; off < end; off += 8) {
102+
long encVal = src.get(srcLayout, off);
103+
double dec = (double) encVal * factor;
103104
out.putDouble(dec);
104105
}
105106

@@ -143,16 +144,15 @@ private Array decodeF32(DecodeContext ctx, EncodingProtos.ALPMetadata meta, int
143144
DType encodedDtype = new DType.Primitive(PType.I32, false);
144145
Array encoded = decodeChildAs(ctx, 0, encodedDtype, n);
145146

146-
float f10 = F10_F32[expF];
147-
float if10 = IF10_F32[expE];
147+
float factor = F10_F32[expF] * IF10_F32[expE];
148148

149-
byte[] outBytes = new byte[(int) (n * 4)];
149+
byte[] outBytes = new byte[Math.toIntExact(n * 4)];
150150
ByteBuffer out = ByteBuffer.wrap(outBytes).order(ByteOrder.LITTLE_ENDIAN);
151151
var srcLayout = ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
152152
MemorySegment src = encoded.buffer(0);
153-
for (long i = 0; i < n; i++) {
154-
int encVal = src.get(srcLayout, i * 4);
155-
float dec = (float) encVal * f10 * if10;
153+
for (long off = 0, end = n * 4; off < end; off += 4) {
154+
int encVal = src.get(srcLayout, off);
155+
float dec = (float) encVal * factor;
156156
out.putFloat(dec);
157157
}
158158

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,10 @@ private static long[] fastlanesUnpack(
184184
int blockCount = (int) ((totalElems + 1023) / 1024);
185185
long bitMask = bitWidth == 64 ? -1L : (1L << bitWidth) - 1L;
186186

187-
for (int block = 0; block < blockCount; block++) {
188-
long blockByteOff = (long) block * 128L * bitWidth;
189-
int blockLogicStart = block * 1024 - offset;
187+
long blockByteOff = 0L;
188+
long blockByteStride = 128L * bitWidth;
189+
for (int block = 0; block < blockCount; block++, blockByteOff += blockByteStride) {
190+
int blockLogicStart = block * 1024 - offset;
190191

191192
for (int row = 0; row < typeBits; row++) {
192193
int currWord = (row * bitWidth) / typeBits;
@@ -195,25 +196,30 @@ private static long[] fastlanesUnpack(
195196
int remainingBits = (nextWord > currWord) ? ((row + 1) * bitWidth) % typeBits : 0;
196197
int currentBits = bitWidth - remainingBits;
197198

199+
// Hoist per-row invariants above the lane loop.
200+
int o = row / 8;
201+
int s = row % 8;
202+
int baseIdx = blockLogicStart + FL_ORDER[o] * 16 + s * 128;
203+
long wordBase = blockByteOff + (long) (LANES * currWord) * wordBytes;
204+
long hiBase = (remainingBits > 0)
205+
? blockByteOff + (long) (LANES * nextWord) * wordBytes : 0L;
206+
long loMask = (remainingBits > 0) ? (1L << currentBits) - 1L : 0L;
207+
long hiMask = (remainingBits > 0) ? (1L << remainingBits) - 1L : 0L;
208+
198209
for (int lane = 0; lane < LANES; lane++) {
199-
int o = row / 8;
200-
int s = row % 8;
201-
int logicalIdx = blockLogicStart + FL_ORDER[o] * 16 + s * 128 + lane;
210+
int logicalIdx = baseIdx + lane;
202211

203212
if (logicalIdx < 0 || logicalIdx >= rowCount) {
204213
continue;
205214
}
206215

207-
long wordOff = blockByteOff + (long) (LANES * currWord + lane) * wordBytes;
216+
long wordOff = wordBase + (long) lane * wordBytes;
208217
long src = readWordFromSeg(buf, wordOff, typeBits);
209218

210219
long value;
211220
if (remainingBits > 0) {
212-
long loMask = (1L << currentBits) - 1L;
213-
long lo = (src >>> shift) & loMask;
214-
long hiOff = blockByteOff + (long) (LANES * nextWord + lane) * wordBytes;
215-
long hi = readWordFromSeg(buf, hiOff, typeBits)
216-
& ((1L << remainingBits) - 1L);
221+
long lo = (src >>> shift) & loMask;
222+
long hi = readWordFromSeg(buf, hiBase + (long) lane * wordBytes, typeBits) & hiMask;
217223
value = lo | (hi << currentBits);
218224
} else {
219225
value = (src >>> shift) & bitMask;

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,15 @@ private Array decodeRustProto(DecodeContext ctx, byte[] metaBytes) {
140140
MemorySegment codesBuf = codesArr.buffer(0);
141141
MemorySegment valuesBuf = valuesArr.buffer(0);
142142

143-
byte[] expanded = new byte[(int) (rowCount * elemSize)];
143+
byte[] expanded = new byte[Math.toIntExact(rowCount * elemSize)];
144144
MemorySegment out = MemorySegment.ofArray(expanded);
145-
for (long i = 0; i < rowCount; i++) {
146-
long code = readCode(codesBuf, codePType, i);
147-
MemorySegment.copy(valuesBuf, code * elemSize, out, i * elemSize, elemSize);
145+
// Loop-unswitch: pull the codePType switch outside the hot loop so the JIT
146+
// sees a tight, type-specific loop with predictable branches.
147+
switch (codePType) {
148+
case U8 -> expandU8(codesBuf, valuesBuf, out, rowCount, elemSize);
149+
case U16 -> expandU16(codesBuf, valuesBuf, out, rowCount, elemSize);
150+
case U32 -> expandU32(codesBuf, valuesBuf, out, rowCount, elemSize);
151+
default -> throw new IllegalStateException("unexpected code type: " + codePType);
148152
}
149153
return new Array(ctx.dtype(), rowCount, new MemorySegment[]{out}, new Array[0], ArrayStats.empty());
150154
}
@@ -209,6 +213,38 @@ private static void writeCode(ByteBuffer buf, PType codePType, int code) {
209213
}
210214
}
211215

216+
private static void expandU8(
217+
MemorySegment codes, MemorySegment values, MemorySegment out,
218+
long rowCount, int elemSize
219+
) {
220+
for (long i = 0, outOff = 0; i < rowCount; i++, outOff += elemSize) {
221+
long code = Byte.toUnsignedLong(codes.get(ValueLayout.JAVA_BYTE, i));
222+
MemorySegment.copy(values, code * elemSize, out, outOff, elemSize);
223+
}
224+
}
225+
226+
private static void expandU16(
227+
MemorySegment codes, MemorySegment values, MemorySegment out,
228+
long rowCount, int elemSize
229+
) {
230+
var layout = ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
231+
for (long i = 0, outOff = 0; i < rowCount; i++, outOff += elemSize) {
232+
long code = Short.toUnsignedLong(codes.get(layout, i * 2));
233+
MemorySegment.copy(values, code * elemSize, out, outOff, elemSize);
234+
}
235+
}
236+
237+
private static void expandU32(
238+
MemorySegment codes, MemorySegment values, MemorySegment out,
239+
long rowCount, int elemSize
240+
) {
241+
var layout = ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
242+
for (long i = 0, outOff = 0; i < rowCount; i++, outOff += elemSize) {
243+
long code = Integer.toUnsignedLong(codes.get(layout, i * 4));
244+
MemorySegment.copy(values, code * elemSize, out, outOff, elemSize);
245+
}
246+
}
247+
212248
private static long readCode(MemorySegment buf, PType codePType, long i) {
213249
return switch (codePType) {
214250
case U8 -> Byte.toUnsignedLong(buf.get(ValueLayout.JAVA_BYTE, i));

0 commit comments

Comments
 (0)