Fix getindex invalidation from AbstractArray + BlockIndices#499
Conversation
The method getindex(::AbstractArray{T,N}, ::BlockIndices{N}) caused
12,082 method invalidations when loading BlockArrays, because it
invalidated compiled instances of getindex for all AbstractArray types.
Remove the AbstractArray fallback and instead add specialized unblock
methods in views.jl that handle BlockIndices on non-blocked axes
(e.g. Base.OneTo) by decomposing into block-level indexing followed by
sub-indexing. The existing getindex(::AbstractUnitRange, ::Block{1})
handles the block-level step, avoiding the recursive dispatch that
previously required the AbstractArray fallback.
The LayoutArray specializations (which cover all AbstractBlockArray
types) and the AbstractBlockedUnitRange methods remain unchanged.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
|
@ChrisRackauckas looks fine assuming the tests pass but I had two comments:
|
|
There aren't great ways to test for invalidations. |
The previous commit removed the broad getindex(::AbstractArray, ::BlockIndices)
method but this caused a regression for OneTo[BlockIndexRange] which went from
returning UnitRange (O(1)) to Vector (O(n)) through the Base._getindex path.
Add a narrow getindex(::AbstractUnitRange{<:Integer}, ::BlockIndices{1}) that
handles plain ranges efficiently without causing the mass invalidation that the
AbstractArray method caused. AbstractBlockedUnitRange has its own more-specific
method that takes precedence.
Benchmarks show no regression on any path:
- OneTo[BlockIndexRange]: identical perf and return type (UnitRange)
- Vector[BlockIndexRange]: 2x faster (82ns vs 167ns, halved allocations)
- BlockArray ops: identical
- mul! operations: identical
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Runtime Performance BenchmarksI ran comprehensive benchmarks comparing the fix against the original code (Julia 1.12.4). All benchmarks use Summary: No regression anywhere. 2x speedup on one path.
Key observations:
Design notes:The initial version of this PR caused a regression for Types that are not Benchmark scriptusing BlockArrays, LinearAlgebra, BenchmarkTools
BenchmarkTools.DEFAULT_PARAMETERS.seconds = 3
A_med = BlockArray(rand(100, 100), [25, 25, 25, 25], [50, 50])
v_med = BlockArray(rand(100), [25, 25, 25, 25])
ax_med = axes(A_med, 1)
ot100 = Base.OneTo(100)
ot1000 = Base.OneTo(1000)
R_med = rand(100, 100)
r_med = rand(100)
V_med = view(Array(A_med), :, 1:50)
function run_bench(label, f)
f(); val = f(); T = typeof(val)
b = @benchmark $f()
t = round(Int, median(b).time)
println("$(rpad(label, 52)) $(lpad(t, 8)) $(b.allocs) $(b.memory) $T")
end
run_bench("BA[Block(2)[1:10], Block(1)[1:20]]", () -> A_med[Block(2)[1:10], Block(1)[1:20]])
run_bench("BA[Block(2,1)]", () -> A_med[Block(2,1)])
run_bench("BlockedAxis[Block(2)[1:10]]", () -> ax_med[Block(2)[1:10]])
run_bench("OneTo(100)[Block(1)[1:50]]", () -> ot100[Block(1)[1:50]])
run_bench("OneTo(1000)[Block(1)[1:500]]", () -> ot1000[Block(1)[1:500]])
run_bench("Vector(100)[Block(1)[1:50]]", () -> r_med[Block(1)[1:50]])
run_bench("view(A,100x50)[Block(1)[1:10],Block(1)[1:20]]", () -> V_med[Block(1)[1:10], Block(1)[1:20]])
C = rand(100,50); A = rand(100,50); B = rand(50,50)
run_bench("mul!(view(C,:,1:50), A, B, 1, 2)", () -> mul!(view(copy(C),:,1:50), A, B, 1, 2)) |
|
Seems like there is no regression from this and it passes tests. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #499 +/- ##
==========================================
- Coverage 94.52% 94.39% -0.13%
==========================================
Files 19 19
Lines 1789 1819 +30
==========================================
+ Hits 1691 1717 +26
- Misses 98 102 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
The performance benchmark tests are amazing! Do I need to pay for Claude to do this myself? |
Summary
getindex(::AbstractArray{T,N}, ::BlockIndices{N})caused 12,082 method invalidations when loading BlockArraysAbstractArrayfallback and added specializedunblockmethods inviews.jlthat handleBlockIndiceson non-blocked axes (e.g.Base.OneTo) by decomposing into block-level + sub-indexingLayoutArrayspecializations (covering allAbstractBlockArraytypes) andAbstractBlockedUnitRangemethods are unchangedContext
When profiling the TTFX of ModelingToolkit's DAE pipeline,
BlockArrayswas identified as the #2 source of method invalidations. Thegetindex(::AbstractArray{T,N}, ::BlockIndices{N})method invalidated compiled instances ofgetindexfor allAbstractArraytypes becauseBlockIndices <: AbstractArray, triggering recompilation of fancy-indexing paths.Changes
src/blockaxis.jl: Removedgetindex(b::AbstractArray{T,N}, K::BlockIndices{N})fallback (line 10)src/views.jl: Added twounblockspecializations:AbstractUnitRange{<:Integer}axes: decomposesBlockIndicesintoblock()+ regular sub-indicesAbstractBlockedUnitRangeaxes: preserves original direct-indexing behaviorDesign rationale
The
AbstractArrayfallback was needed becauseunblockinto_indicesrecursively callsgetindex(axis, BlockIndices). For non-blocked axes likeBase.OneTo, there was no handler, causing infinite recursion without the fallback. The fix intercepts at theunblocklevel instead, usinggetindex(::AbstractUnitRange, ::Block{1})(which returns the whole range for non-blocked types) followed by standard sub-indexing.Test plan
🤖 Generated with Claude Code