Skip to content

matrixutil: implement horzcat/vertcat for all layouts, tighten row/co… - #146

Merged
Quafadas merged 6 commits into
mainfrom
matrixutil-horzcat-vertcat
Aug 11, 2026
Merged

matrixutil: implement horzcat/vertcat for all layouts, tighten row/co…#146
Quafadas merged 6 commits into
mainfrom
matrixutil-horzcat-vertcat

Conversation

@Quafadas

Copy link
Copy Markdown
Owner

…l, de-allocate the mapRows/mapCols family

horzcat/vertcat

Both were if isDenseColMajor then ... else ???, so any row-major or strided operand threw NotImplementedError. Neither checked that the operands were conformable, and horzcat's fast path was m.raw.appendedAll(m2.raw) guarded only by isDenseColMajor - which does not imply dataLength == numel, so a leading-column submatrix view (dense by stride, but still carrying the parent's full backing array) spliced the parent's trailing columns into the result.

Now layout-agnostic: both read each operand through its own linearIndex via foreach2D, so views, offsets and row-major inputs all work. The arraycopy fast path is retained but re-guarded on hasSimpleContiguousMemoryLayout && isDenseColMajor, which is the condition that actually makes it sound. Added the missing rows/cols conformability checks (MatrixDimensionMismatch).

row / col

Not rewritten with foreach2D: that walks the whole matrix, which would turn these O(cols)/O(rows) reads into O(rows*cols) ones - foreach2D is for whole-matrix passes. Instead the per-element m((i, j)) (a multiply-add plus a bounds check each step) becomes a hoisted base plus a single add per step, with an arraycopy when the line is contiguous. Bounds checking now validates only the line index, so an empty matrix still yields an empty array rather than throwing.

mapRows / mapCols family

Each iteration wrote back via updateInPlace(Array[Int](idx), ::, ...), costing three array allocations per line: the one-element Array[Int] naming the line, plus - inside updateInPlace - range(::, n), which is Array.from((0 until n).toArray) and so materialises the full index array twice. Replaced with writeRow/writeCol helpers doing the same strided walk as row/col, leaving only the unavoidable line copy that the Array[A] => Array[B] signature requires. They also now reject a function that changes the line length, instead of silently truncating or throwing an opaque AIOOBE.

mapRowsToScalar/mapColsToScalar's hand-rolled isDenseRowMajor / isDenseColMajor branches are deleted: row/col now do exactly that arraycopy under the strictly weaker colStride == 1 / rowStride == 1, so the special cases were both redundant and narrower than the general path they guarded.

Tests cover horzcat/vertcat across all four fast/slow operand combinations, the submatrix-view regression, row/col over row-major / col-major / padded layouts, in-place mapping on non-column-major matrices, and the new conformability and line-length errors.

Simon Parten and others added 2 commits August 11, 2026 11:02
…l, de-allocate the mapRows/mapCols family

horzcat/vertcat
---------------
Both were `if isDenseColMajor then ... else ???`, so any row-major or
strided operand threw NotImplementedError. Neither checked that the
operands were conformable, and horzcat's fast path was
`m.raw.appendedAll(m2.raw)` guarded only by `isDenseColMajor` - which
does not imply `dataLength == numel`, so a leading-column submatrix
view (dense by stride, but still carrying the parent's full backing
array) spliced the parent's trailing columns into the result.

Now layout-agnostic: both read each operand through its own
`linearIndex` via `foreach2D`, so views, offsets and row-major inputs
all work. The arraycopy fast path is retained but re-guarded on
`hasSimpleContiguousMemoryLayout && isDenseColMajor`, which is the
condition that actually makes it sound. Added the missing
`rows`/`cols` conformability checks (MatrixDimensionMismatch).

row / col
---------
Not rewritten with foreach2D: that walks the whole matrix, which would
turn these O(cols)/O(rows) reads into O(rows*cols) ones - foreach2D is
for whole-matrix passes. Instead the per-element `m((i, j))` (a
multiply-add plus a bounds check each step) becomes a hoisted base
plus a single add per step, with an arraycopy when the line is
contiguous. Bounds checking now validates only the line index, so an
empty matrix still yields an empty array rather than throwing.

mapRows / mapCols family
------------------------
Each iteration wrote back via `updateInPlace(Array[Int](idx), ::, ...)`,
costing three array allocations per line: the one-element Array[Int]
naming the line, plus - inside updateInPlace - `range(::, n)`, which is
`Array.from((0 until n).toArray)` and so materialises the full index
array twice. Replaced with writeRow/writeCol helpers doing the same
strided walk as row/col, leaving only the unavoidable line copy that
the `Array[A] => Array[B]` signature requires. They also now reject a
function that changes the line length, instead of silently truncating
or throwing an opaque AIOOBE.

mapRowsToScalar/mapColsToScalar's hand-rolled isDenseRowMajor /
isDenseColMajor branches are deleted: row/col now do exactly that
arraycopy under the strictly weaker `colStride == 1` / `rowStride == 1`,
so the special cases were both redundant and narrower than the general
path they guarded.

Tests cover horzcat/vertcat across all four fast/slow operand
combinations, the submatrix-view regression, row/col over row-major /
col-major / padded layouts, in-place mapping on non-column-major
matrices, and the new conformability and line-length errors.
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

Bytecode audit (Tier 1) — ✅ clean

JDK 25.0.1 (major 25), OpenJDK 64-Bit Server VM

Vector lanes (DoubleVector.SPECIES_PREFERRED.length()): 4

threshold value provenance
MaxTrivialSize 6 discovered
MaxInlineSize 35 discovered
FreqInlineSize 325 discovered
MaxInlineLevel 15 discovered
InlineSmallCode 2500 discovered
NodeCountInliningCutoff 18000 assumed
HugeMethodLimit 8000 assumed

