Fixed type instability in array_cache for LazyArray#1303
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1303 +/- ##
=======================================
Coverage 88.87% 88.88%
=======================================
Files 228 228
Lines 29988 29982 -6
=======================================
- Hits 26653 26650 -3
+ Misses 3335 3332 -3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
My though about this:
But since there are tons of instances of not having a function barrier in the code, and with several or lots of |
|
Hold on a second, there's zero instance of calling Other avenue: since to actually create a first |
|
More info after some discussion with @JordiManyer and some work:
Hacky solution: function _array_cache_type(dict,a::LazyArray)
cache = _array_cache!(dict, a)
return typeof(cache)
end
function array_cache(dict::Dict,a::LazyArray)
T = begin Base.@assume_effects :foldable :nothrow _array_cache_type(dict,a) end
existing = _get_cache(dict,a)
if isnothing(existing)
cache = _array_cache!(dict,a)
dict[objectid(a)] = (a,cache)
return cache::T
else
existing
dict[objectid(a)] = (a,existing)
return existing::T
end
endWe also discussed the opportunity to get rid of the function array_cache(dict::IdDict, a::LazyArray)
T = begin Base.@assume_effects :foldable :nothrow _array_cache_type(dict,a) end
# T = _array_cache_type(dict,a) # this actually allocates all the children's caches..
# exchange the two lines above to get the "existing solution with removed same_branch benchmark"
cache = get!(dict, a) do
_array_cache!(dict, a)
end
return cache::T
endThe |
|
Ok, I think I found an ideal solution: caches will only be computed once, and the only dynamic thing happening is the type assertion function array_cache(dict::IdDict, a::LazyArray)
T = _allocate_cache_and_compute_its_type(dict,a)
cache = get(dict, a) do
@unreachable "The cache should be stored already."
end
return cache::T
end
function _allocate_cache_and_compute_its_type(dict, a::LazyArray)
cache = get!(dict, a) do
_array_cache!(dict, a)
end
return typeof(cache)
endThis code assumes that Some benchmarks using
Benchmark using
|
|
@JordiManyer I think I should implement the last solution on this branch, do we agree that we can remove |
Oh I like this, I like it very much indeed. Let me look at this tomorrow to test myself. Concerning same_branch, let me also have a look. In the PR you mention, the conclusion seems to be that it matters. Maybe @ericneiva remembers in which cases it mattered? |
|
Hello, @JordiManyer and @Antoinemarteau,
I barely remember and I feel bad about this, but I do know the short answer is it matters in weak forms with a large number of terms. IIRC, here is the example that motivated this optimization. It's an implementation of an Embedded ALE formulation of the time-dependent Navier-Stokes equations. Hope this gives you some hints. |
I've been tracking this type instability for some time: with the current implementation, we have
The issue here is that, when we hit the cache and have
!isnothing(cache), the only thing the compiler can see is that the cache comes from aDict{UInt,Any}and thus sets the return type toAny.This is HORRIBLE, since it generates a type instability that hinders the evaluation of every single subsequent call to
getindex!, since thatAnytyping is baked into the cache.After exploring several solutions, I think we should just pay the price of re-computing the caches. We keep the memoization and the memory savings (the recomputed cache gets thrown away), but we are now able to tell the compiler what type it should expect. The final version is:
This is quite delicate, so I would appreciate a review @amartinhuertas @Antoinemarteau