From 785cc693da17a74cb5c335d3a01472db0e64e66d Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Tue, 4 Aug 2026 15:57:17 +0200 Subject: [PATCH] De-inline the cumsum pair and the BLAS forwarders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six methods per file, in doublearrays and its floatarrays mirror. None of them meets any of the three conditions the inlining policy keeps `inline` for: no closure parameter, no VectorOperators constant to hold, and a concrete element type at every point they touch an element. cumsum! @HotPath @AllocFree cumsum @Thin dot @Thin norm @Thin - @Thin -= @Thin (the Array overload; see below) `cumsum!` is the interesting one. A prefix sum carries a dependency — `vec(i)` needs the value just written to `vec(i - 1)` — so it cannot be vectorised and there is no Vector API in the body at all. That makes it the only `@AllocFree` kernel in the library whose zero is unconditional rather than contingent on C2 applying intrinsics: there is no DoubleVector whose failure to scalarise could put bytes on the heap. It is worth measuring precisely because it is the control case for every other kernel in D1 — if it ever fails, the harness is wrong and not the kernel. Added to D1Suite and D1EAOffSuite for both element types, which brings both suites to 31 tests and keeps every @AllocFree method covered in both. `dot`, `norm` and the array-argument `-=` are `@Thin` rather than `@HotPath`, and the distinction is the annotation's own wording: `@HotPath` describes "code that runs once per element", and the per-element loop in these is inside netlib's ddot/dnrm2/daxpy, not in the method. What the method does is name the operation and dispatch, which is `@Thin`'s definition. C3's no-backward-branch assertion holds for the same reason and would fail if anyone open-coded the loop back in, which is the right outcome. None of the three is `@AllocFree`. `blas` is `JavaBLAS.getInstance`, so these call into a third-party pure-Java implementation whose allocation behaviour is not visible from here and has never been measured. Both annotations that turned out to be false — `**!` and `intarrays.dot` — were applied by inspection, so inspection is not the standard being used here. Worth a comment where it lands: `-=(Array)` is a BLAS forwarder while `-=(Double)` is a hand-written Vector API loop carrying @HotPath @AllocFree. Same name, different implementations, so different annotations, and not an inconsistency for someone to tidy up later. Scope note: four of these were named directly; `norm` and `-` were added because they sit inside the same block and are the same two shapes, and leaving them `inline` would half-convert it. `add`, `+` and `+=` in the same file are the same shapes again but are left alone — `add` forwards to `+`, so it cannot become a clean forwarder until `+` is emitted, and that ordering is a batch of its own. Co-Authored-By: Claude Opus 5 --- jitAudit/eaOffTest/src/D1EAOffSuite.scala | 12 +++++++ jitAudit/test/src/D1Suite.scala | 13 +++++++ vecxt/src-jvm/doublearrays.scala | 42 +++++++++++++++++++---- vecxt/src-jvm/floatarrays.scala | 30 ++++++++++++---- 4 files changed, 85 insertions(+), 12 deletions(-) diff --git a/jitAudit/eaOffTest/src/D1EAOffSuite.scala b/jitAudit/eaOffTest/src/D1EAOffSuite.scala index 1a3bc806..4b888a74 100644 --- a/jitAudit/eaOffTest/src/D1EAOffSuite.scala +++ b/jitAudit/eaOffTest/src/D1EAOffSuite.scala @@ -199,6 +199,13 @@ class D1EAOffSuite extends FunSuite: assertAllocFreeWithEAOff("doublearrays.abs!")(arr.`abs!`) } + // The control case: no Vector API in the body, so this cannot be on the software fallback path and its zero does not + // depend on either intrinsification or EA. If this one ever fails, the harness is wrong, not the kernel. + test("D1-EAOff: doublearrays.cumsum!") { + val arr = Array.fill(N)(1.0) + assertAllocFreeWithEAOff("doublearrays.cumsum!")(arr.`cumsum!`) + } + // ── Float ─────────────────────────────────────────────────────────────────── test("D1-EAOff: floatarrays.sumSIMD") { @@ -258,6 +265,11 @@ class D1EAOffSuite extends FunSuite: assertAllocFreeWithEAOff("floatarrays.*=(Float)")(arr *= 1.0f) } + test("D1-EAOff: floatarrays.cumsum!") { + val arr = Array.fill(N)(1.0f) + assertAllocFreeWithEAOff("floatarrays.cumsum!")(arr.`cumsum!`) + } + // ── Int ───────────────────────────────────────────────────────────────────── test("D1-EAOff: intarrays.sumSIMD") { diff --git a/jitAudit/test/src/D1Suite.scala b/jitAudit/test/src/D1Suite.scala index 6be7c973..e059218e 100644 --- a/jitAudit/test/src/D1Suite.scala +++ b/jitAudit/test/src/D1Suite.scala @@ -148,6 +148,14 @@ class D1Suite extends FunSuite: } } + // A prefix sum carries a dependency, so it cannot be vectorised and there is no Vector API in the body at all. + // That makes this the only @AllocFree kernel in the library whose zero is unconditional rather than contingent on + // C2 applying intrinsics — worth having measured precisely because it is the control case for all the others. + test("D1: doublearrays.cumsum!") { + val arr = Array.fill(N)(1.0) + assertAllocFree("doublearrays.cumsum!")(arr.`cumsum!`) + } + // ── Float ─────────────────────────────────────────────────────────────────── test("D1: floatarrays.sumSIMD") { @@ -209,6 +217,11 @@ class D1Suite extends FunSuite: assertAllocFree("floatarrays.*=(Float)")(arr *= 1.0f) } + test("D1: floatarrays.cumsum!") { + val arr = Array.fill(N)(1.0f) + assertAllocFree("floatarrays.cumsum!")(arr.`cumsum!`) + } + // ── Int ───────────────────────────────────────────────────────────────────── test("D1: intarrays.sumSIMD") { diff --git a/vecxt/src-jvm/doublearrays.scala b/vecxt/src-jvm/doublearrays.scala index fcb3d013..5c42779a 100644 --- a/vecxt/src-jvm/doublearrays.scala +++ b/vecxt/src-jvm/doublearrays.scala @@ -937,7 +937,16 @@ object doublearrays: maxVal + Math.log(sumExp) end logSumExp - inline def `cumsum!`: Unit = + /** The one kernel in this file whose `@AllocFree` does not depend on intrinsification. + * + * A prefix sum carries a dependency — `vec(i)` needs the value just written to `vec(i - 1)` — so it cannot be + * vectorised and there is no Vector API here at all. Nothing to intrinsify, therefore nothing whose failure to + * intrinsify could leave a `DoubleVector` on the heap. Every other `@AllocFree` in this file is contingent on C2 + * applying `VectorSupport` intrinsics; this one is true because the body allocates nothing, full stop. + */ + @HotPath + @AllocFree + def `cumsum!`: Unit = var i = 1 while i < vec.length do vec(i) = vec(i - 1) + vec(i) @@ -945,27 +954,48 @@ object doublearrays: end while end `cumsum!` - inline def cumsum: Array[Double] = + @Thin + def cumsum: Array[Double] = val out = vec.clone() out.`cumsum!` out end cumsum - inline def dot(v1: Array[Double]): Double = + /** `@Thin`, not `@HotPath`, and the distinction is the annotation's own wording. + * + * `@HotPath` describes "code that runs once per element". The per-element loop here is inside netlib's `ddot`, not + * in this method — what this method does is name the operation and dispatch to it, which is exactly `@Thin`'s + * definition. C3's no-backward-branch assertion is satisfied for the same reason, and would fail if someone ever + * open-coded the loop back into it, which is the right outcome. + * + * Not `@AllocFree`. `blas` is `JavaBLAS.getInstance`, so this is a call into a third-party pure-Java + * implementation. Whether it allocates is not visible from here and has never been measured, and the annotation + * exists to record measurements rather than expectations — the two that turned out to be false (`**!`, and + * `intarrays.dot`) were both applied by inspection. Same for `norm` and the array-argument `-=` below. + */ + @Thin + def dot(v1: Array[Double]): Double = dimCheck(vec, v1) blas.ddot(vec.length, vec, 1, v1, 1) end dot - inline def norm: Double = blas.dnrm2(vec.length, vec, 1) + @Thin + def norm: Double = blas.dnrm2(vec.length, vec, 1) - inline def -(vec2: Array[Double]): Array[Double] = + @Thin + def -(vec2: Array[Double]): Array[Double] = dimCheck(vec, vec2) val out = vec.clone out -= vec2 out end - - inline def -=(vec2: Array[Double]): Unit = + /** Note the asymmetry with the scalar overload below, which is `@HotPath @AllocFree`: that one is a hand-written + * Vector API loop, this one delegates the whole operation to `daxpy`. Same name, different implementations, so + * different annotations. Not an inconsistency to tidy up. + */ + @Thin + def -=(vec2: Array[Double]): Unit = dimCheck(vec, vec2) blas.daxpy(vec.length, -1.0, vec2, 1, vec, 1) end -= diff --git a/vecxt/src-jvm/floatarrays.scala b/vecxt/src-jvm/floatarrays.scala index cabd2de0..189e7342 100644 --- a/vecxt/src-jvm/floatarrays.scala +++ b/vecxt/src-jvm/floatarrays.scala @@ -4,6 +4,7 @@ import scala.reflect.ClassTag import vecxt.annotations.AllocFree import vecxt.annotations.HotPath +import vecxt.annotations.Thin import vecxt.matrix.Matrix @@ -640,12 +641,18 @@ object floatarrays: inline def stdDev(mode: VarianceMode): Float = std(mode) - inline def dot(v1: Array[Float]): Float = + /** `@Thin` rather than `@HotPath`, and not `@AllocFree`. See the doublearrays twin for the reasoning: the + * per-element loop is inside netlib's `sdot`, not here, and whether that third-party code allocates has never been + * measured. + */ + @Thin + def dot(v1: Array[Float]): Float = dimCheck(vec, v1) blas.sdot(vec.length, vec, 1, v1, 1) end dot - inline def norm: Float = blas.snrm2(vec.length, vec, 1) + @Thin + def norm: Float = blas.snrm2(vec.length, vec, 1) @HotPath def increments: Array[Float] = @@ -669,7 +676,12 @@ object floatarrays: out end increments - inline def `cumsum!`: Unit = + /** A prefix sum carries a dependency, so it cannot be vectorised and there is no Vector API here at all. That makes + * this `@AllocFree` unconditional rather than contingent on C2 applying intrinsics — see the doublearrays twin. + */ + @HotPath + @AllocFree + def `cumsum!`: Unit = var i = 1 while i < vec.length do vec(i) = vec(i - 1) + vec(i) @@ -677,7 +689,8 @@ object floatarrays: end while end `cumsum!` - inline def cumsum: Array[Float] = + @Thin + def cumsum: Array[Float] = val out = vec.clone() out.`cumsum!` out @@ -706,14 +719,19 @@ object floatarrays: (maxVal + Math.log(sumExp)).toFloat end logSumExp - inline def -(vec2: Array[Float]): Array[Float] = + @Thin + def -(vec2: Array[Float]): Array[Float] = dimCheck(vec, vec2) val out = vec.clone out -= vec2 out end - - inline def -=(vec2: Array[Float]): Unit = + /** Asymmetric with the scalar overload, which is `@HotPath @AllocFree`: that one is a hand-written Vector API loop, + * this one delegates to `saxpy`. Same name, different implementations, different annotations. + */ + @Thin + def -=(vec2: Array[Float]): Unit = dimCheck(vec, vec2) blas.saxpy(vec.length, -1.0f, vec2, 1, vec, 1) end -=