Summary
Investigate using Java's Vector API (JEP 508) to accelerate the pairwise distance computation in PairDistanceCalculator, which is the innermost hot loop of the eDivisive change-point detection algorithm. Initial analysis suggests 4-8x speedup on the core distance computation loops.
Motivation
The eDivisive algorithm spends the majority of its time computing pairwise distances between data points. The PairDistanceCalculator has three O(n^2) loops that are textbook SIMD candidates:
Loop 1: V (column sums) — computeVH() outer loop
for (int j = 1; j < n; j++) {
double sum = 0;
for (int i = 0; i < j; i++) {
sum += distance(i, j); // Math.abs(series[i] - series[j])
}
V[j - 1] = sum;
}
The inner loop computes sum += |series[i] - series[j]| for all i < j. For a fixed j, series[j] is a constant and the loop iterates contiguous series[i] values. This maps directly to the "common SIMD shape":
- Broadcast:
series[j] into all vector lanes
- Loop: load
series[i..i+lanes] one vector at a time
- Operate:
v.sub(broadcast_j).abs() — subtract and absolute value in parallel
- Reduce: accumulate into a sum vector, then
reduceLanes(ADD) at the end
- Scalar tail: handle remaining elements
// Vectorized inner loop (conceptual)
DoubleVector broadcastJ = DoubleVector.broadcast(SPECIES_256, series[j]);
DoubleVector sumVec = DoubleVector.zero(SPECIES_256);
int i = 0;
for (; i + lanes <= j; i += lanes) {
DoubleVector vi = DoubleVector.fromArray(SPECIES_256, series, i);
sumVec = sumVec.add(vi.sub(broadcastJ).abs());
}
double sum = sumVec.reduceLanes(VectorOperators.ADD);
for (; i < j; i++) sum += Math.abs(series[i] - series[j]); // scalar tail
With AVX2 (4 doubles per vector): ~4x speedup on this inner loop.
With AVX-512 (8 doubles per vector): ~8x speedup.
Loop 2: H (row cumulative sums) — computeVH() second loop
for (int i = 0; i < n - 1; i++) {
double cumsum = 0;
for (int j = i; j < n - 1; j++) {
cumsum += distance(i, j + 1);
H[i][j] = cumsum;
}
}
The inner loop is a running sum with stores — harder to vectorize because each H[i][j] depends on the previous cumulative sum. However, the distance(i, j+1) calls can be vectorized (same broadcast-subtract-abs pattern), and the prefix sum can use SIMD prefix-sum techniques.
Loop 3: Column prefix sums — computeColPrefix()
for (int j = 1; j < n; j++) {
double sum = 0;
for (int i = 0; i < j; i++) {
sum += distance(i, j);
colPrefix[j - 1][i] = sum;
}
}
Same pattern as Loop 2 — distance computation is vectorizable, prefix sum requires more care.
Loop 4: Q matrix computation — getQVals()
for (int i = 0; i < size; i++) {
for (int j = i; j < size; j++) {
// arithmetic on cumsumV, cumsumH values
Q[i][j] = A - B - C;
}
}
This loop does arithmetic on precomputed arrays. The inner loop body is more complex but operates on contiguous memory, making partial vectorization possible.
Estimated Impact
For a time series of length N:
- V computation: O(N^2/2) distance calls. With 4x SIMD: ~O(N^2/8) effective operations.
- H computation: O(N^2/2) distance calls + prefix sums.
- Total: for N=1000, that's ~500K distance calls reduced to ~125K vector operations (AVX2).
For h5m's typical use case (change detection on 50-500 datapoints per fingerprint), the absolute time savings may be modest. But for larger series (1000+ points) or batch processing across many fingerprints, the improvement compounds.
The "Common SIMD Shape" (Hashimoto's Pattern)
All vectorizable loops follow the same five steps (reference):
- Broadcast constants —
DoubleVector.broadcast(species, series[j])
- Loop one vector at a time —
for (; i + lanes <= j; i += lanes)
- Parallel operation —
v.sub(broadcast).abs()
- Reduce the result —
sumVec.reduceLanes(ADD) or store to output array
- Scalar tail — handle remaining 0 to lanes-1 elements with the original loop
The distance function |series[i] - series[j]| (when power=1.0, the common case) maps perfectly to steps 1-3: broadcast series[j], load series[i..i+lanes], subtract, absolute value.
When power != 1.0, Math.pow(diff, power) is harder to vectorize efficiently, but power=1.0 is the default and most common configuration.
Java Vector API Status
The Vector API (JEP 508) is in its 10th incubation round as of JDK 25. Key considerations:
- Requires
--add-modules jdk.incubator.vector at runtime
- API may change between JDK versions (incubator status)
- Performance depends on Valhalla for value types (vectors are currently heap-allocated)
- jhunter has no zero-dependency constraint (unlike jjq-core), so using the incubator module is acceptable behind a feature flag
Proposed Approach
- Create a
VectorPairDistanceCalculator that extends or replaces PairDistanceCalculator when the Vector API is available
- Feature flag: detect Vector API availability at runtime via
Module API or class loading; fall back to scalar implementation
- Benchmark: use the existing
PairDistanceCalculatorBenchmark to compare scalar vs vectorized on series of length 100, 500, 1000, 5000
- Start with Loop 1 (V column sums) — cleanest vectorization, highest impact per line of code changed
Acceptance Criteria
References
Summary
Investigate using Java's Vector API (JEP 508) to accelerate the pairwise distance computation in
PairDistanceCalculator, which is the innermost hot loop of the eDivisive change-point detection algorithm. Initial analysis suggests 4-8x speedup on the core distance computation loops.Motivation
The eDivisive algorithm spends the majority of its time computing pairwise distances between data points. The
PairDistanceCalculatorhas three O(n^2) loops that are textbook SIMD candidates:Loop 1: V (column sums) —
computeVH()outer loopThe inner loop computes
sum += |series[i] - series[j]|for alli < j. For a fixedj,series[j]is a constant and the loop iterates contiguousseries[i]values. This maps directly to the "common SIMD shape":series[j]into all vector lanesseries[i..i+lanes]one vector at a timev.sub(broadcast_j).abs()— subtract and absolute value in parallelreduceLanes(ADD)at the endWith AVX2 (4 doubles per vector): ~4x speedup on this inner loop.
With AVX-512 (8 doubles per vector): ~8x speedup.
Loop 2: H (row cumulative sums) —
computeVH()second loopThe inner loop is a running sum with stores — harder to vectorize because each
H[i][j]depends on the previous cumulative sum. However, thedistance(i, j+1)calls can be vectorized (same broadcast-subtract-abs pattern), and the prefix sum can use SIMD prefix-sum techniques.Loop 3: Column prefix sums —
computeColPrefix()Same pattern as Loop 2 — distance computation is vectorizable, prefix sum requires more care.
Loop 4: Q matrix computation —
getQVals()This loop does arithmetic on precomputed arrays. The inner loop body is more complex but operates on contiguous memory, making partial vectorization possible.
Estimated Impact
For a time series of length N:
For h5m's typical use case (change detection on 50-500 datapoints per fingerprint), the absolute time savings may be modest. But for larger series (1000+ points) or batch processing across many fingerprints, the improvement compounds.
The "Common SIMD Shape" (Hashimoto's Pattern)
All vectorizable loops follow the same five steps (reference):
DoubleVector.broadcast(species, series[j])for (; i + lanes <= j; i += lanes)v.sub(broadcast).abs()sumVec.reduceLanes(ADD)or store to output arrayThe distance function
|series[i] - series[j]|(when power=1.0, the common case) maps perfectly to steps 1-3: broadcastseries[j], loadseries[i..i+lanes], subtract, absolute value.When power != 1.0,
Math.pow(diff, power)is harder to vectorize efficiently, but power=1.0 is the default and most common configuration.Java Vector API Status
The Vector API (JEP 508) is in its 10th incubation round as of JDK 25. Key considerations:
--add-modules jdk.incubator.vectorat runtimeProposed Approach
VectorPairDistanceCalculatorthat extends or replacesPairDistanceCalculatorwhen the Vector API is availableModuleAPI or class loading; fall back to scalar implementationPairDistanceCalculatorBenchmarkto compare scalar vs vectorized on series of length 100, 500, 1000, 5000Acceptance Criteria
computeVH()for the power=1.0 (default) caseReferences