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 -=