Problem
RleEncodingDecoder returns lazy LazyRleXxxArray carriers — but populates them by copying the run values and the run indices out of their mmapped segments into heap arrays first (reader/decode/RleEncodingDecoder.java:98-119):
MemorySegment valuesSeg = ctx.decodeChildSegment(0, valuesDtype, valuesLen);
Array result = switch (ptype) {
case I64, U64 -> new LazyRleLongArray(ctx.dtype(), rowCount,
readLongs(valuesSeg, (int) valuesLen, ptype), // -> long[]
indices, valuesIdxOffsets, firstOffset, valuesLen, numChunks, offset);
...
};
with indices itself already a heap int[] from readIndices(ctx.materialize(indicesArr), ...) (:75).
So the array is lazy with respect to rows — which is the big win, and it is already there — but not with respect to the compressed payload: each of readLongs/readInts/readShorts/readBytes/readDoubles/readFloats/readIndices (:144-208) allocates a heap array sized by the run count and copies the segment into it, widening I8/I16 values to their boxed-width Java primitive along the way.
This is the mildest of the allocation findings — sized by numRuns, not rowCount — but it is an off-heap→heap copy of data that is already resident and directly indexable, and it defeats the zero-copy memory model for the one part of an RLE column that actually holds data.
Fix
Have the LazyRleXxxArray records hold the MemorySegment (plus its ptype/capacity) rather than a heap array, reading values through the segment in getXxx. That keeps the carrier zero-copy end to end and removes seven readXxx helpers.
This touches the LazyRle*Array record shapes, so it is a wider change than the other items in this sweep and mostly matters for columns with many short runs.
Two things worth folding in while there:
- Every one of the
readXxx helpers has an i % cap in its loop body (:149, :163, :174, :187, :196, :205, :217), and readLongs additionally has a per-element switch (ptype) whose only non-default case is I64, U64. Per CLAUDE.md's hot-loop rule these need branch-splitting on cap == count; the modulo exists solely for the ConstantEncoding broadcast path.
readDoubles/readFloats are deliberately package-private so the cap < count branch is testable directly (see the comment at :180-182). Whatever replaces them should keep that branch reachable from tests.
Context
Found in a sweep for remaining eager materializations after #329 / 7e0d6e7. Lowest priority of the set — the row-scale laziness is already correct here, unlike the vortex.runend string expansion, vortex.sequence, and the vortex.dict primitive path, which have no laziness at all.
Problem
RleEncodingDecoderreturns lazyLazyRleXxxArraycarriers — but populates them by copying the run values and the run indices out of their mmapped segments into heap arrays first (reader/decode/RleEncodingDecoder.java:98-119):with
indicesitself already a heapint[]fromreadIndices(ctx.materialize(indicesArr), ...)(:75).So the array is lazy with respect to rows — which is the big win, and it is already there — but not with respect to the compressed payload: each of
readLongs/readInts/readShorts/readBytes/readDoubles/readFloats/readIndices(:144-208) allocates a heap array sized by the run count and copies the segment into it, widening I8/I16 values to their boxed-width Java primitive along the way.This is the mildest of the allocation findings — sized by
numRuns, notrowCount— but it is an off-heap→heap copy of data that is already resident and directly indexable, and it defeats the zero-copy memory model for the one part of an RLE column that actually holds data.Fix
Have the
LazyRleXxxArrayrecords hold theMemorySegment(plus its ptype/capacity) rather than a heap array, reading values through the segment ingetXxx. That keeps the carrier zero-copy end to end and removes sevenreadXxxhelpers.This touches the
LazyRle*Arrayrecord shapes, so it is a wider change than the other items in this sweep and mostly matters for columns with many short runs.Two things worth folding in while there:
readXxxhelpers has ani % capin its loop body (:149,:163,:174,:187,:196,:205,:217), andreadLongsadditionally has a per-elementswitch (ptype)whose only non-default case isI64, U64. Per CLAUDE.md's hot-loop rule these need branch-splitting oncap == count; the modulo exists solely for theConstantEncodingbroadcast path.readDoubles/readFloatsare deliberately package-private so thecap < countbranch is testable directly (see the comment at:180-182). Whatever replaces them should keep that branch reachable from tests.Context
Found in a sweep for remaining eager materializations after #329 / 7e0d6e7. Lowest priority of the set — the row-scale laziness is already correct here, unlike the
vortex.runendstring expansion,vortex.sequence, and thevortex.dictprimitive path, which have no laziness at all.