Problem
DeltaEncodingDecoder.decode (reader/decode/DeltaEncodingDecoder.java:66-94) routes the entire column through four row-scaled heap long[] arrays before finally writing one arena segment:
long[] basesAll = readLongs(ctx.decodeChildSegment(0, dtype, basesLen), (int) basesLen, ptype);
long[] deltasAll = readLongs(ctx.decodeChildSegment(1, dtype, deltasLen), (int) deltasLen, ptype);
...
long[] decoded = new long[(int) deltasLen];
...
long[] result = new long[(int) rowCount];
System.arraycopy(decoded, offset, result, 0, (int) rowCount);
MemorySegment seg = PrimitiveArrays.fromLongs(result, ptype, ctx.arena());
This is CLAUDE.md's allocation rule ("never new byte[] + heap arrays for decode output — always ctx.arena().allocate(...)") violated four times at row scale. Every value is widened to 8 bytes regardless of ptype, so an I8 delta column allocates 8× its natural width on the GC heap, three times over, plus the final off-heap segment.
decoded → result is additionally a pure duplicate: the only thing the second array adds is dropping offset leading elements, which System.arraycopy then pays a second full traversal to do.
Fix
Two independent steps, either useful alone:
-
Write into the arena directly. The per-chunk scratch (untransposedChunk, chunkBases, chunkDeltas, chunkUndelta — all FastLanes.CHUNK/lanes sized) is fine as-is: fixed-size, cache-resident, genuinely scratch. The row-scaled ones are not. decoded and result should collapse into a single ctx.arena().allocate(rowCount * ptype.byteSize()), with the untransposed chunk written straight to its final position at -offset and at the ptype's real width, dropping both the widening and the arraycopy. basesAll/deltasAll can be read from their segments in place rather than copied out.
-
Fix readLongs' hot loop (:116-134), which carries both hot-loop anti-patterns at once:
for (int i = 0; i < count; i++) {
long off = (i % cap) * elemSize; // modulo per element
out[i] = switch (ptype) { // non-uniform body per element
case I8 -> buf.get(ValueLayout.JAVA_BYTE, off);
...
};
}
Per CLAUDE.md, both must be hoisted: branch-split on cap == count to get a modulo-free fast path (the modulo only exists for the ConstantEncoding broadcast case), and hoist the ptype switch outside the loop into specialized uniform bodies. DictEncodingDecoder.expandU8 (:294-311) is the in-repo template for exactly this shape.
Context
Found in a sweep for remaining eager materializations after #329 / 7e0d6e7, alongside the vortex.runend string expansion, vortex.sequence, the vortex.dict primitive path, and vortex.patched. Unlike those, this one is not about adding a lazy carrier — delta genuinely has to reconstruct values — it is about not doing that reconstruction on the heap at 8 bytes per element.
Worth a JavaVsJniReadBenchmark-style before/after, since the same i % cap + per-element-switch pattern is what caused the 5–10× regressions recorded in CLAUDE.md (ed658b7→051a794→442021f).
Problem
DeltaEncodingDecoder.decode(reader/decode/DeltaEncodingDecoder.java:66-94) routes the entire column through four row-scaled heaplong[]arrays before finally writing one arena segment:This is CLAUDE.md's allocation rule ("never
new byte[]+ heap arrays for decode output — alwaysctx.arena().allocate(...)") violated four times at row scale. Every value is widened to 8 bytes regardless of ptype, so an I8 delta column allocates 8× its natural width on the GC heap, three times over, plus the final off-heap segment.decoded→resultis additionally a pure duplicate: the only thing the second array adds is droppingoffsetleading elements, whichSystem.arraycopythen pays a second full traversal to do.Fix
Two independent steps, either useful alone:
Write into the arena directly. The per-chunk scratch (
untransposedChunk,chunkBases,chunkDeltas,chunkUndelta— allFastLanes.CHUNK/lanessized) is fine as-is: fixed-size, cache-resident, genuinely scratch. The row-scaled ones are not.decodedandresultshould collapse into a singlectx.arena().allocate(rowCount * ptype.byteSize()), with the untransposed chunk written straight to its final position at-offsetand at the ptype's real width, dropping both the widening and thearraycopy.basesAll/deltasAllcan be read from their segments in place rather than copied out.Fix
readLongs' hot loop (:116-134), which carries both hot-loop anti-patterns at once:Per CLAUDE.md, both must be hoisted: branch-split on
cap == countto get a modulo-free fast path (the modulo only exists for theConstantEncodingbroadcast case), and hoist the ptype switch outside the loop into specialized uniform bodies.DictEncodingDecoder.expandU8(:294-311) is the in-repo template for exactly this shape.Context
Found in a sweep for remaining eager materializations after #329 / 7e0d6e7, alongside the
vortex.runendstring expansion,vortex.sequence, thevortex.dictprimitive path, andvortex.patched. Unlike those, this one is not about adding a lazy carrier — delta genuinely has to reconstruct values — it is about not doing that reconstruction on the heap at 8 bytes per element.Worth a
JavaVsJniReadBenchmark-style before/after, since the samei % cap+ per-element-switch pattern is what caused the 5–10× regressions recorded in CLAUDE.md (ed658b7→051a794→442021f).