-XX:HugeMethodLimit= was rejected on the command line: a develop flag compiled out of this product build, so 8000 is taken from the HotSpot source and cannot be confirmed against the running JVM.

metric now baseline delta
cheatsheet methods 97
total bytes 38578 51103 -24.5%
distinct library ops 205 178 +15.2%
bytes per op 188.2 287.1 -34.5%

none

Annotated methods (180)
method annotations bytes budget used loop at
vecxt.floatarrays$.$minus$eq @Thin 26 74% of 35 no floatarrays.scala:736
vecxt.doublearrays$.$minus$eq @Thin 26 74% of 35 no doublearrays.scala:1031
vecxt.intarrays$.$plus @Thin 24 69% of 35 no intarrays.scala:546
vecxt.intarrays$.$minus @Thin 24 69% of 35 no intarrays.scala:399
vecxt.floatarrays$.$minus @Thin 24 69% of 35 no floatarrays.scala:725
vecxt.doublearrays$.$minus @Thin 24 69% of 35 no doublearrays.scala:1019
vecxt.floatarrays$.dot @Thin 23 66% of 35 no floatarrays.scala:651
vecxt.doublearrays$.dot @Thin 23 66% of 35 no doublearrays.scala:1004
vecxt.NDArrayFloatOps$.compareGeneral @HotPath 195 60% of 325 yes ndarrayFloatOps.scala:67
vecxt.NDArrayFloatOps$.binaryOpGeneral @HotPath 195 60% of 325 yes ndarrayFloatOps.scala:20
vecxt.NDArrayIntOps$.compareGeneral @HotPath 186 57% of 325 yes ndarrayIntOps.scala:67
vecxt.NDArrayIntOps$.binaryOpGeneral @HotPath 186 57% of 325 yes ndarrayIntOps.scala:19
vecxt.NDArrayDoubleOps$.compareGeneral @HotPath 186 57% of 325 yes ndarrayDoubleOps.scala:74
vecxt.NDArrayDoubleOps$.binaryOpGeneral @HotPath 186 57% of 325 yes ndarrayDoubleOps.scala:24
vecxt.doublearrays$.clamp$bang @AllocFree @HotPath 180 55% of 325 yes doublearrays.scala:898
vecxt.matrix$Layout.linearIndex @Thin 19 54% of 35 no matrix.scala:48
vecxt.floatarrays$.clamp$bang @AllocFree @HotPath 172 53% of 325 yes floatarrays.scala:419
vecxt.NDArrayFloatOps$.compareScalarGeneral @HotPath 165 51% of 325 yes ndarrayFloatOps.scala:94
vecxt.NDArrayFloatOps$.binaryOpInPlaceGeneral @HotPath 162 50% of 325 yes ndarrayFloatOps.scala:119
vecxt.NDArrayDoubleOps$.compareScalarGeneral @HotPath 157 48% of 325 yes ndarrayDoubleOps.scala:102
vecxt.NDArrayIntOps$.compareScalarGeneral @HotPath 156 48% of 325 yes ndarrayIntOps.scala:94
vecxt.NDArrayIntOps$.binaryOpInPlaceGeneral @HotPath 153 47% of 325 yes ndarrayIntOps.scala:119
vecxt.NDArrayDoubleOps$.binaryOpInPlaceGeneral @HotPath 153 47% of 325 yes ndarrayDoubleOps.scala:131
vecxt.NDArrayIntOps$.unaryOpGeneral @HotPath 152 47% of 325 yes ndarrayIntOps.scala:42
vecxt.NDArrayDoubleOps$.unaryOpGeneral @HotPath 152 47% of 325 yes ndarrayDoubleOps.scala:48
vecxt.intarrays$.$minus @Thin 16 46% of 35 no intarrays.scala:519
vecxt.floatarrays$.cumsum @Thin 15 43% of 35 no floatarrays.scala:695
vecxt.doublearrays$.cumsum @Thin 15 43% of 35 no doublearrays.scala:985
vecxt.doublearrays$.fillLinspace @AllocFree @HotPath 133 41% of 325 yes doublearrays.scala:34
vecxt.intarrays$.increments @HotPath 121 37% of 325 yes intarrays.scala:219
vecxt.ndarray$.mkNDArray @Thin 13 37% of 35 no ndarray.scala:188
vecxt.floatarrays$.norm @Thin 13 37% of 35 no floatarrays.scala:656
vecxt.doublearrays$.norm @Thin 13 37% of 35 no doublearrays.scala:1009
vecxt.doublearrays$.$plus @AllocFree @HotPath 116 36% of 325 yes doublearrays.scala:1065
vecxt.doublearrays$.$minus @AllocFree @HotPath 116 36% of 325 yes doublearrays.scala:1128
vecxt.doublearrays$.$div @AllocFree @HotPath 116 36% of 325 yes doublearrays.scala:1341
vecxt.doublearrays$.increments @HotPath 110 34% of 325 yes doublearrays.scala:397
vecxt.intarrays$.dot @AllocFree @HotPath 108 33% of 325 yes intarrays.scala:372
vecxt.doublearrays$.unary_$minus @HotPath 108 33% of 325 yes doublearrays.scala:172
vecxt.doublearrays$.tanh @HotPath 108 33% of 325 yes doublearrays.scala:365
vecxt.doublearrays$.tan @HotPath 108 33% of 325 yes doublearrays.scala:354
vecxt.doublearrays$.sqrt @HotPath 108 33% of 325 yes doublearrays.scala:317
vecxt.doublearrays$.sinh @HotPath 108 33% of 325 yes doublearrays.scala:339
vecxt.doublearrays$.sin @HotPath 108 33% of 325 yes doublearrays.scala:328
vecxt.doublearrays$.log1p @HotPath 108 33% of 325 yes doublearrays.scala:306
vecxt.doublearrays$.log10 @HotPath 108 33% of 325 yes doublearrays.scala:295
vecxt.doublearrays$.log @HotPath 108 33% of 325 yes doublearrays.scala:284
vecxt.doublearrays$.expm1 @HotPath 108 33% of 325 yes doublearrays.scala:273
vecxt.doublearrays$.exp @HotPath 108 33% of 325 yes doublearrays.scala:262
vecxt.doublearrays$.cosh @HotPath 108 33% of 325 yes doublearrays.scala:251
vecxt.doublearrays$.cos @HotPath 108 33% of 325 yes doublearrays.scala:240
vecxt.doublearrays$.cbrt @HotPath 108 33% of 325 yes doublearrays.scala:229
vecxt.doublearrays$.atan @HotPath 108 33% of 325 yes doublearrays.scala:218
vecxt.doublearrays$.asin @HotPath 108 33% of 325 yes doublearrays.scala:207
vecxt.doublearrays$.acos @HotPath 108 33% of 325 yes doublearrays.scala:196
vecxt.doublearrays$.abs @HotPath 108 33% of 325 yes doublearrays.scala:184
vecxt.intarrays$.$less @HotPath 106 33% of 325 yes intarrays.scala:48
vecxt.intarrays$.$less$eq @HotPath 106 33% of 325 yes intarrays.scala:52
vecxt.intarrays$.$greater @HotPath 106 33% of 325 yes intarrays.scala:56
vecxt.intarrays$.$greater$eq @HotPath 106 33% of 325 yes intarrays.scala:60
vecxt.intarrays$.$eq$colon$eq @HotPath 106 33% of 325 yes intarrays.scala:40
vecxt.intarrays$.$bang$colon$eq @HotPath 106 33% of 325 yes intarrays.scala:44
vecxt.floatarrays$.unary_$minus @HotPath 104 32% of 325 yes floatarrays.scala:116
vecxt.floatarrays$.tanh @HotPath 104 32% of 325 yes floatarrays.scala:305
vecxt.floatarrays$.tan @HotPath 104 32% of 325 yes floatarrays.scala:294
vecxt.floatarrays$.sqrt @HotPath 104 32% of 325 yes floatarrays.scala:261
vecxt.floatarrays$.sinh @HotPath 104 32% of 325 yes floatarrays.scala:283
vecxt.floatarrays$.sin @HotPath 104 32% of 325 yes floatarrays.scala:272
vecxt.floatarrays$.log1p @HotPath 104 32% of 325 yes floatarrays.scala:250
vecxt.floatarrays$.log10 @HotPath 104 32% of 325 yes floatarrays.scala:239
vecxt.floatarrays$.log @HotPath 104 32% of 325 yes floatarrays.scala:228
vecxt.floatarrays$.expm1 @HotPath 104 32% of 325 yes floatarrays.scala:217
vecxt.floatarrays$.exp @HotPath 104 32% of 325 yes floatarrays.scala:206
vecxt.floatarrays$.cosh @HotPath 104 32% of 325 yes floatarrays.scala:195
vecxt.floatarrays$.cos @HotPath 104 32% of 325 yes floatarrays.scala:184
vecxt.floatarrays$.cbrt @HotPath 104 32% of 325 yes floatarrays.scala:173
vecxt.floatarrays$.atan @HotPath 104 32% of 325 yes floatarrays.scala:162
vecxt.floatarrays$.asin @HotPath 104 32% of 325 yes floatarrays.scala:151
vecxt.floatarrays$.acos @HotPath 104 32% of 325 yes floatarrays.scala:140
vecxt.floatarrays$.abs @HotPath 104 32% of 325 yes floatarrays.scala:128
vecxt.intarrays$.mean @Thin 11 31% of 35 no intarrays.scala:287
vecxt.doublearrays$.sumSIMD @AllocFree @HotPath 102 31% of 325 yes doublearrays.scala:703
vecxt.doublearrays$.tanh$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.tan$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.sqrt$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.sinh$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.sin$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.log1p$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.log10$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.log$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.expm1$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.exp$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.cosh$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.cos$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.cbrt$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.atan$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.asin$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.acos$bang @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.abs$bang @AllocFree @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.doublearrays$.$minus$bang @AllocFree @HotPath 98 30% of 325 yes doublearrays.scala:151
vecxt.floatarrays$.increments @HotPath 97 30% of 325 yes floatarrays.scala:660
vecxt.doublearrays$.$times$times$bang @HotPath 95 29% of 325 yes doublearrays.scala:372
vecxt.intarrays$.$less @HotPath 94 29% of 325 yes intarrays.scala:139
vecxt.intarrays$.$less$eq @HotPath 94 29% of 325 yes intarrays.scala:143
vecxt.intarrays$.$greater @HotPath 94 29% of 325 yes intarrays.scala:147
vecxt.intarrays$.$greater$eq @HotPath 94 29% of 325 yes intarrays.scala:151
vecxt.intarrays$.$eq$colon$eq @HotPath 94 29% of 325 yes intarrays.scala:131
vecxt.intarrays$.$bang$colon$eq @HotPath 94 29% of 325 yes intarrays.scala:135
vecxt.floatarrays$.tanh$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.tan$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.sqrt$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.sinh$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.sin$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.log1p$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.log10$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.log$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.expm1$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.exp$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.cosh$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.cos$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.cbrt$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.atan$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.asin$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.acos$bang @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.abs$bang @AllocFree @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.floatarrays$.$minus$bang @AllocFree @HotPath 93 29% of 325 yes floatarrays.scala:96
vecxt.intarrays$.variance @Thin 10 29% of 35 no intarrays.scala:304
vecxt.intarrays$.std @Thin 10 29% of 35 no intarrays.scala:361
vecxt.doublearrays$.variance @AllocFree @Thin 10 29% of 35 no doublearrays.scala:541
vecxt.doublearrays$.$plus$eq @AllocFree @HotPath 90 28% of 325 yes doublearrays.scala:1087
vecxt.doublearrays$.$minus$eq @AllocFree @HotPath 90 28% of 325 yes doublearrays.scala:1175
vecxt.doublearrays$.$times$eq @AllocFree @HotPath 88 27% of 325 yes doublearrays.scala:1250
vecxt.doublearrays$.productSIMD @AllocFree @HotPath 86 26% of 325 yes doublearrays.scala:726
vecxt.floatarrays$.$times$times$bang @HotPath 85 26% of 325 yes floatarrays.scala:316
vecxt.doublearrays$.sumSIMD @AllocFree @HotPath 85 26% of 325 yes doublearrays.scala:677
vecxt.doublearrays$.fma$bang @AllocFree @HotPath 85 26% of 325 yes doublearrays.scala:1150
vecxt.intarrays$.$plus$eq @AllocFree @HotPath 84 26% of 325 yes intarrays.scala:555
vecxt.intarrays$.$minus$eq @AllocFree @HotPath 84 26% of 325 yes intarrays.scala:527
vecxt.floatarrays$.$times$eq @AllocFree @HotPath 84 26% of 325 yes floatarrays.scala:893
vecxt.floatarrays$.$plus$eq @AllocFree @HotPath 84 26% of 325 yes floatarrays.scala:791
vecxt.floatarrays$.$minus$eq @AllocFree @HotPath 84 26% of 325 yes floatarrays.scala:831
vecxt.ndarrayOps.expandDims @Thin 9 26% of 35 no ndarrayOps.scala
vecxt.intarrays.stdDev @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays.meanAndVariance @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays.lte @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays.lte @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays.lt @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays.lt @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays.gte @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays.gte @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays.gt @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays.gt @Thin 9 26% of 35 no intarrays.scala
vecxt.intarrays$.variance @Thin 9 26% of 35 no intarrays.scala:291
vecxt.intarrays$.stdDev @Thin 9 26% of 35 no intarrays.scala:364
vecxt.intarrays$.std @Thin 9 26% of 35 no intarrays.scala:357
vecxt.intarrays$.meanAndVariance @Thin 9 26% of 35 no intarrays.scala:308
vecxt.doublearrays.meanAndVariance @Thin 9 26% of 35 no doublearrays.scala
vecxt.doublearrays$.meanAndVariance @Thin 9 26% of 35 no doublearrays.scala:555
vecxt.intarrays$.minSIMD @AllocFree @HotPath 82 25% of 325 yes intarrays.scala:575
vecxt.intarrays$.maxSIMD @AllocFree @HotPath 82 25% of 325 yes intarrays.scala:595
vecxt.floatarrays$.productSIMD @AllocFree @HotPath 82 25% of 325 yes floatarrays.scala:539
vecxt.intarrays$.sumSIMD @AllocFree @HotPath 81 25% of 325 yes intarrays.scala:265
vecxt.floatarrays$.sumSIMD @AllocFree @HotPath 81 25% of 325 yes floatarrays.scala:518
vecxt.floatarrays$.fma$bang @AllocFree @HotPath 80 25% of 325 yes floatarrays.scala:342
vecxt.floatarrays$.$times$eq @AllocFree @HotPath 78 24% of 325 yes floatarrays.scala:945
vecxt.ndarray.shapeArray @Thin 8 23% of 35 no ndarray.scala
vecxt.matrix$Matrix.rows @Thin 8 23% of 35 no matrix.scala:195
vecxt.matrix$Matrix.rowStride @Thin 8 23% of 35 no matrix.scala:201
vecxt.matrix$Matrix.offset @Thin 8 23% of 35 no matrix.scala:207
vecxt.matrix$Matrix.numel @Thin 8 23% of 35 no matrix.scala:210
vecxt.matrix$Matrix.isDenseRowMajor @Thin 8 23% of 35 no matrix.scala:216
vecxt.matrix$Matrix.isDenseColMajor @Thin 8 23% of 35 no matrix.scala:213
vecxt.matrix$Matrix.hasSimpleContiguousMemoryLayout @Thin 8 23% of 35 no matrix.scala:228
vecxt.matrix$Matrix.cols @Thin 8 23% of 35 no matrix.scala:198
vecxt.matrix$Matrix.colStride @Thin 8 23% of 35 no matrix.scala:204
vecxt.intarrays$.countsToIdx @HotPath 70 22% of 325 yes intarrays.scala:244
vecxt.intarrays$.$minus$eq @AllocFree @HotPath 67 21% of 325 yes intarrays.scala:408
vecxt.floatarrays$.cumsum$bang @AllocFree @HotPath 27 8% of 325 yes floatarrays.scala:686
vecxt.doublearrays$.cumsum$bang @AllocFree @HotPath 27 8% of 325 yes doublearrays.scala:976
vecxt.floatarrays$.$plus$eq @AllocFree @HotPath 24 7% of 325 no floatarrays.scala:750
Method sizes
band methods
<= 6 (trivial, always inlined) 1435
7-35 (inlinable cold) 3682
36-325 (inlinable when hot) 693
326-8000 (not inlined) 112
> 8000 (NEVER JIT COMPILED) 0
bytes method module at
5019 CheatsheetTest$.matrixRangeSlicing experiments cheatsheet.scala:118
3801 CheatsheetTest$.ndArrayInt experiments cheatsheet.scala:416
3779 CheatsheetTest$.ndArrayBoolean experiments cheatsheet.scala:430
3399 CheatsheetTest$.matrixReverseSlicing experiments cheatsheet.scala:125
3061 CheatsheetTest$.ndArrayFloat experiments cheatsheet.scala:395
3015 CheatsheetTest$.ndArrayFloatReductions experiments cheatsheet.scala:405
2500 vecxt_re.Tower.show vecxt_re Tower.scala:63
2483 CheatsheetTest$.matrixCreation experiments cheatsheet.scala:72
2120 CheatsheetTest$.matrixOps experiments cheatsheet.scala:167
1594 vecxt.ndarrayOps$.apply vecxt ndarrayOps.scala:452
1225 vecxt.JvmFloatMatrix$.floatmatrixSubVector vecxt floatmatrix.scala:324
1200 CheatsheetTest$.arrayManipulation experiments cheatsheet.scala:305
1189 vecxt.JvmDoubleMatrix$.$plus$eq vecxt doublematrix.scala:220
1165 vecxt.JvmFloatMatrix$.floatmatrixAddVectorInPlace vecxt floatmatrix.scala:242
1165 vecxt.JvmFloatMatrix$.floatmatrixSubVectorInPlace vecxt floatmatrix.scala:342
1043 vecxt.JvmDoubleMatrix$.$plus$eq vecxt doublematrix.scala:302
1028 vecxt.JvmFloatMatrix$.floatmatrixAddScalarInPlace vecxt floatmatrix.scala:418
1028 vecxt.JvmFloatMatrix$.floatmatrixSubScalarInPlace vecxt floatmatrix.scala:490
969 vecxt_re.Scenarr$.combine vecxt_re scenarr.scala:160
908 vecxt.Svd$.svd vecxt svd.scala:135
902 vecxt.Svd$.pinv vecxt svd.scala:42
896 vecxt_re.NegativeBinomial$.volweightedMle vecxt_re NegativeBinomial.scala:281
878 vecxt_re.NegativeBinomial$.mle vecxt_re NegativeBinomial.scala:151
822 vecxt.DoubleMatrix$.hadamard vecxt doublematrix.scala:263
816 vecxt_re.Layer.equals vecxt_re Layer.scala:21
Proposed baseline
{
  "jdkMajor": 25,
  "c9": { "totalBytes": 38578, "distinctOps": 205 },
  "annotated": {
    "vecxt.NDArrayDoubleOps$.binaryOpGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 186,
    "vecxt.NDArrayDoubleOps$.binaryOpInPlaceGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)V": 153,
    "vecxt.NDArrayDoubleOps$.compareGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 186,
    "vecxt.NDArrayDoubleOps$.compareScalarGeneral(Lvecxt/ndarray$NDArray;DLscala/Function2;)Lvecxt/ndarray$NDArray;": 157,
    "vecxt.NDArrayDoubleOps$.unaryOpGeneral(Lvecxt/ndarray$NDArray;Lscala/Function1;)Lvecxt/ndarray$NDArray;": 152,
    "vecxt.NDArrayFloatOps$.binaryOpGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 195,
    "vecxt.NDArrayFloatOps$.binaryOpInPlaceGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)V": 162,
    "vecxt.NDArrayFloatOps$.compareGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 195,
    "vecxt.NDArrayFloatOps$.compareScalarGeneral(Lvecxt/ndarray$NDArray;FLscala/Function2;)Lvecxt/ndarray$NDArray;": 165,
    "vecxt.NDArrayIntOps$.binaryOpGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 186,
    "vecxt.NDArrayIntOps$.binaryOpInPlaceGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)V": 153,
    "vecxt.NDArrayIntOps$.compareGeneral(Lvecxt/ndarray$NDArray;Lvecxt/ndarray$NDArray;Lscala/Function2;)Lvecxt/ndarray$NDArray;": 186,
    "vecxt.NDArrayIntOps$.compareScalarGeneral(Lvecxt/ndarray$NDArray;ILscala/Function2;)Lvecxt/ndarray$NDArray;": 156,
    "vecxt.NDArrayIntOps$.unaryOpGeneral(Lvecxt/ndarray$NDArray;Lscala/Function1;)Lvecxt/ndarray$NDArray;": 152,
    "vecxt.doublearrays$.$div([DDII[DI)V": 116,
    "vecxt.doublearrays$.$minus$bang([D)V": 98,
    "vecxt.doublearrays$.$minus$eq([DD)V": 90,
    "vecxt.doublearrays$.$minus$eq([D[D)V": 26,
    "vecxt.doublearrays$.$minus([DDII[DI)V": 116,
    "vecxt.doublearrays$.$minus([D[D)[D": 24,
    "vecxt.doublearrays$.$plus$eq([DD)V": 90,
    "vecxt.doublearrays$.$plus([DDII[DI)V": 116,
    "vecxt.doublearrays$.$times$eq([D[D)V": 88,
    "vecxt.doublearrays$.$times$times$bang([DD)V": 95,
    "vecxt.doublearrays$.abs$bang([D)V": 98,
    "vecxt.doublearrays$.abs([D)[D": 108,
    "vecxt.doublearrays$.acos$bang([D)V": 98,
    "vecxt.doublearrays$.acos([D)[D": 108,
    "vecxt.doublearrays$.asin$bang([D)V": 98,
    "vecxt.doublearrays$.asin([D)[D": 108,
    "vecxt.doublearrays$.atan$bang([D)V": 98,
    "vecxt.doublearrays$.atan([D)[D": 108,
    "vecxt.doublearrays$.cbrt$bang([D)V": 98,
    "vecxt.doublearrays$.cbrt([D)[D": 108,
    "vecxt.doublearrays$.clamp$bang([DDD)V": 180,
    "vecxt.doublearrays$.cos$bang([D)V": 98,
    "vecxt.doublearrays$.cos([D)[D": 108,
    "vecxt.doublearrays$.cosh$bang([D)V": 98,
    "vecxt.doublearrays$.cosh([D)[D": 108,
    "vecxt.doublearrays$.cumsum$bang([D)V": 27,
    "vecxt.doublearrays$.cumsum([D)[D": 15,
    "vecxt.doublearrays$.dot([D[D)D": 23,
    "vecxt.doublearrays$.exp$bang([D)V": 98,
    "vecxt.doublearrays$.exp([D)[D": 108,
    "vecxt.doublearrays$.expm1$bang([D)V": 98,
    "vecxt.doublearrays$.expm1([D)[D": 108,
    "vecxt.doublearrays$.fillLinspace([DDD)V": 133,
    "vecxt.doublearrays$.fma$bang([DDD)V": 85,
    "vecxt.doublearrays$.increments([D)[D": 110,
    "vecxt.doublearrays$.log$bang([D)V": 98,
    "vecxt.doublearrays$.log([D)[D": 108,
    "vecxt.doublearrays$.log10$bang([D)V": 98,
    "vecxt.doublearrays$.log10([D)[D": 108,
    "vecxt.doublearrays$.log1p$bang([D)V": 98,
    "vecxt.doublearrays$.log1p([D)[D": 108,
    "vecxt.doublearrays$.meanAndVariance([D)Lvecxt/MeanAndVariance;": 9,
    "vecxt.doublearrays$.norm([D)D": 13,
    "vecxt.doublearrays$.productSIMD([D)D": 86,
    "vecxt.doublearrays$.sin$bang([D)V": 98,
    "vecxt.doublearrays$.sin([D)[D": 108,
    "vecxt.doublearrays$.sinh$bang([D)V": 98,
    "vecxt.doublearrays$.sinh([D)[D": 108,
    "vecxt.doublearrays$.sqrt$bang([D)V": 98,
    "vecxt.doublearrays$.sqrt([D)[D": 108,
    "vecxt.doublearrays$.sumSIMD([D)D": 85,
    "vecxt.doublearrays$.sumSIMD([DII)D": 102,
    "vecxt.doublearrays$.tan$bang([D)V": 98,
    "vecxt.doublearrays$.tan([D)[D": 108,
    "vecxt.doublearrays$.tanh$bang([D)V": 98,
    "vecxt.doublearrays$.tanh([D)[D": 108,
    "vecxt.doublearrays$.unary_$minus([D)[D": 108,
    "vecxt.doublearrays$.variance([DLvecxt/VarianceMode;)D": 10,
    "vecxt.doublearrays.meanAndVariance([DLvecxt/VarianceMode;)Lvecxt/MeanAndVariance;": 9,
    "vecxt.floatarrays$.$minus$bang([F)V": 93,
    "vecxt.floatarrays$.$minus$eq([FF)V": 84,
    "vecxt.floatarrays$.$minus$eq([F[F)V": 26,
    "vecxt.floatarrays$.$minus([F[F)[F": 24,
    "vecxt.floatarrays$.$plus$eq([FF)V": 84,
    "vecxt.floatarrays$.$plus$eq([F[F)V": 24,
    "vecxt.floatarrays$.$times$eq([FF)V": 78,
    "vecxt.floatarrays$.$times$eq([F[F)V": 84,
    "vecxt.floatarrays$.$times$times$bang([FF)V": 85,
    "vecxt.floatarrays$.abs$bang([F)V": 93,
    "vecxt.floatarrays$.abs([F)[F": 104,
    "vecxt.floatarrays$.acos$bang([F)V": 93,
    "vecxt.floatarrays$.acos([F)[F": 104,
    "vecxt.floatarrays$.asin$bang([F)V": 93,
    "vecxt.floatarrays$.asin([F)[F": 104,
    "vecxt.floatarrays$.atan$bang([F)V": 93,
    "vecxt.floatarrays$.atan([F)[F": 104,
    "vecxt.floatarrays$.cbrt$bang([F)V": 93,
    "vecxt.floatarrays$.cbrt([F)[F": 104,
    "vecxt.floatarrays$.clamp$bang([FFF)V": 172,
    "vecxt.floatarrays$.cos$bang([F)V": 93,
    "vecxt.floatarrays$.cos([F)[F": 104,
    "vecxt.floatarrays$.cosh$bang([F)V": 93,
    "vecxt.floatarrays$.cosh([F)[F": 104,
    "vecxt.floatarrays$.cumsum$bang([F)V": 27,
    "vecxt.floatarrays$.cumsum([F)[F": 15,
    "vecxt.floatarrays$.dot([F[F)F": 23,
    "vecxt.floatarrays$.exp$bang([F)V": 93,
    "vecxt.floatarrays$.exp([F)[F": 104,
    "vecxt.floatarrays$.expm1$bang([F)V": 93,
    "vecxt.floatarrays$.expm1([F)[F": 104,
    "vecxt.floatarrays$.fma$bang([FFF)V": 80,
    "vecxt.floatarrays$.increments([F)[F": 97,
    "vecxt.floatarrays$.log$bang([F)V": 93,
    "vecxt.floatarrays$.log([F)[F": 104,
    "vecxt.floatarrays$.log10$bang([F)V": 93,
    "vecxt.floatarrays$.log10([F)[F": 104,
    "vecxt.floatarrays$.log1p$bang([F)V": 93,
    "vecxt.floatarrays$.log1p([F)[F": 104,
    "vecxt.floatarrays$.norm([F)F": 13,
    "vecxt.floatarrays$.productSIMD([F)F": 82,
    "vecxt.floatarrays$.sin$bang([F)V": 93,
    "vecxt.floatarrays$.sin([F)[F": 104,
    "vecxt.floatarrays$.sinh$bang([F)V": 93,
    "vecxt.floatarrays$.sinh([F)[F": 104,
    "vecxt.floatarrays$.sqrt$bang([F)V": 93,
    "vecxt.floatarrays$.sqrt([F)[F": 104,
    "vecxt.floatarrays$.sumSIMD([F)F": 81,
    "vecxt.floatarrays$.tan$bang([F)V": 93,
    "vecxt.floatarrays$.tan([F)[F": 104,
    "vecxt.floatarrays$.tanh$bang([F)V": 93,
    "vecxt.floatarrays$.tanh([F)[F": 104,
    "vecxt.floatarrays$.unary_$minus([F)[F": 104,
    "vecxt.intarrays$.$bang$colon$eq([II)[Z": 94,
    "vecxt.intarrays$.$bang$colon$eq([I[I)[Z": 106,
    "vecxt.intarrays$.$eq$colon$eq([II)[Z": 94,
    "vecxt.intarrays$.$eq$colon$eq([I[I)[Z": 106,
    "vecxt.intarrays$.$greater$eq([II)[Z": 94,
    "vecxt.intarrays$.$greater$eq([I[I)[Z": 106,
    "vecxt.intarrays$.$greater([II)[Z": 94,
    "vecxt.intarrays$.$greater([I[I)[Z": 106,
    "vecxt.intarrays$.$less$eq([II)[Z": 94,
    "vecxt.intarrays$.$less$eq([I[I)[Z": 106,
    "vecxt.intarrays$.$less([II)[Z": 94,
    "vecxt.intarrays$.$less([I[I)[Z": 106,
    "vecxt.intarrays$.$minus$eq([II)V": 67,
    "vecxt.intarrays$.$minus$eq([I[I)V": 84,
    "vecxt.intarrays$.$minus([II)[I": 16,
    "vecxt.intarrays$.$minus([I[I)[I": 24,
    "vecxt.intarrays$.$plus$eq([I[I)V": 84,
    "vecxt.intarrays$.$plus([I[I)[I": 24,
    "vecxt.intarrays$.countsToIdx([I)[I": 70,
    "vecxt.intarrays$.dot([I[I)I": 108,
    "vecxt.intarrays$.increments([I)[I": 121,
    "vecxt.intarrays$.maxSIMD([I)I": 82,
    "vecxt.intarrays$.mean([I)D": 11,
    "vecxt.intarrays$.meanAndVariance([I)Lvecxt/MeanAndVariance;": 9,
    "vecxt.intarrays$.minSIMD([I)I": 82,
    "vecxt.intarrays$.std([I)D": 9,
    "vecxt.intarrays$.std([ILvecxt/VarianceMode;)D": 10,
    "vecxt.intarrays$.stdDev([I)D": 9,
    "vecxt.intarrays$.sumSIMD([I)I": 81,
    "vecxt.intarrays$.variance([I)D": 9,
    "vecxt.intarrays$.variance([ILvecxt/VarianceMode;)D": 10,
    "vecxt.intarrays.gt([II)[Z": 9,
    "vecxt.intarrays.gt([I[I)[Z": 9,
    "vecxt.intarrays.gte([II)[Z": 9,
    "vecxt.intarrays.gte([I[I)[Z": 9,
    "vecxt.intarrays.lt([II)[Z": 9,
    "vecxt.intarrays.lt([I[I)[Z": 9,
    "vecxt.intarrays.lte([II)[Z": 9,
    "vecxt.intarrays.lte([I[I)[Z": 9,
    "vecxt.intarrays.meanAndVariance([ILvecxt/VarianceMode;)Lvecxt/MeanAndVariance;": 9,
    "vecxt.intarrays.stdDev([ILvecxt/VarianceMode;)D": 9,
    "vecxt.matrix$Layout.linearIndex(II)I": 19,
    "vecxt.matrix$Matrix.colStride()I": 8,
    "vecxt.matrix$Matrix.cols()I": 8,
    "vecxt.matrix$Matrix.hasSimpleContiguousMemoryLayout()Z": 8,
    "vecxt.matrix$Matrix.isDenseColMajor()Z": 8,
    "vecxt.matrix$Matrix.isDenseRowMajor()Z": 8,
    "vecxt.matrix$Matrix.numel()I": 8,
    "vecxt.matrix$Matrix.offset()I": 8,
    "vecxt.matrix$Matrix.rowStride()I": 8,
    "vecxt.matrix$Matrix.rows()I": 8,
    "vecxt.ndarray$.mkNDArray(Ljava/lang/Object;[I[II)Lvecxt/ndarray$NDArray;": 13,
    "vecxt.ndarray.shapeArray(Lvecxt/ndarray$NDArray;)[I": 8,
    "vecxt.ndarrayOps.expandDims(Lvecxt/ndarray$NDArray;I)Lvecxt/ndarray$NDArray;": 9
  }
}

Simon Parten and others added 4 commits August 11, 2026 11:09
mapRows reads a row at a time and writes a row at a time, but built its
destination with Matrix(raw, rows, cols) — dense column-major — so every
write strode by `rows`. After the previous commit the read side already
arraycopies for a row-major source; making the destination row-major
means writeRow arraycopies too, so mapRows on a row-major input is now
contiguous on both ends. It also brings this in line with the layout
policy the scalar ops document in doublematrix.scala (result orientation
follows the operation's own axis) instead of unconditionally normalising
to column-major. mapCols keeps column-major for the mirror reason.

Guarded on zero dimensions: the strided factory goes through
strideMatInstantiateCheck, which rejects rows/cols <= 0, whereas the
(raw, rows, cols) factory permits them — without the guard an empty
matrix would start throwing InvalidMatrix instead of mapping to an
empty result.

Two existing assertions compared `mapRows(...).raw` against a
column-major expectation, i.e. they pinned storage order rather than
logical content, so they fail on any layout change however correct.
Both now compare via assertMatrixEquals, which indexes by (i, j) and is
indifferent to either side's layout; the logical values they check are
unchanged. Added tests asserting the two orientations explicitly, so a
future change to either default has to be deliberate, plus a
single-column case where row- and column-major coincide.
…Into

The bytecode audit flagged four ScalaRunTime array_apply/array_update
call sites — the generic fallback loops in horzcat and vertcat. Cause
was mine: I had written those loops as per-element reads over
`Array[A]` and, at the same time, demoted `vertcat` from `inline def`
to a plain `def` (horzcat was already plain, but its old body only used
appendedAll/arraycopy, which take Object wholesale and so never needed
typed element access).

With `A` abstract and the method not inline, `Array[A]` erases to
Object and every element access compiles to ScalaRunTime — boxing, and
per C6a's own notes no primitive array left for a vectorised path to
fall back from. Scala 3 has no @specialized, so `inline` is the
mechanism that recovers a concrete double[]/int[] per call site; it is
why every other element-touching method in this file is already inline.
Both are now `inline def`, which restores vertcat's original modifier.
This specialises the access rather than merely silencing the check.

Also folded the four near-identical copy branches into one private
inline `blitInto(src, dest, destRows, rowOff, colOff)`. horzcat and
vertcat differ only in which offset advances, so they are now two calls
each. Its three paths (whole-matrix arraycopy / per-column arraycopy /
strided linearIndex read) are the same ones as before, with the index
arithmetic checked against the previous inline versions — including the
degenerate zero-row/zero-column operands, which now take the
whole-matrix path rather than a zero-trip column loop.
@Quafadas
Quafadas merged commit 6a0c786 into main Aug 11, 2026
7 checks passed
Quafadas added a commit that referenced this pull request Aug 11, 2026
…#149)

* intarray: scaladoc all three methods, de-inline select and contiguous

select and contiguous are now plain `def`s. Both extend Array[Int], so
their reads and writes already compile to iaload/iastore with nothing
for `inline` to specialise, and neither body folds to a constant — they
hold loops. LongArrays.select is the same method over Array[Long] and
was already a plain def, so this also removes an inconsistency. C9's
ratchet names "something became `inline` that did not need to be" as a
regression it watches for, and this moves that metric the right way:
two loop bodies stop being copied into every call site.

mask keeps `inline`, deliberately. Unlike its neighbours it is generic
in A, so Array[A] erases to Object and all three of dimCheck(vec, index)
(which reads vec.length), vec(i) and newVec(j) = _ compile to
ScalaRunTime$ calls — array_length, array_apply, array_update — every
one of them on C6a's banned list. Scala 3 has no @specialized, so
`inline` is what recovers a concrete primitive array at the call site.
This is the same line NDArrayCheck.scala already draws in its header
comment: generic code that reaches into Array[A] keeps inline, generic
code that only touches Array[Int] does not need it. Dropping it here
would reproduce the C6a failure that horzcat/vertcat hit in #146.

Scaladoc covers behaviour rather than restating the signature, in
particular the edges that other code depends on: mask's result length
is the number of trues; select is a gather, so indices may be permuted
or repeated and the result length is unrelated to the source's; and
contiguous is vacuously true for empty and single-element arrays, which
is precisely what lets Matrix.submatrix and
Matrix.apply(rowRange, colRange) take the zero-copy path for a
single-index selection like mat(Array(1), ::).

Also gave mask an explicit Array[A] return type, which was inferred.

* [autofix.ci] apply automated fixes

* intarray: annotate select and contiguous @hotpath, drop breakable

Both carry a loop, so per the annotation docs the applicable budget is
FreqInlineSize and the annotation is @hotpath, not @thin@thin is for
forwarders and explicitly forbids the backward branch these have. This
is the same call intarrays.=:= documents for itself. Neither gets
@AllocFree: select returns a fresh array by construction, which the
annotation's own doc rules out ("a method returning a fresh array is not
a candidate however hot it is").

mask stays unannotated. Its body is a per-element loop and would
otherwise qualify, but check A1 fails an annotation on an `inline def`
by name — an inline body is never emitted, so there is no bytecode for
C2 to measure and the annotation would read as a guarantee nothing
verifies. Here the two requirements are mutually exclusive: annotating
it would mean dropping the `inline` that C6a requires for its generic
Array[A] access.

contiguous no longer uses scala.util.control.Breaks. That was not
cosmetic. `breakable` takes its block by-name, so the loop compiled to a
lambda and the `var`s it mutated were boxed into IntRef/BooleanRef to be
captured — an allocation per call on a path every submatrix and every
apply(rowRange, colRange) runs, to express an early exit the `while`
condition states directly. It also left @hotpath with nothing to
describe: the per-element work sat in the synthetic lambda rather than
in this method's bytecode, so the annotation would have measured the
wrong body. The condition-based loop is behaviour-identical; I checked
empty, single-element, ascending, descending, stepped, repeated and
break-at-the-last-pair against the old semantics, and added tests for
the cases the scaladoc now claims — the empty and single-element ones
being load-bearing for the matrix zero-copy path.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Simon Parten <simon.parten@sire.ch>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
@Quafadas
Quafadas deleted the matrixutil-horzcat-vertcat branch August 11, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant