Refresh the benchmark suite and implement low hanging performance fixes (AI driven PR) - #511
Refresh the benchmark suite and implement low hanging performance fixes (AI driven PR)#511KristofferC wants to merge 16 commits into
Conversation
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.
|
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
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
2067ddd to
c75e9d1
Compare
|
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. |
| 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))) |
There was a problem hiding this comment.
Does this introduce a type-instability? Maybe we should add @inferred to appropriate places in the tests to makes check.
| 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...) |
There was a problem hiding this comment.
Shouldn't this be overriden on the level of mapreduce?
| 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...) |
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.
|
I tried to address the comments, one commit per review comment. |
| 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...) |
There was a problem hiding this comment.
Doesn't the default implementation of sum already do this?
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.
|
Hopefully, the latest set of review comments are addressed |
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: