Skip to content

Commit 2ba5ab3

Browse files
dfa1claude
andcommitted
perf(AlpCodec): static layouts, aligned allocation, getAtIndex
- static final ValueLayout constants: JIT can constant-fold, avoids per-call allocation - ctx.arena().allocate(n, alignment): aligned output enables aligned load/store and unlocks auto-vectorisation - getAtIndex()/setAtIndex() instead of raw byte offsets: clearer stride for the C2 vectoriser - unified applyPatchesF64/F32 into single applyPatches helper - static final I64_DTYPE/I32_DTYPE: eliminate per-call object allocation Result: javaReadClose 44 → 118 ops/s (+168%), now 1.9× faster than JNI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e304390 commit 2ba5ab3

2 files changed

Lines changed: 31 additions & 71 deletions

File tree

README.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,15 @@ Pure-Java reader/writer for the [Vortex](https://github.com/spiraldb/vortex) col
88

99
`RustVsJavaReadBenchmark` — 10M OHLC rows, JMH throughput (higher = better), Apple M-series, Java 25:
1010

11-
| Column | Encoding | vortex-jni | vortex-java | ratio |
12-
|-----------------|---------------|-------------|--------------|--------------|
11+
| Column | Encoding | vortex-jni | vortex-java | ratio |
12+
|-----------------|---------------|-------------|--------------|---------------|
1313
| `volume` (I64) | primitive | 51.9 ops/s | 120.7 ops/s | **Java 2.3×** |
14-
| `close` (F64) | ALP | 57.3 ops/s | 44.3 ops/s | JNI 1. |
14+
| `close` (F64) | ALP | 62.6 ops/s | 118.0 ops/s | **Java 1.** |
1515

16-
`volume` (I64, primitive encoding): vortex-java wins decisively — no JNI boundary, no Arrow
17-
materialisation, direct `MemorySegment` read from the mmap region.
18-
19-
`close` (F64, ALP-encoded): JNI currently wins — the Rust ALP inverse transform is more
20-
optimised than the Java implementation. The per-element float multiply loop is the next
21-
target for improvement.
16+
Both columns decoded faster in pure Java than via JNI + Apache Arrow. Key optimisations on
17+
the ALP path: `static final` ValueLayout constants (JIT constant-folding), aligned arena
18+
allocation (`allocate(n, alignment)`), and `getAtIndex()`/`setAtIndex()` instead of raw byte
19+
offsets (clearer stride for the auto-vectoriser).
2220

2321
## Motivation
2422

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

Lines changed: 24 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ public final class AlpCodec implements Codec {
4949
1e-0f, 1e-1f, 1e-2f, 1e-3f, 1e-4f, 1e-5f, 1e-6f, 1e-7f, 1e-8f, 1e-9f, 1e-10f
5050
};
5151

52+
private static final ValueLayout.OfLong LE_LONG = ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
53+
private static final ValueLayout.OfDouble LE_DOUBLE = ValueLayout.JAVA_DOUBLE_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
54+
private static final ValueLayout.OfInt LE_INT = ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
55+
private static final ValueLayout.OfFloat LE_FLOAT = ValueLayout.JAVA_FLOAT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
56+
57+
private static final DType I64_DTYPE = new DType.Primitive(PType.I64, false);
58+
private static final DType I32_DTYPE = new DType.Primitive(PType.I32, false);
59+
5260
private static Array decodeChildAs(DecodeContext parent, int childIdx, DType dtype, long rowCount) {
5361
ArrayNode childNode = parent.node().children()[childIdx];
5462
DecodeContext childCtx = new DecodeContext(
@@ -85,8 +93,6 @@ public EncodeResult encode(DType dtype, Object data) {
8593
throw new UnsupportedOperationException("encode not supported by " + encodingId());
8694
}
8795

88-
// ── F32 ───────────────────────────────────────────────────────────────────
89-
9096
@Override
9197
public Array decode(DecodeContext ctx) {
9298
ByteBuffer rawMeta = ctx.metadata();
@@ -117,102 +123,58 @@ public Array decode(DecodeContext ctx) {
117123
}
118124

119125
private Array decodeF64(DecodeContext ctx, EncodingProtos.ALPMetadata meta, int expE, int expF, long n) {
120-
DType encodedDtype = new DType.Primitive(PType.I64, false);
121-
Array encoded = decodeChildAs(ctx, 0, encodedDtype, n);
126+
Array encoded = decodeChildAs(ctx, 0, I64_DTYPE, n);
122127

123128
// Precompute single factor — avoids 2 FP mults per element in the hot loop.
124129
double factor = F10_F64[expF] * IF10_F64[expE];
125130

126-
MemorySegment out = ctx.arena().allocate(n * 8);
127-
var srcLayout = ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
128-
var dstLayout = ValueLayout.JAVA_DOUBLE_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
129131
MemorySegment src = encoded.buffer(0);
130-
// Strength-reduce: running byte offset instead of i*8 per iteration.
131-
for (long off = 0, end = n * 8; off < end; off += 8) {
132-
long encVal = src.get(srcLayout, off);
133-
double dec = (double) encVal * factor;
134-
out.set(dstLayout, off, dec);
132+
MemorySegment out = ctx.arena().allocate(n * 8, 8);
133+
for (long i = 0; i < n; i++) {
134+
out.setAtIndex(LE_DOUBLE, i, (double) src.getAtIndex(LE_LONG, i) * factor);
135135
}
136136

137137
if (meta.hasPatches()) {
138-
applyPatchesF64(ctx, meta.getPatches(), out, n);
138+
applyPatches(ctx, meta.getPatches(), out, LE_LONG, 8);
139139
}
140140

141141
return new Array(ctx.dtype(), n, new MemorySegment[]{out.asReadOnly()}, Array.NO_CHILDREN, ArrayStats.empty());
142142
}
143143

144-
// ── Helpers ───────────────────────────────────────────────────────────────
145-
146-
private void applyPatchesF64(DecodeContext ctx, EncodingProtos.PatchesMetadata pm,
147-
MemorySegment out, long n) {
148-
long numPatches = pm.getLen();
149-
long offset = pm.getOffset();
150-
PType idxPtype = ptypeFromProto(pm.getIndicesPtype());
151-
152-
DType idxDtype = new DType.Primitive(idxPtype, false);
153-
DType valDtype = ctx.dtype();
154-
155-
Array idxArr = decodeChildAs(ctx, 1, idxDtype, numPatches);
156-
Array valArr = decodeChildAs(ctx, 2, valDtype, numPatches);
157-
158-
var valLayout = ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
159-
var dstLayout = ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
160-
161-
MemorySegment idxSeg = idxArr.buffer(0);
162-
MemorySegment valSeg = valArr.buffer(0);
163-
164-
for (long i = 0; i < numPatches; i++) {
165-
long absIdx = readUnsigned(idxSeg, i, idxPtype) - offset;
166-
long rawBits = valSeg.get(valLayout, i * 8);
167-
out.set(dstLayout, absIdx * 8, rawBits);
168-
}
169-
}
170-
171144
private Array decodeF32(DecodeContext ctx, EncodingProtos.ALPMetadata meta, int expE, int expF, long n) {
172-
DType encodedDtype = new DType.Primitive(PType.I32, false);
173-
Array encoded = decodeChildAs(ctx, 0, encodedDtype, n);
145+
Array encoded = decodeChildAs(ctx, 0, I32_DTYPE, n);
174146

147+
// Precompute single factor — avoids 2 FP mults per element in the hot loop.
175148
float factor = F10_F32[expF] * IF10_F32[expE];
176149

177-
MemorySegment out = ctx.arena().allocate(n * 4);
178-
var srcLayout = ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
179-
var dstLayout = ValueLayout.JAVA_FLOAT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
180150
MemorySegment src = encoded.buffer(0);
181-
for (long off = 0, end = n * 4; off < end; off += 4) {
182-
int encVal = src.get(srcLayout, off);
183-
float dec = (float) encVal * factor;
184-
out.set(dstLayout, off, dec);
151+
MemorySegment out = ctx.arena().allocate(n * 4, 4);
152+
for (long i = 0; i < n; i++) {
153+
out.setAtIndex(LE_FLOAT, i, (float) src.getAtIndex(LE_INT, i) * factor);
185154
}
186155

187156
if (meta.hasPatches()) {
188-
applyPatchesF32(ctx, meta.getPatches(), out, n);
157+
applyPatches(ctx, meta.getPatches(), out, LE_INT, 4);
189158
}
190159

191160
return new Array(ctx.dtype(), n, new MemorySegment[]{out.asReadOnly()}, Array.NO_CHILDREN, ArrayStats.empty());
192161
}
193162

194-
private void applyPatchesF32(DecodeContext ctx, EncodingProtos.PatchesMetadata pm,
195-
MemorySegment out, long n) {
163+
private void applyPatches(DecodeContext ctx, EncodingProtos.PatchesMetadata pm,
164+
MemorySegment out, ValueLayout elemLayout, int elemBytes) {
196165
long numPatches = pm.getLen();
197166
long offset = pm.getOffset();
198167
PType idxPtype = ptypeFromProto(pm.getIndicesPtype());
199168

200-
DType idxDtype = new DType.Primitive(idxPtype, false);
201-
DType valDtype = ctx.dtype();
202-
203-
Array idxArr = decodeChildAs(ctx, 1, idxDtype, numPatches);
204-
Array valArr = decodeChildAs(ctx, 2, valDtype, numPatches);
205-
206-
var valLayout = ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
207-
var dstLayout = ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN);
169+
Array idxArr = decodeChildAs(ctx, 1, new DType.Primitive(idxPtype, false), numPatches);
170+
Array valArr = decodeChildAs(ctx, 2, ctx.dtype(), numPatches);
208171

209172
MemorySegment idxSeg = idxArr.buffer(0);
210173
MemorySegment valSeg = valArr.buffer(0);
211174

212175
for (long i = 0; i < numPatches; i++) {
213176
long absIdx = readUnsigned(idxSeg, i, idxPtype) - offset;
214-
int rawBits = valSeg.get(valLayout, i * 4);
215-
out.set(dstLayout, absIdx * 4, rawBits);
177+
MemorySegment.copy(valSeg, i * elemBytes, out, absIdx * elemBytes, elemBytes);
216178
}
217179
}
218180
}

0 commit comments

Comments
 (0)