Skip to content

Refresh the benchmark suite and implement low hanging performance fixes (AI driven PR) - #511

Open
KristofferC wants to merge 16 commits into
masterfrom
kc/low_hanging_fruits
Open

Refresh the benchmark suite and implement low hanging performance fixes (AI driven PR)#511
KristofferC wants to merge 16 commits into
masterfrom
kc/low_hanging_fruits

Conversation

@KristofferC

@KristofferC KristofferC commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

This PR consists of AI driven fixes to low-hanging performance "bugs". It first makes the benchmark suite work, then for each commit there is a description of the fix, the performance data, addition to the benchmark suite, and possibly also adding test that checks allocations/inference and maybe if there is missing coverage.

The PR can be summarized via the table:

   Commit     Change                                       Before → after
  ━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   3e33ad8    Modernize benchmark harness                  Infrastructure
  ─────────  ───────────────────────────────────────────  ──────────────────────────────────────────────────────
   1f23e2f    Allocation-free BlockKron.axes               Matrix: 753 ns, 12 allocs → 2.3 ns, 0 allocs
  ─────────  ───────────────────────────────────────────  ──────────────────────────────────────────────────────
   c9cd60f    Linear merge for sorted block boundaries     215 μs → 20.5 μs
  ─────────  ───────────────────────────────────────────  ──────────────────────────────────────────────────────
   90b126b    Type-stable khatri_rao                       14.5 μs → 2.59 μs
  ─────────  ───────────────────────────────────────────  ──────────────────────────────────────────────────────
   0c1f10b    Delegate matching BlockedArray broadcasts    29.0 μs, 8 allocs → 2.94 μs, 0 allocs
  ─────────  ───────────────────────────────────────────  ──────────────────────────────────────────────────────
   b95da7f    N-dimensional block broadcast fast path      44.8 μs → 13.4 μs
  ─────────  ───────────────────────────────────────────  ──────────────────────────────────────────────────────
   893b59c    Reduce sums per stored block                 sum: 90.4 μs → 3.48 μs; sum(abs2): 91.0 μs → 4.10 μs
  ─────────  ───────────────────────────────────────────  ──────────────────────────────────────────────────────
   c75e9d1    Compute 2-norm from block norms              191.8 μs, 1 alloc → 18.7 μs, 0 allocs

Give benchmarks a dedicated environment and use BenchmarkTools JSON serialization instead of the undeclared JLD/FileIO stack. Also repair report generation on current Julia.
Represent uniform Kronecker block lengths with Fill instead of allocating and accumulating dense vectors. Preserve implicit singleton dimensions for mixed vector/matrix products.

MWE: benchmark metadata/BlockKron/axes

Vector: 3.213 us, 160.12 KiB, 6 allocs -> 2.041 ns, 0 allocs
Matrix: 752.941 ns, 30.25 KiB, 12 allocs -> 2.292 ns, 0 allocs
Specialize sortedunion for strided integer vectors so broadcast axis combination does one linear pass instead of hashing and sorting already-sorted boundaries.

MWE: benchmark metadata/sortedunion/vectors (10k + 10k boundaries)

Before: 215.334 us, 1.33 MiB, 53 allocs
After: 20.458 us, 160.06 KiB, 3 allocs
Build result blocks with a typed product map and take non-copying views of the inputs. This removes the nested Any vectors and temporary copies of every input block.

MWE: benchmark product/khatri_rao/10x10/2x2 blocks

Before: 14.458 us, 63.78 KiB, 702 allocs, return type Any
After: 2.588 us, 23.44 KiB, 212 allocs, concrete BlockMatrix
Unwrap pure BlockedStyle broadcasts for in-place assignment and reuse identical block axes instead of rebuilding their boundaries. Mismatched partitions continue through the existing axis-combination path.

MWE: benchmark broadcast/in-place/BlockedArray/matching (128x128, 32x32 blocks)

Before: 29.041 us, 2.25 KiB, 8 allocs
After: 2.940 us, 0 bytes, 0 allocs
Generalize the vector-only fast path to any dimensionality when every array argument has exactly the destination's block axes. Dimension expansion and mismatched partitions retain the generic sub-block iterator.

MWE: benchmark broadcast/in-place/BlockArray/matching (128x128, 32x32 blocks)

Before: 44.833 us, 0 allocs
After: 13.417 us, 0 allocs
Specialize whole-array sum and mapped sum without changing the existing dims path. Apply init only once at the outer reduction and retain Base's generic identity handling when there are no stored blocks.

MWE: benchmark reduction/{sum,sum(abs2)}/BlockArray/matrix (128x128, 32x32 blocks)

sum: 90.375 us -> 3.479 us (26.0x)
sum(abs2): 91.000 us -> 4.101 us (22.2x)
Both remain allocation-free.
@KristofferC

Copy link
Copy Markdown
Collaborator Author

