Skip to content

Commit 2ac0e6c

Browse files
dfa1claude
andcommitted
perf(compute): add naive for-loop baselines to ComputeKernelBenchmark
The honest control is the obvious hand-written accessor loop a developer writes without the compute layer (for i: if a.getX(i) > t count++), per encoding, sharing the same thresholds as the Compute.filter methods. Steady-state (1M rows, Apple M5, us/op) reframes the optimisation target: forLoop vs Compute.filter — ALP 469 vs 4428, Dict 369 vs 3395, FoR 468 vs 2985, plain 467 vs 2883; sum 577 vs 578. The filter kernel is 6-9x slower than a naive count even on a plain column, while sum matches the loop — so the cost is the per-element off-heap bitmap write in the filter output, not decode (the for-loop decodes the same values, and ALP/FoR/plain loops cost the same: decode is ~free at steady state). This says the next lever is the filter mask-write path, not encoded-domain decode avoidance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dfe1bc4 commit 2ac0e6c

1 file changed

Lines changed: 95 additions & 0 deletions

File tree

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@
6161
/// `@Setup` asserts each decoded column is the expected encoded type and fails loudly otherwise,
6262
/// so the baseline can never silently measure a plain column.
6363
///
64+
/// Each `filterX`/`sumX` kernel method is paired with a `forLoopX` method holding the true control:
65+
/// the obvious hand-written accessor loop a developer writes WITHOUT the compute layer — no [Mask],
66+
/// no [Compute], no off-heap bitmap, just `getDouble(i)`/`getLong(i)` and a counter. The paired
67+
/// methods share the exact predicate and threshold constant so they cannot drift, giving three
68+
/// reference points:
69+
/// - `forLoopX` — the naive decode-per-element loop, the developer's baseline.
70+
/// - `filterX` — the current kernel, which still decodes through the accessor; the `forLoopX`→
71+
/// `filterX` gap is the kernel's overhead (or benefit) today.
72+
/// - the future encoded-domain specialisation — measured against `forLoopX`, which it must beat by
73+
/// comparing and reducing in the integer domain instead of decoding every element.
74+
///
6475
/// Run: java -jar performance/target/benchmarks.jar ComputeKernelBenchmark
6576
@State(Scope.Benchmark)
6677
@BenchmarkMode(Mode.AverageTime)
@@ -222,6 +233,90 @@ public Number filterThenSumAlp() {
222233
}
223234
}
224235

236+
/// Naive baseline for [#filterAlpDouble()]: the hand-written `price > 500` count loop over the
237+
/// ALP accessor, with no [Mask], no [Compute] and no off-heap bitmap. Decodes every double per
238+
/// element. Returns the count so JMH cannot eliminate the loop.
239+
///
240+
/// @return the number of rows with `price > 500`
241+
@Benchmark
242+
public long forLoopAlpDouble() {
243+
LazyAlpDoubleArray array = price;
244+
long n = array.length();
245+
long count = 0;
246+
for (long i = 0; i < n; i++) {
247+
if (array.getDouble(i) > PRICE_THRESHOLD) {
248+
count++;
249+
}
250+
}
251+
return count;
252+
}
253+
254+
/// Naive baseline for [#filterForLong()]: the hand-written `measure > base + spread/2` count loop
255+
/// over the Frame-of-Reference accessor, reconstructing each `offset + ref` long per element.
256+
///
257+
/// @return the number of rows with `measure > base + spread/2`
258+
@Benchmark
259+
public long forLoopForLong() {
260+
LazyForLongArray array = measure;
261+
long n = array.length();
262+
long count = 0;
263+
for (long i = 0; i < n; i++) {
264+
if (array.getLong(i) > MEASURE_THRESHOLD) {
265+
count++;
266+
}
267+
}
268+
return count;
269+
}
270+
271+
/// Naive baseline for [#filterDict()]: the hand-written `category == 7` count loop over the
272+
/// dictionary accessor, resolving each code through the dictionary per element.
273+
///
274+
/// @return the number of rows with `category == 7`
275+
@Benchmark
276+
public long forLoopDict() {
277+
DictLongArray array = category;
278+
long n = array.length();
279+
long count = 0;
280+
for (long i = 0; i < n; i++) {
281+
if (array.getLong(i) == CATEGORY_VALUE) {
282+
count++;
283+
}
284+
}
285+
return count;
286+
}
287+
288+
/// Naive baseline for [#filterPlainControl()]: the hand-written `plain > 0` count loop over the
289+
/// materialised accessor, reading each long straight from the segment per element.
290+
///
291+
/// @return the number of rows with `plain > 0`
292+
@Benchmark
293+
public long forLoopPlainControl() {
294+
MaterializedLongArray array = plain;
295+
long n = array.length();
296+
long count = 0;
297+
for (long i = 0; i < n; i++) {
298+
if (array.getLong(i) > 0L) {
299+
count++;
300+
}
301+
}
302+
return count;
303+
}
304+
305+
/// Naive baseline for [#sumAlpDouble()]: the hand-written running sum over the ALP accessor,
306+
/// decoding every double per element. Returns the sum so JMH cannot eliminate the loop.
307+
///
308+
/// @return the sum of all `price` values
309+
@Benchmark
310+
public double forLoopSumAlp() {
311+
LazyAlpDoubleArray array = price;
312+
long n = array.length();
313+
double acc = 0;
314+
for (long i = 0; i < n; i++) {
315+
acc += array.getDouble(i);
316+
}
317+
return acc;
318+
}
319+
225320
private void write(Path path) throws IOException {
226321
double[] priceData = new double[ROWS];
227322
long[] measureData = new long[ROWS];

0 commit comments

Comments
 (0)