Skip to content

Commit b604f21

Browse files
dfa1claude
andcommitted
feat(reader): lazy Sparse decode via top-level records
Replace the eager primitive Sparse expansion with six top-level record types in reader.array: LazySparseLongArray, LazySparseIntArray, LazySparseDoubleArray, LazySparseFloatArray, LazySparseShortArray, LazySparseByteArray. Each holds (dtype, length, fillValue, patchValues, patchIndices, offset) and resolves getXxx(i) via binary search over patchIndices through the new package-private SparseArrays helper. forEachXxx and fold walk the patches in order, emitting fillValue for runs of unpatched positions — O(numPatches) work plus length emissions, not O(length × allocation + patch scatter). SparseArrays (package-private) centralises readPatchIdx (indices-type switch) and two binary searches: findPatch (exact hit or -1 miss) for scalar access and findFirstAtOrAfter for the forEach run-walker. Bool and VarBin Sparse paths stay eager — Bool's bit-packing and VarBin's offset rebasing don't trivially express as patch lookup on read. The lazy switch only fires for I64/U64, I32/U32, F64, F32, I16/U16, I8/U8. SparseEncodingDecoder.decode now decodes patch children via decodeChild (returning Arrays) instead of decodeChildSegment + manual scatter. Dropped the dead fillSegment / applyPatches / readElem / writeElem helpers. LazySparseShortArray and LazySparseByteArray each carry both a raw signed fill (for the typed getShort/getByte) and a widened int fill (for the getInt path) so unsigned U16/U8 narrowing is preserved at the lazy boundary. 10 new unit tests in LazySparseArrayTest cover fill vs patch dispatch, ordered forEach iteration, fold reduction, offset slicing, and the no-patches degenerate case. ./mvnw verify green (13 modules, integration suite 41s). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c59e2f6 commit b604f21

9 files changed

