Skip to content

Make no_offset_view inferrable#189

Merged
timholy merged 15 commits into
masterfrom
teh/infer_no_offset_view
Feb 9, 2021
Merged

Make no_offset_view inferrable#189
timholy merged 15 commits into
masterfrom
teh/infer_no_offset_view

Conversation

@timholy
Copy link
Copy Markdown
Member

@timholy timholy commented Jan 17, 2021

This changes the implementation so that the axis values
do not affect the type of the output.

Technically this is a breaking change, but only at the level
of the returned types, not at the level of returned values.

This changes the implementation so that the axis *value*s
do not affect the type of the output.

Technically this is a breaking change, but only at the level
of the returned types, not at the level of returned values.
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 17, 2021

Codecov Report

Merging #189 (dac2419) into master (df73941) will decrease coverage by 0.66%.
The diff coverage is 90.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #189      +/-   ##
==========================================
- Coverage   98.92%   98.26%   -0.67%     
==========================================
  Files           5        5              
  Lines         279      288       +9     
==========================================
+ Hits          276      283       +7     
- Misses          3        5       +2     
Impacted Files Coverage Δ
src/utils.jl 100.00% <ø> (ø)
src/OffsetArrays.jl 97.94% <84.61%> (-0.99%) ⬇️
src/axes.jl 100.00% <100.00%> (+2.08%) ⬆️
src/precompile.jl 92.85% <0.00%> (-7.15%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update df73941...dac2419. Read the comment docs.

@timholy
Copy link
Copy Markdown
Member Author

timholy commented Jan 17, 2021

Test failure is just a coverage failure. I'd recommend waiting on this until #185 is dealt with, and any other "non-new-feature" PRs. Then this might go along with any other new features (#187? #179?) for a 1.6 release.

@jishnub
Copy link
Copy Markdown
Member

jishnub commented Jan 17, 2021

I've wondered before if one objective of no_offset_view was to reduce the number of wrappers around the parent? Otherwise type-stability is a good idea

@timholy
Copy link
Copy Markdown
Member Author

timholy commented Jan 17, 2021

I'm sure it is, but anyone who complains can specialize it for their array type. Conversely, we can never work around the instability.

@timholy
Copy link
Copy Markdown
Member Author

timholy commented Jan 19, 2021

Before merging, I plan to add one fallback: for unknown array types with Base.OneTo axes, just return the array. That should address most of #189 (comment).

Copy link
Copy Markdown
Member

@johnnychen94 johnnychen94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good change, but I hope we could pend this PR for a few weeks to see if there are other breaking changes; as far as I know, Augmentor.jl is vulnerable to this change due to its over-verbose dispatches and tests.

Said that, if this PR does fix some known performance issue, please do merge and release it.

@timholy
Copy link
Copy Markdown
Member Author

timholy commented Jan 21, 2021

as far as I know, Augmentor.jl is vulnerable to this change due to its over-verbose dispatches and tests.

On 1.6, it seems to fail tests even on the master branch. Any further thoughts about this? There's no real urgency, but I wondered if the OneTo change here might circumvent Augmentor needing a fix.

@jishnub
Copy link
Copy Markdown
Member

jishnub commented Jan 28, 2021

Perhaps AbstractUnitRanges may be transformed to other AbstractUnitRanges by no_offset_view? This should fix #198.

one choice could be

OffsetArrays.no_offset_view(a::AbstractUnitRange) = UnitRange(a)

@timholy
Copy link
Copy Markdown
Member Author

timholy commented Jan 28, 2021

Better:

OffsetArrays.no_offset_view(a::AbstractUnitRange) = first(a):last(a)

That's more explicit that it only looks at the values, not the indices.

@jishnub
Copy link
Copy Markdown
Member

jishnub commented Jan 28, 2021

Wouldn't this be open to someone defining a custom integer type such that a:b returns Base.IdentityUnitRange(UnitRange(a,b))? UnitRanges on the other hand always use 1-based indexing.

@jishnub
Copy link
Copy Markdown
Member

jishnub commented Feb 7, 2021

While we are at it, we might unwrap views of OffsetArrays as well. For example,

julia> a = reshape(1:25, 5, 5);

julia> sa = @view a[2:3, 2:3];

julia> osa = OffsetArray(sa, OffsetArrays.Origin(2));

julia> sosa = @view osa[:,2];

julia> ososa = OffsetArrays.no_offset_view(sosa)
2-element OffsetArray(view(OffsetArray(view(reshape(::UnitRange{Int64}, 5, 5), 2:3, 2:3), 2:3, 2:3), :, 2), 1:2) with eltype Int64 with indices 1:2:
 7
 8

julia> av = @view sa[:, 1];

julia> av == ososa
true

In this case no_offset_view(sosa) could have stripped off a few layers and returned av. I wonder if such an optimization is worth it?

Something like this for example might be added:

function no_offset_view(SOA::SubArray{<:Any,<:Any,<:OffsetArray,<:Tuple{Vararg{Union{Real,AbstractUnitRange}}}})
    indsSOA = Base.parentindices(SOA)
    OA = parent(SOA)
    inds = _removeoffset(indsSOA, OA.offsets)
    v = view(parent(OA), inds...)
    no_offset_view(v)
end

_removeoffset(i::Tuple{AbstractVector, Vararg{Any}}, o::Tuple{Int,Vararg{Int}}) = (first(i) .- first(o), _removeoffset(tail(i), tail(o))...)
_removeoffset(i::Tuple{Real, Vararg{Any}}, o::Tuple{Int, Vararg{Int}}) = (first(i) - first(o), _removeoffset(tail(i), tail(o))...)
_removeoffset(i::Tuple, o::Tuple{}) = i

in which case

julia> OffsetArrays.no_offset_view(sosa)
2-element view(reshape(::UnitRange{Int64}, 5, 5), 2:3, 2) with eltype Int64:
 7
 8

@timholy
Copy link
Copy Markdown
Member Author

timholy commented Feb 7, 2021

Wouldn't this be open to someone defining a custom integer type such that a:b returns Base.IdentityUnitRange(UnitRange(a,b))? UnitRanges on the other hand always use 1-based indexing.

I guess I was worried about whether UnitRange(Base.IdentityUnitRange(-1, 1)) should return -1:1 or an error. convert seems like it should error (they are not "equivalent", whatever that means 😄), but constructor-calls probably shouldn't. After all, Base.IdentityUnitRange doesn't complain. So upon reflection I like your version better.

Closes #198

Co-authored-by: Jishnu Bhattacharya <jishnub@users.noreply.github.com>
@timholy timholy force-pushed the teh/infer_no_offset_view branch from 055d2f4 to 87f15e9 Compare February 7, 2021 13:49
@timholy timholy changed the title Make no_offset_array inferrable Make no_offset_view inferrable Feb 7, 2021
@timholy
Copy link
Copy Markdown
Member Author

timholy commented Feb 7, 2021

While we are at it, we might unwrap views of OffsetArrays as well

Good idea. I was a little bit worried about how far your suggestion delved into the internal representations of all types. What do you think about the more minimal version I just pushed? It doesn't doubly-unwrap like yours, but it does prevent the types from growing even more complex. As a bonus, it handles other types of shifted views, too (that's not piracy because we own no_offset_view). Demo:

julia> using OffsetArrays

julia> A = reshape(1:12, 3, 4)
3×4 reshape(::UnitRange{Int64}, 3, 4) with eltype Int64:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> V = view(A, OffsetArrays.IdentityUnitRange(2:3), OffsetArrays.IdentityUnitRange(2:3))
2×2 view(reshape(::UnitRange{Int64}, 3, 4), :, :) with eltype Int64 with indices 2:3×2:3:
 5  8
 6  9

julia> typeof(V)
SubArray{Int64, 2, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}, Tuple{Base.IdentityUnitRange{UnitRange{Int64}}, Base.IdentityUnitRange{UnitRange{Int64}}}, false}

julia> OffsetArrays.no_offset_view(V)
2×2 view(reshape(::UnitRange{Int64}, 3, 4), 2:3, 2:3) with eltype Int64:
 5  8
 6  9

julia> typeof(OffsetArrays.no_offset_view(V))
SubArray{Int64, 2, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}, Tuple{UnitRange{Int64}, UnitRange{Int64}}, false}

julia> O = OffsetArray(A, -1:1, 0:3)
3×4 OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3) with eltype Int64 with indices -1:1×0:3:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> V = view(O, 0:1, 1:2)
2×2 view(OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3), 0:1, 1:2) with eltype Int64:
 5  8
 6  9