Tangentially, I use https://github.com/KristofferC/Tachometer.jl to track changes to benchmark numbers over commits, for example, Ferrite-FEM/Ferrite.jl#1475 (comment). It also generates a dashboard where you can track performance over time https://ferrite-fem.github.io/Ferrite.jl/benchmarks/. Just mentioning here in case it could be useful.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.60%. Comparing base (81a9c1c) to head (aa4f109).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #511      +/-   ##
==========================================
- Coverage   94.39%   92.60%   -1.80%     
==========================================
  Files          19       20       +1     
  Lines        1821     1906      +85     
==========================================
+ Hits         1719     1765      +46     
- Misses        102      141      +39     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Combine each stored block's stable 2-norm with hypot. Other p-norms and arrays with no stored blocks continue through the generic implementation.

MWE: benchmark reduction/norm/BlockArray/matrix (128x128, 32x32 blocks)

Before: 191.750 us, 48 bytes, 1 alloc
After: 18.666 us, 0 bytes, 0 allocs
@KristofferC
KristofferC force-pushed the kc/low_hanging_fruits branch from 2067ddd to c75e9d1 Compare August 12, 2026 10:42
@KristofferC

Copy link
Copy Markdown
Collaborator Author

A possibly alternative way to do this is to first repair the benchmark suite and add all the new benchmarks and then do the improvements in another PR.

Comment thread src/blockbroadcast.jl Outdated
Comment thread src/blockbroadcast.jl Outdated
sortedunion(a::AbstractUnitRange, b::AbstractUnitRange) = min(first(a),first(b)):max(last(a),last(b))
combine_blockaxes(a, b) = _BlockedUnitRange(sortedunion(blocklasts(a), blocklasts(b)))
combine_blockaxes(a::BlockedOneTo, b::BlockedOneTo) = BlockedOneTo(sortedunion(blocklasts(a), blocklasts(b)))
combine_blockaxes(a, b) = blockisequal(a, b) ? a : _BlockedUnitRange(sortedunion(blocklasts(a), blocklasts(b)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this introduce a type-instability? Maybe we should add @inferred to appropriate places in the tests to makes check.

Comment thread src/blockreduce.jl Outdated
Base.mapreduce(f::F, op::OP, B::BlockedArray; kw...) where {F, OP} =
mapreduce(f, op, B.blocks; kw...)

Base.sum(B::BlockArray; dims=:, kw...) = sum(identity, B; dims, kw...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be overriden on the level of mapreduce?

Comment thread src/blockreduce.jl Outdated
mapreduce(f, op, B.blocks; kw...)

Base.sum(B::BlockArray; dims=:, kw...) = sum(identity, B; dims, kw...)
function Base.sum(f, B::BlockArray; dims=:, kw...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Comment thread src/blockreduce.jl Outdated
There is no public Base vector merge that exploits sorted inputs. Co-locate BlockArrays' linear vector specialization with the existing lazy sortedunion axis methods, and document why the local implementation is needed.
Only reuse an input axis when the matching and merged branches have the same concrete representation. Add inference tests for reusable, heterogeneous integer, and lazy block axes.
Put the per-block identity/add_sum reduction at the mapreduce level and make sum(::BlockArray) delegate to it. Direct mapreduce calls now share the optimized path while dims and empty storage retain Base's generic behavior.
Generalize the add_sum mapreduce specialization to mapped reductions and make sum(f, ::BlockArray) delegate to it. Cover direct mapped mapreduce calls, init, dims fallback, and BlockVector dispatch.
Extend LinearAlgebra's dedicated norm2 hook instead of norm itself. Generic norm dispatch now handles all other p values without an invoke, while the blockwise stable hypot reduction remains allocation-free.
@KristofferC

Copy link
Copy Markdown
Collaborator Author

I tried to address the comments, one commit per review comment.

Comment thread src/blockreduce.jl Outdated
Base.mapreduce(f::F, op::OP, B::BlockedArray; kw...) where {F, OP} =
mapreduce(f, op, B.blocks; kw...)

Base.sum(B::BlockArray; dims=:, kw...) = mapreduce(identity, Base.add_sum, B; dims, kw...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, d0cd82d

Base already routes AbstractArray sums and mapped sums through mapreduce for whole-array reductions. Remove the redundant BlockArray methods and let the add_sum specialization remain the single extension point.
A logically nonempty BlockArray may still store zero-length blocks. Exclude them from the outer per-block reduction so mapped sums do not require an additive identity for an empty block when the overall input is nonempty.
Julia's generic 2-norm propagates NaN even when another element is infinite, whereas hypot(NaN, Inf) returns Inf. Use a combining function that propagates NaN and missing before applying hypot.
@KristofferC

Copy link
Copy Markdown
Collaborator Author

Hopefully, the latest set of review comments are addressed

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.

2 participants