Lines changed: 613 additions & 71 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.github.dfa1.vortex.reader.array;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
5+
import java.util.function.LongBinaryOperator;
6+
7+
/// Lazy Sparse-encoded {@link ByteArray}. See {@link LazySparseLongArray} for semantics.
8+
///
9+
/// @param dtype logical element type
10+
/// @param length total logical row count
11+
/// @param fillValue value at every unpatched position (raw signed byte)
12+
/// @param fillInt value at every unpatched position widened to int (unsigned-aware for U8)
13+
/// @param patchValues values for patched positions
14+
/// @param patchIndices sorted absolute positions of patches
15+
/// @param offset starting absolute position
16+
public record LazySparseByteArray(
17+
DType dtype, long length, byte fillValue, int fillInt,
18+
ByteArray patchValues, Array patchIndices, long offset)
19+
implements ByteArray {
20+
21+
@Override
22+
public byte getByte(long i) {
23+
int p = SparseArrays.findPatch(patchIndices, patchValues.length(), i + offset);
24+
return p >= 0 ? patchValues.getByte(p) : fillValue;
25+
}
26+
27+
@Override
28+
public int getInt(long i) {
29+
int p = SparseArrays.findPatch(patchIndices, patchValues.length(), i + offset);
30+
return p >= 0 ? patchValues.getInt(p) : fillInt;
31+
}
32+
33+
@Override
34+
public long fold(long identity, LongBinaryOperator op) {
35+
long[] acc = {identity};
36+
long numPatches = patchValues.length();
37+
long absStart = offset;
38+
long absEnd = offset + length;
39+
int p = SparseArrays.findFirstAtOrAfter(patchIndices, numPatches, absStart);
40+
long pos = absStart;
41+
while (pos < absEnd && p < numPatches) {
42+
long patchAbs = SparseArrays.readPatchIdx(patchIndices, p);
43+
if (patchAbs >= absEnd) {
44+
break;
45+
}
46+
for (long r = pos; r < patchAbs; r++) {
47+
acc[0] = op.applyAsLong(acc[0], fillInt);
48+
}
49+
acc[0] = op.applyAsLong(acc[0], patchValues.getInt(p));
50+
pos = patchAbs + 1;
51+
p++;
52+
}
53+
for (long r = pos; r < absEnd; r++) {
54+
acc[0] = op.applyAsLong(acc[0], fillInt);
55+
}
56+
return acc[0];
57+
}
58+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.github.dfa1.vortex.reader.array;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
5+
import java.util.function.DoubleBinaryOperator;
6+
import java.util.function.DoubleConsumer;
7+
8+
/// Lazy Sparse-encoded {@link DoubleArray}. See {@link LazySparseLongArray} for semantics.
9+
///
10+
/// @param dtype logical element type
11+
/// @param length total logical row count
12+
/// @param fillValue value at every unpatched position
13+
/// @param patchValues values for patched positions
14+
/// @param patchIndices sorted absolute positions of patches
15+
/// @param offset starting absolute position
16+
public record LazySparseDoubleArray(
17+
DType dtype, long length, double fillValue,
18+
DoubleArray patchValues, Array patchIndices, long offset)
19+
implements DoubleArray {
20+
21+
@Override
22+
public double getDouble(long i) {
23+
int p = SparseArrays.findPatch(patchIndices, patchValues.length(), i + offset);
24+
return p >= 0 ? patchValues.getDouble(p) : fillValue;
25+
}
26+
27+
@Override
28+
public void forEachDouble(DoubleConsumer c) {
29+
long numPatches = patchValues.length();
30+
long absStart = offset;
31+
long absEnd = offset + length;
32+
int p = SparseArrays.findFirstAtOrAfter(patchIndices, numPatches, absStart);
33+
long pos = absStart;
34+
while (pos < absEnd && p < numPatches) {
35+
long patchAbs = SparseArrays.readPatchIdx(patchIndices, p);
36+
if (patchAbs >= absEnd) {
37+
break;
38+
}
39+
for (long r = pos; r < patchAbs; r++) {
40+
c.accept(fillValue);
41+
}
42+
c.accept(patchValues.getDouble(p));
43+
pos = patchAbs + 1;
44+
p++;
45+
}
46+
for (long r = pos; r < absEnd; r++) {
47+
c.accept(fillValue);
48+
}
49+
}
50+
51+
@Override
52+
public double fold(double identity, DoubleBinaryOperator op) {
53+
double[] acc = {identity};
54+
forEachDouble(v -> acc[0] = op.applyAsDouble(acc[0], v));
55+
return acc[0];
56+
}
57+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.github.dfa1.vortex.reader.array;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
5+
import java.util.function.DoubleBinaryOperator;
6+
7+
/// Lazy Sparse-encoded {@link FloatArray}. See {@link LazySparseLongArray} for semantics.
8+
///
9+
/// @param dtype logical element type
10+
/// @param length total logical row count
11+
/// @param fillValue value at every unpatched position
12+
/// @param patchValues values for patched positions
13+
/// @param patchIndices sorted absolute positions of patches
14+
/// @param offset starting absolute position
15+
public record LazySparseFloatArray(
16+
DType dtype, long length, float fillValue,
17+
FloatArray patchValues, Array patchIndices, long offset)
18+
implements FloatArray {
19+
20+
@Override
21+
public float getFloat(long i) {
22+
int p = SparseArrays.findPatch(patchIndices, patchValues.length(), i + offset);
23+
return p >= 0 ? patchValues.getFloat(p) : fillValue;
24+
}
25+
26+
@Override
27+
public double fold(double identity, DoubleBinaryOperator op) {
28+
double[] acc = {identity};
29+
long numPatches = patchValues.length();
30+
long absStart = offset;
31+
long absEnd = offset + length;
32+
int p = SparseArrays.findFirstAtOrAfter(patchIndices, numPatches, absStart);
33+
long pos = absStart;
34+
while (pos < absEnd && p < numPatches) {
35+
long patchAbs = SparseArrays.readPatchIdx(patchIndices, p);
36+
if (patchAbs >= absEnd) {
37+
break;
38+
}
39+
for (long r = pos; r < patchAbs; r++) {
40+
acc[0] = op.applyAsDouble(acc[0], fillValue);
41+
}
42+
acc[0] = op.applyAsDouble(acc[0], patchValues.getFloat(p));
43+
pos = patchAbs + 1;
44+
p++;
45+
}
46+
for (long r = pos; r < absEnd; r++) {
47+
acc[0] = op.applyAsDouble(acc[0], fillValue);
48+
}
49+
return acc[0];
50+
}
51+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.github.dfa1.vortex.reader.array;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
5+
import java.util.function.IntBinaryOperator;
6+
import java.util.function.IntConsumer;
7+
8+
/// Lazy Sparse-encoded {@link IntArray}. See {@link LazySparseLongArray} for semantics.
9+
///
10+
/// @param dtype logical element type
11+
/// @param length total logical row count
12+
/// @param fillValue value at every unpatched position
13+
/// @param patchValues values for patched positions
14+
/// @param patchIndices sorted absolute positions of patches
15+
/// @param offset starting absolute position
16+
public record LazySparseIntArray(
17+
DType dtype, long length, int fillValue,
18+
IntArray patchValues, Array patchIndices, long offset)
19+
implements IntArray {
20+
21+
@Override
22+
public int getInt(long i) {
23+
int p = SparseArrays.findPatch(patchIndices, patchValues.length(), i + offset);
24+
return p >= 0 ? patchValues.getInt(p) : fillValue;
25+
}
26+
27+
@Override
28+
public void forEachInt(IntConsumer c) {
29+
long numPatches = patchValues.length();
30+
long absStart = offset;
31+
long absEnd = offset + length;
32+
int p = SparseArrays.findFirstAtOrAfter(patchIndices, numPatches, absStart);
33+
long pos = absStart;
34+
while (pos < absEnd && p < numPatches) {
35+
long patchAbs = SparseArrays.readPatchIdx(patchIndices, p);
36+
if (patchAbs >= absEnd) {
37+
break;
38+
}
39+
for (long r = pos; r < patchAbs; r++) {
40+
c.accept(fillValue);
41+
}
42+
c.accept(patchValues.getInt(p));
43+
pos = patchAbs + 1;
44+
p++;
45+
}
46+
for (long r = pos; r < absEnd; r++) {
47+
c.accept(fillValue);
48+
}
49+
}
50+
51+
@Override
52+
public int fold(int identity, IntBinaryOperator op) {
53+
int[] acc = {identity};
54+
forEachInt(v -> acc[0] = op.applyAsInt(acc[0], v));
55+
return acc[0];
56+
}
57+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.github.dfa1.vortex.reader.array;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
5+
import java.util.function.LongBinaryOperator;
6+
import java.util.function.LongConsumer;
7+
8+
/// Lazy Sparse-encoded {@link LongArray}: {@code getLong(i) = patches[binSearch(i + offset)]
9+
/// or fillValue}. The {@code patchIndices} array is typed as {@link Array} because the
10+
/// indices ptype varies — backed by one of {@link ByteArray}, {@link ShortArray},
11+
/// {@link IntArray}, {@link LongArray}.
12+
///
13+
/// {@code forEachLong} / {@code fold} walk the patches in order, emitting {@code fillValue}
14+
/// for runs of unpatched positions — O(numPatches) binary-search-equivalent steps plus
15+
/// {@code length} emissions, not O(length × log(numPatches)).
16+
///
17+
/// @param dtype logical element type
18+
/// @param length total logical row count
19+
/// @param fillValue value at every unpatched position
20+
/// @param patchValues values for patched positions; length = {@code numPatches}
21+
/// @param patchIndices sorted absolute positions of patches; length = {@code numPatches}
22+
/// @param offset starting absolute position; logical row {@code i} maps to
23+
/// absolute {@code i + offset}
24+
public record LazySparseLongArray(
25+
DType dtype, long length, long fillValue,
26+
LongArray patchValues, Array patchIndices, long offset)
27+
implements LongArray {
28+
29+
@Override
30+
public long getLong(long i) {
31+
int p = SparseArrays.findPatch(patchIndices, patchValues.length(), i + offset);
32+
return p >= 0 ? patchValues.getLong(p) : fillValue;
33+
}
34+
35+
@Override
36+
public void forEachLong(LongConsumer c) {
37+
long numPatches = patchValues.length();
38+
long absStart = offset;
39+
long absEnd = offset + length;
40+
int p = SparseArrays.findFirstAtOrAfter(patchIndices, numPatches, absStart);
41+
long pos = absStart;
42+
while (pos < absEnd && p < numPatches) {
43+
long patchAbs = SparseArrays.readPatchIdx(patchIndices, p);
44+
if (patchAbs >= absEnd) {
45+
break;
46+
}
47+
for (long r = pos; r < patchAbs; r++) {
48+
c.accept(fillValue);
49+
}
50+
c.accept(patchValues.getLong(p));
51+
pos = patchAbs + 1;
52+
p++;
53+
}
54+
for (long r = pos; r < absEnd; r++) {
55+
c.accept(fillValue);
56+
}
57+
}
58+
59+
@Override
60+
public long fold(long identity, LongBinaryOperator op) {
61+
long[] acc = {identity};
62+
forEachLong(v -> acc[0] = op.applyAsLong(acc[0], v));
63+
return acc[0];
64+
}
65+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.github.dfa1.vortex.reader.array;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
5+
import java.util.function.LongBinaryOperator;
6+
7+
/// Lazy Sparse-encoded {@link ShortArray}. See {@link LazySparseLongArray} for semantics.
8+
///
9+
/// @param dtype logical element type
10+
/// @param length total logical row count
11+
/// @param fillValue value at every unpatched position (raw signed short)
12+
/// @param fillInt value at every unpatched position widened to int (unsigned-aware for U16)
13+
/// @param patchValues values for patched positions
14+
/// @param patchIndices sorted absolute positions of patches
15+
/// @param offset starting absolute position
16+
public record LazySparseShortArray(
17+
DType dtype, long length, short fillValue, int fillInt,
18+
ShortArray patchValues, Array patchIndices, long offset)
19+
implements ShortArray {
20+
21+
@Override
22+
public short getShort(long i) {
23+
int p = SparseArrays.findPatch(patchIndices, patchValues.length(), i + offset);
24+
return p >= 0 ? patchValues.getShort(p) : fillValue;
25+
}
26+
27+
@Override
28+
public int getInt(long i) {
29+
int p = SparseArrays.findPatch(patchIndices, patchValues.length(), i + offset);
30+
return p >= 0 ? patchValues.getInt(p) : fillInt;
31+
}
32+
33+
@Override
34+
public long fold(long identity, LongBinaryOperator op) {
35+
long[] acc = {identity};
36+
long numPatches = patchValues.length();
37+
long absStart = offset;
38+
long absEnd = offset + length;
39+
int p = SparseArrays.findFirstAtOrAfter(patchIndices, numPatches, absStart);
40+
long pos = absStart;
41+
while (pos < absEnd && p < numPatches) {
42+
long patchAbs = SparseArrays.readPatchIdx(patchIndices, p);
43+
if (patchAbs >= absEnd) {
44+
break;
45+
}
46+
for (long r = pos; r < patchAbs; r++) {
47+
acc[0] = op.applyAsLong(acc[0], fillInt);
48+
}
49+
acc[0] = op.applyAsLong(acc[0], patchValues.getInt(p));
50+
pos = patchAbs + 1;
51+
p++;
52+
}
53+
for (long r = pos; r < absEnd; r++) {
54+
acc[0] = op.applyAsLong(acc[0], fillInt);
55+
}
56+
return acc[0];
57+
}
58+
}

0 commit comments

Comments
 (0)