julia> typeof(V)
SubArray{Int64, 2, OffsetMatrix{Int64, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}, Tuple{UnitRange{Int64}, UnitRange{Int64}}, false}

julia> OffsetArrays.no_offset_view(V)
2×2 view(OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3), 0:1, 1:2) with eltype Int64:
 5  8
 6  9

julia> typeof(OffsetArrays.no_offset_view(V))
SubArray{Int64, 2, OffsetMatrix{Int64, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}, Tuple{UnitRange{Int64}, UnitRange{Int64}}, false}

julia> r1, r2 = OffsetArrays.IdOffsetRange(1:3, -2), OffsetArrays.IdentityUnitRange(2:3)
(OffsetArrays.IdOffsetRange(-1:1), Base.IdentityUnitRange(2:3))

julia> V = view(O, r1, r2)
3×2 view(OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3), OffsetArrays.IdOffsetRange(-1:1), :) with eltype Int64 with indices OffsetArrays.IdOffsetRange(-1:1)×2:3:
 7  10
 8  11
 9  12

julia> typeof(V)
SubArray{Int64, 2, OffsetMatrix{Int64, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}, Tuple{OffsetArrays.IdOffsetRange{Int64, UnitRange{Int64}}, Base.IdentityUnitRange{UnitRange{Int64}}}, false}

julia> OffsetArrays.no_offset_view(V)
3×2 view(OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3), -1:1, 2:3) with eltype Int64:
 7  10
 8  11
 9  12

julia> typeof(OffsetArrays.no_offset_view(V))
SubArray{Int64, 2, OffsetMatrix{Int64, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}, Tuple{UnitRange{Int64}, UnitRange{Int64}}, false}

To me this feels like a good place to be---I don't think we gain a lot by unwrapping the parent OffsetArray, and the risk is less generic handling of the axes.

@timholy
Copy link
Copy Markdown
Member Author

timholy commented Feb 7, 2021

So weird that codecov marks all of no_offset_view and its helpers as untested. Locally, every line of this diff is tested.

@jishnub
Copy link
Copy Markdown
Member

jishnub commented Feb 8, 2021

While we are at it, we might unwrap views of OffsetArrays as well

Good idea. I was a little bit worried about how far your suggestion delved into the internal representations of all types. What do you think about the more minimal version I just pushed? It doesn't doubly-unwrap like yours, but it does prevent the types from growing even more complex. As a bonus, it handles other types of shifted views, too (that's not piracy because we own no_offset_view). Demo:

julia> using OffsetArrays

julia> A = reshape(1:12, 3, 4)
3×4 reshape(::UnitRange{Int64}, 3, 4) with eltype Int64:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> V = view(A, OffsetArrays.IdentityUnitRange(2:3), OffsetArrays.IdentityUnitRange(2:3))
2×2 view(reshape(::UnitRange{Int64}, 3, 4), :, :) with eltype Int64 with indices 2:3×2:3:
 5  8
 6  9

julia> typeof(V)
SubArray{Int64, 2, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}, Tuple{Base.IdentityUnitRange{UnitRange{Int64}}, Base.IdentityUnitRange{UnitRange{Int64}}}, false}

julia> OffsetArrays.no_offset_view(V)
2×2 view(reshape(::UnitRange{Int64}, 3, 4), 2:3, 2:3) with eltype Int64:
 5  8
 6  9

julia> typeof(OffsetArrays.no_offset_view(V))
SubArray{Int64, 2, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}, Tuple{UnitRange{Int64}, UnitRange{Int64}}, false}

julia> O = OffsetArray(A, -1:1, 0:3)
3×4 OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3) with eltype Int64 with indices -1:1×0:3:
 1  4  7  10
 2  5  8  11
 3  6  9  12

julia> V = view(O, 0:1, 1:2)
2×2 view(OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3), 0:1, 1:2) with eltype Int64:
 5  8
 6  9

julia> typeof(V)
SubArray{Int64, 2, OffsetMatrix{Int64, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}, Tuple{UnitRange{Int64}, UnitRange{Int64}}, false}

julia> OffsetArrays.no_offset_view(V)
2×2 view(OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3), 0:1, 1:2) with eltype Int64:
 5  8
 6  9

julia> typeof(OffsetArrays.no_offset_view(V))
SubArray{Int64, 2, OffsetMatrix{Int64, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}, Tuple{UnitRange{Int64}, UnitRange{Int64}}, false}

julia> r1, r2 = OffsetArrays.IdOffsetRange(1:3, -2), OffsetArrays.IdentityUnitRange(2:3)
(OffsetArrays.IdOffsetRange(-1:1), Base.IdentityUnitRange(2:3))

julia> V = view(O, r1, r2)
3×2 view(OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3), OffsetArrays.IdOffsetRange(-1:1), :) with eltype Int64 with indices OffsetArrays.IdOffsetRange(-1:1)×2:3:
 7  10
 8  11
 9  12

julia> typeof(V)
SubArray{Int64, 2, OffsetMatrix{Int64, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}, Tuple{OffsetArrays.IdOffsetRange{Int64, UnitRange{Int64}}, Base.IdentityUnitRange{UnitRange{Int64}}}, false}

julia> OffsetArrays.no_offset_view(V)
3×2 view(OffsetArray(reshape(::UnitRange{Int64}, 3, 4), -1:1, 0:3), -1:1, 2:3) with eltype Int64:
 7  10
 8  11
 9  12

julia> typeof(OffsetArrays.no_offset_view(V))
SubArray{Int64, 2, OffsetMatrix{Int64, Base.ReshapedArray{Int64, 2, UnitRange{Int64}, Tuple{}}}, Tuple{UnitRange{Int64}, UnitRange{Int64}}, false}

To me this feels like a good place to be---I don't think we gain a lot by unwrapping the parent OffsetArray, and the risk is less generic handling of the axes.

This seems like a good (and a pretty cool) solution. If there are specific performance enhancements possible we might look into them later.

@jishnub
Copy link
Copy Markdown
Member

jishnub commented Feb 8, 2021

We might require some special handling of Base.Slice to avoid changing the IndexStyle:

julia> a = reshape(1:25, 5, 5);

julia> oa = OffsetArray(a, OffsetArrays.Origin(2));

julia> soa = @view oa[:, :];

julia> IndexStyle(soa)
IndexLinear()

julia> OffsetArrays.no_offset_view(soa) |> IndexStyle
IndexCartesian()

This might fix it:

no_offset_view(a::Base.Slice) = Base.Slice(UnitRange(a))

timholy and others added 7 commits February 9, 2021 02:58
This changes the implementation so that the axis *value*s
do not affect the type of the output.

Technically this is a breaking change, but only at the level
of the returned types, not at the level of returned values.
Closes #198

Co-authored-by: Jishnu Bhattacharya <jishnub@users.noreply.github.com>
Not introduced by this PR, but easily fixed
In 1.0 Slice doesn't cleanly mean "the whole axis", it's conflated
with IdentityUnitRange.
Comment thread src/OffsetArrays.jl
@timholy
Copy link
Copy Markdown
Member Author

timholy commented Feb 9, 2021

OK to merge?

Comment thread test/runtests.jl Outdated
@jishnub
Copy link
Copy Markdown
Member

jishnub commented Feb 9, 2021

Looks good to be merged

@timholy timholy merged commit 3b5b11b into master Feb 9, 2021
@timholy timholy deleted the teh/infer_no_offset_view branch February 9, 2021 12:42
@timholy
Copy link
Copy Markdown
Member Author

timholy commented Feb 9, 2021

Thanks for all your help, @jishnub!

@timholy
Copy link
Copy Markdown
Member Author

timholy commented Feb 9, 2021

Should this be 1.6 or 1.5.4? It's not exactly a bugfix, but neither is it a new feature.

@jishnub
Copy link
Copy Markdown
Member

jishnub commented Feb 9, 2021

Perhaps 1.6, as certain return types are altered

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants