Skip to content

LazyRleXxxArray carriers hold heap copies of run values/indices instead of MemorySegments #342

Description

@dfa1

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions