Skip to content

Commit cf0afc8

Browse files
dfa1claude
andcommitted
refactor(compute): remove materialized Mask/kernel path
The fused single-pass kernels (filteredSum, filteredAggregate) fully subsume the value-level compute machinery, so drop it: the selection Mask, Reductions, FilterKernel, ReduceKernel, StreamingFilterKernel, Bits, and the bulk of PrimitiveFilter — a materialized selection bitmap is a strictly slower primitive the reduce must re-scan. Extract the two helpers the fused kernels still need: NumericColumns (dtype-aware widen/unwrap/validity) and PredicateEvaluator (per-element fallback). Public Compute surface is now just filteredSum + filteredAggregate. None of the removed types ever shipped past v0.11.0, so the CHANGELOG drops the never-released deprecation churn and records filteredAggregate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a741dcd commit cf0afc8

20 files changed

Lines changed: 345 additions & 3116 deletions

CHANGELOG.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- `Compute.filteredSum(filterColumn, predicate, aggColumn)` fuses a filter and a sum into a single scan — a row folds into the total only when the predicate selects it (a null filter row is excluded) and the aggregate value is non-null — with no intermediate selection bitmap. It matches a hand-written fused loop and is ~1.5× faster than the two-pass `filter` + `sum`. ([57d2225b](https://github.com/dfa1/vortex-java/commit/57d2225b))
13-
14-
### Deprecated
15-
16-
- The selection `Mask` and `Compute.filter(Array, Predicate, Arena)` are deprecated for removal: a materialized selection mask is a slower primitive than a fused single-pass scan (it builds a positional bitmap the reduce must re-scan). Prefer the fused `Compute.filteredSum` (and the forthcoming fused multi-column `filteredReduce`), which fold filter and aggregate in one pass with no intermediate bitmap. ([319cfd97](https://github.com/dfa1/vortex-java/commit/319cfd97))
13+
- `Compute.filteredAggregate(chunk, filter, aggColumn)` fuses a whole multi-column `RowFilter` (an n-ary `AND` of column-bound predicate leaves) and folds the selected rows' `SUM`/`MIN`/`MAX`/non-null count over an aggregate column in a single pass — the multi-column counterpart of `filteredSum`, and the row-level kernel behind the Calcite boundary-chunk aggregate push-down. A `null` aggregate column counts selected rows only (`COUNT(*)`). ([2ba54888](https://github.com/dfa1/vortex-java/commit/2ba54888))
1714

1815
## [0.11.0] — 2026-06-28
1916

performance/src/main/java/io/github/dfa1/vortex/performance/ComputeKernelBenchmark.java

Lines changed: 35 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@
1414
import io.github.dfa1.vortex.reader.array.OffsetDoubleArray;
1515
import io.github.dfa1.vortex.reader.array.OffsetLongArray;
1616
import io.github.dfa1.vortex.reader.compute.Compute;
17-
import io.github.dfa1.vortex.reader.compute.Mask;
1817
import io.github.dfa1.vortex.reader.compute.Predicate;
1918
import io.github.dfa1.vortex.writer.VortexWriter;
2019
import io.github.dfa1.vortex.writer.WriteOptions;
2120
import java.io.IOException;
22-
import java.lang.foreign.Arena;
2321
import java.nio.channels.FileChannel;
2422
import java.nio.file.Files;
2523
import java.nio.file.Path;
@@ -44,14 +42,13 @@
4442

4543
/// Baseline for the encoded-domain compute-kernel specialization of ADR 0013.
4644
///
47-
/// The compute kernels ([Compute#filter(Array, Predicate, Arena)] and
48-
/// [Compute#sum(Array, Mask)]) today decode every element through the typed accessor: the
49-
/// generic streaming filter path and the type-specialized, boxing-free reduce lane both read
50-
/// `getLong(i)` / `getDouble(i)` per row, so an ALP or Frame-of-Reference column is fully
51-
/// reconstructed into the value domain before a single comparison or addition runs. The future
52-
/// work compares and reduces directly in the encoded integer domain (ALP residuals, FoR offsets)
53-
/// without decoding. This benchmark pins the CURRENT decode-via-accessor cost so that win is
54-
/// provable: the same `@Benchmark` methods will show the speedup once the specialized kernels land.
45+
/// The fused compute kernel ([Compute#filteredSum(Array, Predicate, Array)]) today decodes every
46+
/// element through the typed accessor: it reads `getLong(i)` / `getDouble(i)` per row, so an ALP or
47+
/// Frame-of-Reference column is fully reconstructed into the value domain before a single comparison
48+
/// or addition runs. The future work compares and reduces directly in the encoded integer domain
49+
/// (ALP residuals, FoR offsets) without decoding. This benchmark pins the CURRENT decode-via-accessor
50+
/// cost so that win is provable: the same `@Benchmark` methods will show the speedup once the
51+
/// specialized kernels land.
5552
///
5653
/// One hundred million rows are written as `TOTAL_ROWS / CHUNK_ROWS` chunks of `CHUNK_ROWS` each
5754
/// with `WriteOptions.cascading(3)`, so the writer picks real encodings and the four columns decode
@@ -77,16 +74,17 @@
7774
/// `@Setup` asserts each decoded column (on the first chunk) is the expected encoded type and fails
7875
/// loudly otherwise, so the baseline can never silently measure a plain column.
7976
///
80-
/// Each `filterX`/`sumX` kernel method is paired with a `forLoopX` method holding the true control:
81-
/// the obvious hand-written accessor loop a developer writes WITHOUT the compute layer — no [Mask],
82-
/// no [Compute], no off-heap bitmap, just `getDouble(i)`/`getLong(i)` and a counter. The paired
83-
/// methods share the exact predicate and threshold constant so they cannot drift, giving three
84-
/// reference points:
77+
/// The fused kernel method `fusedFilteredSumAlp` is paired with a `forLoopFilteredSum` control: the
78+
/// obvious hand-written accessor loop a developer writes WITHOUT the compute layer — no off-heap
79+
/// bitmap, just `getDouble(i)` / `getLong(i)` and an accumulator. The standalone `forLoopX` baselines
80+
/// measure the naive decode-per-element scan of each encoded column on its own. The paired methods
81+
/// share the exact predicate and threshold constant so they cannot drift, giving the reference
82+
/// points:
8583
/// - `forLoopX` — the naive decode-per-element loop, the developer's baseline.
86-
/// - `filterX` — the current kernel, which still decodes through the accessor; the `forLoopX`→
87-
/// `filterX` gap is the kernel's overhead (or benefit) today.
88-
/// - the future encoded-domain specialization — measured against `forLoopX`, which it must beat by
89-
/// comparing and reducing in the integer domain instead of decoding every element.
84+
/// - `fusedFilteredSumAlp` — the current fused kernel, which still decodes through the accessor; the
85+
/// `forLoopFilteredSum`→`fusedFilteredSumAlp` gap is the kernel's overhead (or benefit) today.
86+
/// - the future encoded-domain specialization — measured against `forLoopFilteredSum`, which it must
87+
/// beat by comparing and reducing in the integer domain instead of decoding every element.
9088
///
9189
/// Run: java -jar performance/target/benchmarks.jar ComputeKernelBenchmark
9290
@State(Scope.Benchmark)
@@ -100,7 +98,6 @@
10098
"--sun-misc-unsafe-memory-access=allow",
10199
"-Xmx4g"
102100
})
103-
@SuppressWarnings("removal") // transitional — benchmarks the deprecated Mask / Compute.filter two-pass path against the fused single-pass kernel
104101
public class ComputeKernelBenchmark {
105102

106103
/// Total rows scanned per op — ≈ 800 MB per column, far larger than L3, so memory-bound.
@@ -203,107 +200,11 @@ public void tearDown() throws IOException {
203200
}
204201
}
205202

206-
/// Filters the ALP-encoded `price` column with `price > 500` across every chunk, decoding each
207-
/// double through the accessor before the compare. A per-chunk confined arena holds the mask and
208-
/// is freed each chunk, matching a realistic streaming scan. Returns the total selected count so
209-
/// the masks cannot be eliminated.
210-
///
211-
/// @return the number of selected rows over the whole dataset
212-
@Benchmark
213-
public long filterAlpDouble() {
214-
long count = 0;
215-
for (DoubleArray a : priceChunks) {
216-
try (Arena arena = Arena.ofConfined()) {
217-
count += Compute.filter(a, new Predicate.Gt(PRICE_THRESHOLD), arena).trueCount();
218-
}
219-
}
220-
return count;
221-
}
222-
223-
/// Filters the Frame-of-Reference-encoded `measure` column with `measure > base + spread/2`
224-
/// across every chunk, reconstructing each `offset + ref` long through the accessor before the
225-
/// compare. A per-chunk confined arena holds the mask and is freed each chunk.
226-
///
227-
/// @return the number of selected rows over the whole dataset
228-
@Benchmark
229-
public long filterForLong() {
230-
long count = 0;
231-
for (LongArray a : measureChunks) {
232-
try (Arena arena = Arena.ofConfined()) {
233-
count += Compute.filter(a, new Predicate.Gt(MEASURE_THRESHOLD), arena).trueCount();
234-
}
235-
}
236-
return count;
237-
}
238-
239-
/// Filters the dictionary-encoded `category` column with `category == 7` across every chunk,
240-
/// resolving each code through the dictionary before the compare. A per-chunk confined arena
241-
/// holds the mask and is freed each chunk.
242-
///
243-
/// @return the number of selected rows over the whole dataset
244-
@Benchmark
245-
public long filterDict() {
246-
long count = 0;
247-
for (LongArray a : categoryChunks) {
248-
try (Arena arena = Arena.ofConfined()) {
249-
count += Compute.filter(a, new Predicate.Eq(CATEGORY_VALUE), arena).trueCount();
250-
}
251-
}
252-
return count;
253-
}
254-
255-
/// Control: filters the plain (non-encoded) `plain` column with `plain > 0` across every chunk,
256-
/// reading each long straight from the materialized segment. Shows the cost without an encoding
257-
/// to unwind. A per-chunk confined arena holds the mask and is freed each chunk.
258-
///
259-
/// @return the number of selected rows over the whole dataset
260-
@Benchmark
261-
public long filterPlainControl() {
262-
long count = 0;
263-
for (LongArray a : plainChunks) {
264-
try (Arena arena = Arena.ofConfined()) {
265-
count += Compute.filter(a, new Predicate.Gt(0L), arena).trueCount();
266-
}
267-
}
268-
return count;
269-
}
270-
271-
/// Reduces the ALP-encoded `price` column over an all-selected mask across every chunk, the
272-
/// boxing-free reduce lane decoding each double through the accessor before the addition.
273-
///
274-
/// @return the sum of all `price` values over the whole dataset
275-
@Benchmark
276-
public double sumAlpDouble() {
277-
double acc = 0;
278-
for (DoubleArray a : priceChunks) {
279-
acc += Compute.sum(a, Mask.allTrue(a.length())).doubleValue();
280-
}
281-
return acc;
282-
}
283-
284-
/// Realistic pipeline: per chunk, filter the ALP-encoded `price` column, then sum the
285-
/// FoR-encoded `measure` column over the resulting mask, accumulating across every chunk. Both
286-
/// stages decode through the accessor today. Price and measure chunks are indexed in lockstep.
287-
///
288-
/// @return the sum of `measure` over the rows where `price > 500` across the whole dataset
289-
@Benchmark
290-
public long filterThenSumAlp() {
291-
long acc = 0;
292-
for (int k = 0; k < priceChunks.size(); k++) {
293-
DoubleArray priceArr = priceChunks.get(k);
294-
LongArray measureArr = measureChunks.get(k);
295-
try (Arena arena = Arena.ofConfined()) {
296-
Mask mask = Compute.filter(priceArr, new Predicate.Gt(PRICE_THRESHOLD), arena);
297-
acc += Compute.sum(measureArr, mask).longValue();
298-
}
299-
}
300-
return acc;
301-
}
302-
303203
/// Fused one-pass pipeline: per chunk, [Compute#filteredSum(Array, Predicate, Array)] filters the
304204
/// ALP-encoded `price` column with `price > 500` and totals the FoR-encoded `measure` column over
305-
/// the selected rows in a single scan, with no intermediate [Mask] and no off-heap bitmap. The
306-
/// one-pass counterpart to [#filterThenSumAlp()]; same semantics, same `price`/`measure` columns.
205+
/// the selected rows in a single scan, with no intermediate selection bitmap. Decodes each price
206+
/// and (when selected) each measure through the accessor. Price and measure chunks are indexed in
207+
/// lockstep.
307208
///
308209
/// @return the sum of `measure` over the rows where `price > 500` across the whole dataset
309210
@Benchmark
@@ -319,8 +220,8 @@ public long fusedFilteredSumAlp() {
319220

320221
/// Hand-fused control for [#fusedFilteredSumAlp()]: the obvious developer loop a fused kernel must
321222
/// match — `for i: if (price.getDouble(i) > 500) acc += measure.getLong(i)` per chunk, with no
322-
/// [Mask], no [Compute] and no off-heap bitmap. Decodes each price and (when selected) each
323-
/// measure through the accessor. Price and measure chunks are indexed in lockstep.
223+
/// off-heap bitmap. Decodes each price and (when selected) each measure through the accessor.
224+
/// Price and measure chunks are indexed in lockstep.
324225
///
325226
/// @return the sum of `measure` over the rows where `price > 500` across the whole dataset
326227
@Benchmark
@@ -339,9 +240,9 @@ public long forLoopFilteredSum() {
339240
return acc;
340241
}
341242

342-
/// Naive baseline for [#filterAlpDouble()]: the hand-written `price > 500` count loop over the
343-
/// ALP accessor across every chunk, with no [Mask], no [Compute] and no off-heap bitmap. Decodes
344-
/// each double per element. Returns the count so JMH cannot eliminate the loop.
243+
/// Naive `price > 500` count baseline: the hand-written count loop over the ALP accessor across
244+
/// every chunk, with no off-heap bitmap. Decodes each double per element. Returns the count so JMH
245+
/// cannot eliminate the loop.
345246
///
346247
/// @return the number of rows with `price > 500` over the whole dataset
347248
@Benchmark
@@ -358,9 +259,9 @@ public long forLoopAlpDouble() {
358259
return count;
359260
}
360261

361-
/// Naive baseline for [#filterForLong()]: the hand-written `measure > base + spread/2` count loop
362-
/// over the Frame-of-Reference accessor across every chunk, reconstructing each `offset + ref`
363-
/// long per element.
262+
/// Naive `measure > base + spread/2` count baseline: the hand-written count loop over the
263+
/// Frame-of-Reference accessor across every chunk, reconstructing each `offset + ref` long per
264+
/// element.
364265
///
365266
/// @return the number of rows with `measure > base + spread/2` over the whole dataset
366267
@Benchmark
@@ -377,9 +278,8 @@ public long forLoopForLong() {
377278
return count;
378279
}
379280

380-
/// Naive baseline for [#filterDict()]: the hand-written `category == 7` count loop over the
381-
/// dictionary accessor across every chunk, resolving each code through the dictionary per
382-
/// element.
281+
/// Naive `category == 7` count baseline: the hand-written count loop over the dictionary accessor
282+
/// across every chunk, resolving each code through the dictionary per element.
383283
///
384284
/// @return the number of rows with `category == 7` over the whole dataset
385285
@Benchmark
@@ -396,9 +296,8 @@ public long forLoopDict() {
396296
return count;
397297
}
398298

399-
/// Naive baseline for [#filterPlainControl()]: the hand-written `plain > 0` count loop over the
400-
/// materialized accessor across every chunk, reading each long straight from the segment per
401-
/// element.
299+
/// Naive `plain > 0` count baseline: the hand-written count loop over the materialized accessor
300+
/// across every chunk, reading each long straight from the segment per element.
402301
///
403302
/// @return the number of rows with `plain > 0` over the whole dataset
404303
@Benchmark
@@ -415,9 +314,9 @@ public long forLoopPlainControl() {
415314
return count;
416315
}
417316

418-
/// Naive baseline for [#sumAlpDouble()]: the hand-written running sum over the ALP accessor
419-
/// across every chunk, decoding each double per element. Returns the sum so JMH cannot eliminate
420-
/// the loop.
317+
/// Naive `price` running-sum baseline: the hand-written running sum over the ALP accessor across
318+
/// every chunk, decoding each double per element. Returns the sum so JMH cannot eliminate the
319+
/// loop.
421320
///
422321
/// @return the sum of all `price` values over the whole dataset
423322
@Benchmark

0 commit comments

Comments
 (0)