1414import io .github .dfa1 .vortex .reader .array .OffsetDoubleArray ;
1515import io .github .dfa1 .vortex .reader .array .OffsetLongArray ;
1616import io .github .dfa1 .vortex .reader .compute .Compute ;
17- import io .github .dfa1 .vortex .reader .compute .Mask ;
1817import io .github .dfa1 .vortex .reader .compute .Predicate ;
1918import io .github .dfa1 .vortex .writer .VortexWriter ;
2019import io .github .dfa1 .vortex .writer .WriteOptions ;
2120import java .io .IOException ;
22- import java .lang .foreign .Arena ;
2321import java .nio .channels .FileChannel ;
2422import java .nio .file .Files ;
2523import java .nio .file .Path ;
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
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 )
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
104101public 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