-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve aliasing detection of wrappers of SubArrays #29546
base: master
Are you sure you want to change the base?
Conversation
Another problem is that defining this for all "child array types" such as mightalias(a::AbstractViewArray, b::AbstractArray) = symmetric_mightalias(parent(a), b) |
Isn't it |
Yes. That's why I said "symmetric and easily overloadable." There is a special aliasing analysis in |
@mbauman is on vacation at the moment so it might take a while before he can take a look at this. He’s not the only one who could review, of course, but he is the primary suspect :) |
Thanks! Good to be informed. |
This certainly isn't wrong, but as noted, it doesn't scale. Not sure how many of those specialized |
How about defining |
Right, the whole key to the mightalias/dataids system is that it allows us to generically define symmetric relationships between chunks of memory without getting into a maze of ambiguities and asymmetries. It's intentionally a rudimentary and non-exact heuristic, geared to preserve performance in the by-far-most-common case where two arrays definitively do not alias while simultaneously being tractable to extend. SubArrays are indeed special. They're really the only array view we have that subsets into memory blocks. That's why it's the only builtin specialization for The proper fix for #29545 is for us to define |
I don't think what I'm suggesting would introduce more complex analysis. It's all about delegating to existing dedicated analysis and making use of it more. I suppose that until the buffer type #12447 is introduced, Aside: Reading #29481, I started thinking that Julia may not have a standard way to define extensible symmetric/commutative interface. My idea above is somewhat based on Python's |
I must say that I am very excited that you're excited and hoping to improve the aliasing detection system! This is a new portion of Julia that not many folks have touched and it's great to get a fresh set of eyes on it. You'll probably find the PR where this was introduced interesting — note that I started trying for a much more complicated and exacting design but iteratively simplified to the most basic thing that could work. The part that becomes intractable is SubArrays of other views. There are generally two idioms to create a symmetric interface: one is indeed trying it in both directions (see, e.g., promotion). The other way is just to create an My thought here has been that |
Thanks a lot for detailed reply. Yes, I should have looked into the pre-existing discussion for why the current implementation is chosen. I'll look into #25890 to catch up.
mightalias(a::Array, b::Array) = # dataids-based analysis
mightalias(a::AbstractArray, b::AbstractArray) =
mightalias(aliasingroot(a), aliasingroot(b)) # possible recursion
function aliasingroot(a::AbstractArray) # maybe use @generated to check .parent field?
if parent(a) === a
return a
else
return aliasingroot(parent(a))
end
end
aliasingroot(a::SubArray) = a But if you define a new "root" array type One "solution" I can think of is to decouple "calling API" and "overloading API", like: # Calling API:
mightalias(a::AbstractArray, b::AbstractArray) = __mightalias__(aliasingroot(a), aliasingroot(b))
# Overloading API:
__mightalias__(::AbstractArray, ::AbstractArray) = # dataids-based analysis
__mightalias__(a::SubArray, b::SubArray) = ... With this, I still need to define |
Unfortunately I'd just have a fallback |
Should that be |
@mbauman Right, I saw you mentioning it in the PR you linked. I wrote "
Initially, I thought it would be nice to have a default implementation like @generated function aliasingroot(a::AbstractArray)
if :parent in fieldnames(a)
return :(aliasingroot(a.parent))
else
return :(a)
end
end so that it works automatically for all "child array types" but maybe manually defining
I was trapped in thinking that
@martinholters Yes, I think this is the right direction, too. (And I guess that's why too much magic in default |
10bb87c
to
46754a8
Compare
I implemented |
Would it be possible for this to be in 1.1? |
If it gets merged this week, yes. What's blocking progress here? |
Well, let's make sure Nanosoldier is still happy: @nanosoldier This is definitely an improvement for wrappers of SubArrays. I'm a little hesitant, though, in that it mixes in a layer of complexity to this interface — now you must think about both For example, it'd be great if So that's where I am here — I like it but I'm hesitant. |
I'd even be open to scrapping |
Restarted Nanosoldier. @nanosoldier |
I agree that it's more complex than simple
It looks like
What is If |
Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @ararslan |
Yeah, sorry, my memory on the specializations was fuzzy. I was thinking about combinations of types, but it's not the specializations that's the hard part (those all get inlined on-demand in any case) — it was the number of implementations that you have to write. That's what going to an integer id wins you. As far as Perhaps this is indeed the right design (or a good enough one), but I keep coming back to the fact that this addition is entirely dedicated to improving the detection of aliasing between views of two SubArrays with the same parent. It sure feels like we should be able to come up with something simpler — or at least keep the weight of the code out of the hot path in the far more likely situation where that's not the case. But I'm not seeing an obvious alternative, and it is indeed an important special case. (Edit: I suppose this may indeed be a zero-cost abstraction at runtime; Nanosoldier likes it). |
As far as the number of the interfaces you have to overload, isn't it the same? You have to write
I wonder external packages like https://github.com/Jutho/Strided.jl needs to use it.
What would be the weight in my current implementation? Maybe compilation of this recursion? _anymightalias(as::Tuple, bs::Tuple) =
any(b -> _mightalias(as[1], b), bs) || _anymightalias(tail(as), bs) If so, does defining manual specialization |
@Jutho I see you are building an advanced package (Strided.jl) on top of broadcasting facility. It looks like it is close |
I have to say that I found the current (at the time of creating Strided.jl) aliasing detection scheme somewhat strange//unintuitive/hard to understand, or to use for my purpose, so I kinda gave up on it. Probably that was just me not wanting to invest sufficient effort. So yes, I defined |
For example, I think I got confused about the point of just returning I think this is a not totally uncommon use case. Start with a large vector, define new arrays by taking just a part of that vector and reshaping it into a specific multidimensional array. E.g. the vector could be the different blocks of an array with block-sparsity stacked underneath each other. You still want to detect that the different blocks don't alias. So a slightly more advanced |
Thanks, it's nice that you commented the painful points so that this PR is more convincing to core devs :)
That's exactly what this PR would solve, by internally calling
Isn't "represent memory locations" equivalent to what But I think this shows that |
@Jutho — yes, thank you for the report. You're exactly right: the existing API explicitly punts on those cases and that's precisely what this change is aiming to address. It punts in favor of simplicity. My fear, though, is that if the existing system was already strange/unintuitive/hard, then I don't think this change is going to make things better. Perhaps some of that is indeed in the name of I also wonder if the fact that we currently have a
The advantage is that we only do the more complicated memory overlap checking if we've already proven that there are two components that match. We could completely outline that last branch, passing just |
Exactly, currently in |
Well, it's unsafe and you need to be very careful to |
Sure, I also keep the offset now even though I didn't quite care about aliasing (I should put this in the readme somewhere), so I don't mind keeping it around. I know you need to |
Oh, I see I misread your comment. Sorry — we're in agreement here. My point is that if you have a bare pointer — while you may be able to compute its original Perhaps @oschultz would be interested in this discussion, too — he implemented |
My impression was that it is too simplistic and limited rather than strange/unintuitive/hard. This is just because my first exposure to the system is via the error which led me to this PR. I think current simplicity has been a great choice for the first stages in the development as it captures most useful cases. But I don't think what you are suggesting here is much more complex for API consumers (I'd say it's even simpler, since most of the time you only need to return
This sounds like a good idea. So something like mightalias(A::AbstractArray, B::AbstractArray) =
!isbits(A) && !isbits(B) && !_isdisjoint(dataids(A), dataids(B)) &&
anyoverlaps(A, B)
@inline dataids(A::Array) = ...
@inline dataids(A::SubArray) = ...
@inline dataids(A) = tuplejoin(map(dataids, memoryregions(A))...)
# tuplejoin from https://discourse.julialang.org/t/efficient-tuple-concatenation/5398
@noinline anyoverlaps(A, B) = _anyoverlaps(memoryregions(A), memoryregions(B))
@inline _anyoverlaps(::Tuple{}, ::Tuple) = false
@inline _anyoverlaps(as::Tuple, bs::Tuple) =
any(b -> __anyoverlaps(as[1], b), bs) || _anyoverlaps(tail(as), bs)
@inline __anyoverlaps(::Any, ::Any) = true # @pure?
@inline __anyoverlaps(A::SubArray, B::SubArray) =
... One small uneasy point is that this runs
We can define |
Yes, that's exactly the kind of design I'd like to find. Let's call your
At the same time, I think walking through |
Oops. Good catch! But I think memoryregions(A::Array) = (view(A, :),) so that we hit
I see. My suggestion above requires implementers to define at least one of Maybe requiring both dataids(A::Array) = (UInt(pointer(A)),)
dataids(::Any) = nothing
_dataids(A) = _splatmap(x -> something(dataids(x), default_dataids(x)),
memoryregions(A))
default_dataids(A::AbstractArray) = (UInt(objectid(A)),)
default_dataids(::Any) = ()
mightalias(A::AbstractArray, B::AbstractArray) =
!isbits(A) && !isbits(B) && !_isdisjoint(_dataids(A), _dataids(B)) &&
anyoverlaps(A, B) but this probably defeats the purpose of this hybrid approach for reducing compiler's work in the hot branch.
Thanks for the tips!
Can that happen with |
Just a quick note that I ran into this issue when I was debugging allocations in my code. My case was equivalent to:
The expected output was not to have a copy due to the alias detection, but the output shows:
The issue is that the alias detection is too conservative in this case as I fixed it for my code by just writing a version of copyto! that skips alias detection. |
It fixes #29545, i.e., it makes the following snippet work
However, this PR still has some problems. For example, it does not support
mightalias(::ReinterpretArray, ::SubArray)
. But definingmightalias(::ReinterpretArray, ::AbstractArray)
andmightalias(::AbstractArray, ::ReinterpretArray)
would cause the ambiguity problem (and not DRY).A general question is: How one can ensure that
mightalias
is symmetric (i.e.,mightalias(a, b) == mightalias(b, a)
) and easily overloadable? One possibility is to define a function (say)and document that the first argument of
mightalias
has to specify the type user defined (to reduce ambiguity).Note that
symmetric_mightalias
has to be exposed as a public (non-overloadable) API so that it can be called from user'smightalias
:Is it a sane approach? I'm pretty sure this is a generic problem solved in Julia community ages ago.