-
Notifications
You must be signed in to change notification settings - Fork 36
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
Safe co-iteration across an axis for 1+ arrays #63
Conversation
I would like to see this. Then that would provide all the information I'd need to implement the optimizations I talked about, i.e.:
It'd also be great to get the compile time length promotion. |
Are you referring to something that does something like |
I forgot to add A = rand(10, 5);
for i in 1:3, j in 1:4
A[i,j] *= 2
end
|
Are you thinking of something like this? is_dense(::Type{T}, i::Int) where {T} = nothing
is_dense(::Type{<:Array}, i::Int) = true
@inline function is_dense(::Type{<:Union{Transpose{T,A},Adjoint{T,A}}}) where {T,A<:AbstractMatrix{T}}
return is_dense(A, (2, 1)[i])
end
@inline function is_dense(::Type{<:PermutedDimsArray{T,N,I1,I2,A}}, i::Int) where {T,N,I1,I2,A<:AbstractArray{T,N}}
return is_dense(A, I1[i])
end I don't really see how an iterator along each dimension in isolation can incorporate the density since it requires knowing about the previous dimensions and strides. |
Something like: is_dense(::Type{T}, i) where {T} = last(stridelayout(T))[i]
You're right. Given what I said: A = rand(2,3,4);
B = PermutedDimsArray(rand(4,3,2), (3,2,1));
for k in indices((A,B),3), j in indices((A,B),2), i in indices((A,B),1)
# do something with A[i,j,k] and B[i,j,k]
end These each of these should be dense, even though we obviously can't flatten the loops. That is,
|
This sounds a lot like what |
Okay, if that's the understood meaning of Simply doing that, and then checking |
src/ranges.jl
Outdated
end | ||
|
||
Base.first(r::OptionallyStaticRange{<:Any,Val{F}}) where {F} = F | ||
Base.first(r::OptionallyStaticRange{<:Any,<:Any}) = getfield(r, :start) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious, what is the reason to prefer getfield(r, :start)
over r.start
?
I've always used the latter in my own code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, it's just a habit from working with types that have overloaded the getproperty
method for metadata like tables. I can change this, as it's probably easier to read r.start
.
src/ranges.jl
Outdated
return last(r) - first(r) + step(r) | ||
end | ||
else | ||
return Integer(div((last(r) - first(r)) + step(r), step(r))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe Base.udiv_int
if we can guarantee the checks will pass?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code for calculating the length on StepRange
is what I'm basing this one and I should probably expand this out to also include this sort of code.
One last question and I think I can get something more polished together. |
Maybe add struct OptionallyStaticUnitRange{T,F,L} <: AbstractUnitRange{T}
start::F
stop::L
end
maybe_slice(r) = r
function maybe_slice(r::OptionallyStaticRange{T,F,::Val{1},L}) where {T,F,L}
Base.Slice(OptionallyStaticUnitRange{T,F,L}(first(r), last(r)))
end Then you could use Although...I think you're right. |
I would really love if we had a common interface where someone could do this with a new type function Base.length(r::NewRangeType{T}) where{T}
if isempty(r)
return zero(T)
else
return ArrayInterface.unsafe_length(first(r), step(r), last(r))
end
end But that turns into a lot of code very quickly and I should probably save that for a different PR. Would it be okay if I just do a unit range thing for now and then we can see how everyone feels about a more aggressive range focused PR later that could include |
Also indices now produces a `Base.Slice` whenever possible.
Do you mean, define an interface such that The |
I think it would ultimately need to be in base but there are a lot of little problems with how ranges are implemented in base and we'd need to start by getting the
I think it might be good to still wrap the return in |
I think this looks good. Are you happy with it? |
This basically does the same thing as `eachindex(A1, A2)` but if the resulting iterators are `AbstractUnitRange{<:Integer}` it wraps the result in a `Slice` so that we know the iterator spans the entire dimension.
With this final change I think it's good. Now you can use |
return reduce(_pick_range, inds) | ||
end | ||
|
||
indices(x, d) = indices(axes(x, d)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This computes the indices of the indices, rather than the indices of x
. You're implicitly assuming that axes(x)
is idempotent. The older implementations of OffsetArrays would break this, for example, although the community seems to have widely settled on idempotency as a useful characteristic for the axes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In terms of iterating values it should theoretically be the same, but it's not entirely idempotent because indices(x::OneTo) = Slice(OptionallyStaticUnitRange(Val(1), last(x)))
. If indices(x::OffsetArray, d) = indices(axes(x, d))
doesn't produce something that is appropriately offset then it could do something like indices(x::OffsetArray, d) = indices(range(x.offset[d], stop = size(x, d))
.
This should help with #62. The main idea is that we can get the convenience of checking compatible indices like in
eachindex(A...)
, but for specific axes. Possible concerns may be:indices(x)
always return iterable indices instead of a tuple of indices. So the default behavior is likeeachindex
. This does two things for us:for i in indices(args...) ... end
is more predictable becauesi
can always point to a single value of the array(s).indices(x)
needs to be specified. If the behavior is unique for the array then onlyindices(x, i)
needs to be specified.If people don't find the first point compelling enough to depart from behavior like
axes
I can change it. I just thought it was a really interesting idea. I'm also happy to implement a range if it's seen as immediately necessary for this PR.