Skip to content

Commit cca160c

Browse files
authored
Preserve ranges in indexing with IdentityUnitRange(::Base.OneTo) (#211)
* Preserve ranges in indexing with IIUR(::OneTo) * remove _maybewrapaxes * return AbstractUnitRanges in indexing OffsetUnitRanges * restrict _maybewrapoffset to integer ranges * to_indices for OffsetUnitRange{<:Integer} * nightly docstring hack * checkindex for OffsetRange * trailing newline * Stronger test for indexing with UnitRange * improve comments in tests * inline to_indices * getindex(::Array, ::OffsetUnitRange) * Add tests * merge getindex method definitions * Test AbstractUnitRange constructor * Add tests * remove specific reductions for Int32 and Int64 * fix test on nightly * fix conversion to AbstractUnitRange * avoid broadcasting in indexing with custom ranges * fix range on v1.0
1 parent b8753a5 commit cca160c

File tree

5 files changed

+336
-121
lines changed

5 files changed

+336
-121
lines changed

benchmark/benchmarks.jl

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ using OffsetArrays
33

44
const dim = 1000
55

6-
x = Array{Float64}(undef, 2*dim)
7-
y = OffsetArray{Float64}(undef, -dim + 1 : dim)
8-
x2d = Array{Float64}(undef, 2*dim, 2*dim)
9-
y2d = OffsetArray{Float64}(undef, -dim + 1 : dim, -dim + 1 : dim)
6+
x = Array{Float64}(undef, 2*dim);
7+
y = OffsetArray{Float64}(undef, -dim + 1 : dim);
8+
x2d = Array{Float64}(undef, 2*dim, 2*dim);
9+
y2d = OffsetArray{Float64}(undef, -dim + 1 : dim, -dim + 1 : dim);
10+
11+
s = OffsetVector(1:dim, 0);
12+
sur = 1:dim;
13+
sior = OffsetArrays.IdOffsetRange(parent(s));
1014

1115
fill1d(x) = for i in axes(x,1); x[i] = i; end
1216
fill2d(x) = for j in axes(x,2); for i in axes(x,1); x[i,j] = i + j; end; end
@@ -20,24 +24,63 @@ unsafe_update(x) = @inbounds(for i in axes(x,1); x[i] = x[i] + i; end)
2024
unsafe_update2d(x) = @inbounds(for j in axes(x,2); for i in axes(x,1); x[i,j] = x[i,j] + i + j; end; end)
2125
unsafe_update_eachindex(x) = @inbounds(for i in eachindex(x); x[i] = x[i] + i; end)
2226

23-
@show @benchmark fill1d(x)
24-
@show @benchmark fill1d(y)
25-
@show @benchmark fill2d(x2d)
26-
@show @benchmark fill2d(y2d)
27-
@show @benchmark update(x)
28-
@show @benchmark update(y)
29-
@show @benchmark update2d(x2d)
30-
@show @benchmark update2d(y2d)
31-
@show @benchmark update_eachindex(x)
32-
@show @benchmark update_eachindex(y)
33-
34-
@show @benchmark unsafe_fill(x)
35-
@show @benchmark unsafe_fill(y)
36-
@show @benchmark unsafe_fill2d(x2d)
37-
@show @benchmark unsafe_fill2d(y2d)
38-
@show @benchmark unsafe_update(x)
39-
@show @benchmark unsafe_update(y)
40-
@show @benchmark unsafe_update2d(x2d)
41-
@show @benchmark unsafe_update2d(y2d)
42-
@show @benchmark unsafe_update_eachindex(x)
43-
@show @benchmark unsafe_update_eachindex(y)
27+
vectorlinearindexing(a, ax) = a[ax]
28+
vectorCartesianindexing(a, ax1, ax2) = a[ax1, ax2]
29+
nestedvectorlinearindexing(a, ax1, ax2) = a[ax1[ax2]]
30+
31+
macro showbenchmark(ex)
32+
print(ex, " : ")
33+
quote
34+
println(@benchmark esc($ex))
35+
end
36+
end
37+
38+
@showbenchmark fill1d(x)
39+
@showbenchmark fill1d(y)
40+
@showbenchmark fill2d(x2d)
41+
@showbenchmark fill2d(y2d)
42+
@showbenchmark update(x)
43+
@showbenchmark update(y)
44+
@showbenchmark update2d(x2d)
45+
@showbenchmark update2d(y2d)
46+
@showbenchmark update_eachindex(x)
47+
@showbenchmark update_eachindex(y)
48+
49+
@showbenchmark unsafe_fill(x)
50+
@showbenchmark unsafe_fill(y)
51+
@showbenchmark unsafe_fill2d(x2d)
52+
@showbenchmark unsafe_fill2d(y2d)
53+
@showbenchmark unsafe_update(x)
54+
@showbenchmark unsafe_update(y)
55+
@showbenchmark unsafe_update2d(x2d)
56+
@showbenchmark unsafe_update2d(y2d)
57+
@showbenchmark unsafe_update_eachindex(x)
58+
@showbenchmark unsafe_update_eachindex(y)
59+
60+
# Benchmarks of vector indexing using OffsetRanges as axes
61+
@showbenchmark vectorlinearindexing(x, s)
62+
@showbenchmark vectorlinearindexing(x, sur)
63+
@showbenchmark vectorlinearindexing(x, sior)
64+
@showbenchmark vectorlinearindexing(y, s)
65+
@showbenchmark vectorlinearindexing(y, sur)
66+
@showbenchmark vectorlinearindexing(y, sior)
67+
68+
@showbenchmark vectorlinearindexing(sur, s)
69+
@showbenchmark vectorlinearindexing(sur, sur)
70+
@showbenchmark vectorlinearindexing(sur, sior)
71+
72+
@showbenchmark vectorCartesianindexing(x2d, s, s)
73+
@showbenchmark vectorCartesianindexing(x2d, sur, sur)
74+
@showbenchmark vectorCartesianindexing(x2d, sior, sior)
75+
76+
@showbenchmark nestedvectorlinearindexing(x, s, s)
77+
@showbenchmark nestedvectorlinearindexing(x, sur, sur)
78+
@showbenchmark nestedvectorlinearindexing(x, s, sior)
79+
@showbenchmark nestedvectorlinearindexing(x, sur, sior)
80+
@showbenchmark nestedvectorlinearindexing(x, sior, sior)
81+
@showbenchmark vectorlinearindexing(x, sior[sior])
82+
@showbenchmark nestedvectorlinearindexing(x, sur, sior)
83+
@showbenchmark vectorlinearindexing(x, sur[sior])
84+
@showbenchmark nestedvectorlinearindexing(x, sior, sur)
85+
@showbenchmark vectorlinearindexing(x, sior[sur])
86+
@showbenchmark nestedvectorlinearindexing(x, sur, sur)

src/OffsetArrays.jl

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -392,44 +392,92 @@ Broadcast.broadcast_unalias(dest::OffsetArray, src::OffsetArray) = parent(dest)
392392

393393
### Special handling for AbstractRange
394394

395-
const OffsetRange{T} = OffsetArray{T,1,<:AbstractRange{T}}
396-
const OffsetUnitRange{T} = OffsetArray{T,1,<:AbstractUnitRange{T}}
395+
const OffsetRange{T} = OffsetVector{T,<:AbstractRange{T}}
396+
const OffsetUnitRange{T} = OffsetVector{T,<:AbstractUnitRange{T}}
397397
const IIUR = IdentityUnitRange{S} where S<:AbstractUnitRange{T} where T<:Integer
398398

399399
Base.step(a::OffsetRange) = step(parent(a))
400400

401-
@propagate_inbounds function Base.getindex(a::OffsetRange, r::OffsetRange)
402-
OffsetArray(a.parent[r.parent .- a.offsets[1]], axes(r))
401+
Base.checkindex(::Type{Bool}, inds::AbstractUnitRange, or::OffsetRange) = Base.checkindex(Bool, inds, parent(or))
402+
403+
# Certain special methods for linear indexing with integer ranges (or OffsetRanges)
404+
# These may bypass the default getindex(A, I...) pathway if the parent types permit this
405+
# For example AbstractUnitRanges and Arrays have special linear indexing behavior defined
406+
407+
# If both the arguments are offset, we may unwrap the indices to call (::OffsetArray)[::AbstractRange{Int}]
408+
@propagate_inbounds function Base.getindex(A::OffsetArray, r::OffsetRange{Int})
409+
_maybewrapoffset(A[parent(r)], axes(r))
410+
end
411+
# If the indices are offset, we may unwrap them and pass the parent to getindex
412+
@propagate_inbounds function Base.getindex(A::AbstractRange, r::OffsetRange{Int})
413+
_maybewrapoffset(A[parent(r)], axes(r))
414+
end
415+
416+
# An OffsetUnitRange might use the rapid getindex(::Array, ::AbstractUnitRange{Int}) for contiguous indexing
417+
@propagate_inbounds function Base.getindex(A::Array, r::OffsetUnitRange{Int})
418+
B = A[_contiguousindexingtype(parent(r))]
419+
OffsetArray(B, axes(r))
420+
end
421+
422+
# avoid hitting the slow method getindex(::Array, ::AbstractRange{Int})
423+
# instead use the faster getindex(::Array, ::UnitRange{Int})
424+
@propagate_inbounds function Base.getindex(A::Array, r::Union{IdOffsetRange, IIUR})
425+
B = A[_contiguousindexingtype(r)]
426+
_maybewrapoffset(B, axes(r))
403427
end
404-
@propagate_inbounds function Base.getindex(a::OffsetRange, r::IdOffsetRange)
405-
OffsetArray(a.parent[r.parent .+ (r.offset - a.offsets[1])], axes(r))
428+
429+
# Linear Indexing of OffsetArrays with AbstractUnitRanges may use the faster contiguous indexing methods
430+
@inline function Base.getindex(A::OffsetArray, r::AbstractUnitRange{Int})
431+
@boundscheck checkbounds(A, r)
432+
# nD OffsetArrays do not have their linear indices shifted, so we may forward the indices provided to the parent
433+
@inbounds B = parent(A)[_contiguousindexingtype(r)]
434+
_maybewrapoffset(B, axes(r))
435+
end
436+
@inline function Base.getindex(A::OffsetVector, r::AbstractUnitRange{Int})
437+
@boundscheck checkbounds(A, r)
438+
# OffsetVectors may have their linear indices shifted, so we subtract the offset from the indices provided
439+
@inbounds B = parent(A)[_subtractoffset(r, A.offsets[1])]
440+
_maybewrapoffset(B, axes(r))
441+
end
442+
443+
# This method added mainly to index an OffsetRange with another range
444+
@inline function Base.getindex(A::OffsetVector, r::AbstractRange{Int})
445+
@boundscheck checkbounds(A, r)
446+
@inbounds B = parent(A)[_subtractoffset(r, A.offsets[1])]
447+
_maybewrapoffset(B, axes(r))
448+
end
449+
450+
# In general we would pass through getindex(A, I...) which calls to_indices(A, I) and finally to_index(I)
451+
# An OffsetUnitRange{Int} has an equivalent IdOffsetRange with the same values and axes,
452+
# something similar also holds for OffsetUnitRange{BigInt}
453+
# We may replace the former with the latter in an indexing operation to obtain a performance boost
454+
@inline function Base.to_index(r::OffsetUnitRange{<:Union{Int,BigInt}})
455+
of = first(axes(r,1)) - 1
456+
IdOffsetRange(_subtractoffset(parent(r), of), of)
406457
end
407-
@propagate_inbounds Base.getindex(a::OffsetRange, r::AbstractRange) = _maybewrapaxes(a.parent[r .- a.offsets[1]], axes(r,1))
408-
@propagate_inbounds Base.getindex(a::AbstractRange, r::OffsetRange) = OffsetArray(a[parent(r)], axes(r))
409458

410459
for OR in [:IIUR, :IdOffsetRange]
411460
for R in [:StepRange, :StepRangeLen, :LinRange, :UnitRange]
412-
@eval @propagate_inbounds Base.getindex(r::$R, s::$OR) = OffsetArray(r[UnitRange(s)], axes(s))
461+
@eval @inline function Base.getindex(r::$R, s::$OR)
462+
@boundscheck checkbounds(r, s)
463+
@inbounds pr = r[UnitRange(s)]
464+
_maybewrapoffset(pr, axes(s,1))
465+
end
413466
end
414467

415468
# this method is needed for ambiguity resolution
416-
@eval @propagate_inbounds Base.getindex(r::StepRangeLen{T,<:Base.TwicePrecision,<:Base.TwicePrecision}, s::$OR) where T =
417-
OffsetArray(r[UnitRange(s)], axes(s))
418-
419-
#= Integer UnitRanges may return an appropriate AbstractUnitRange{<:Integer}, as the result may be used in indexing, and
420-
indexing is faster with ranges =#
421-
@eval @propagate_inbounds function Base.getindex(r::UnitRange{<:Integer}, s::$OR)
422-
rs = r[UnitRange(s)]
423-
offset_s = first(axes(s,1)) - 1
424-
IdOffsetRange(UnitRange(rs .- offset_s), offset_s)
469+
@eval @inline function Base.getindex(r::StepRangeLen{T,<:Base.TwicePrecision,<:Base.TwicePrecision}, s::$OR) where T
470+
@boundscheck checkbounds(r, s)
471+
@inbounds pr = r[UnitRange(s)]
472+
_maybewrapoffset(pr, axes(s,1))
425473
end
426474
end
427475

428476
# mapreduce is faster with an IdOffsetRange than with an OffsetUnitRange
429477
# We therefore convert OffsetUnitRanges to IdOffsetRanges with the same values and axes
430478
function Base.mapreduce(f, op, As::OffsetUnitRange{<:Integer}...; kw...)
431479
ofs = map(A -> first(axes(A,1)) - 1, As)
432-
AIds = map((A, of) -> IdOffsetRange(UnitRange(parent(A)) .- of, of), As, ofs)
480+
AIds = map((A, of) -> IdOffsetRange(_subtractoffset(parent(A), of), of), As, ofs)
433481
mapreduce(f, op, AIds...; kw...)
434482
end
435483

src/axes.jl

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ julia> ro[-1]
2020
-1
2121
2222
julia> ro[3]
23-
ERROR: BoundsError: attempt to access 3-element UnitRange{$Int} at index [5]
23+
ERROR: BoundsError: attempt to access 3-element OffsetArrays.$(IdOffsetRange{Int,UnitRange{Int}}) with indices -1:1 at index [3]
2424
```
2525
2626
If the range doesn't start at 1, the values may be different from the indices:
@@ -35,7 +35,7 @@ julia> ro[-1]
3535
9
3636
3737
julia> ro[3]
38-
ERROR: BoundsError: attempt to access 3-element UnitRange{$Int} at index [5]
38+
ERROR: BoundsError: attempt to access 3-element OffsetArrays.$(IdOffsetRange{Int,UnitRange{Int}}) with indices -1:1 at index [3]
3939
```
4040
4141
# Extended help
@@ -121,11 +121,11 @@ end
121121

122122
# Conversions to an AbstractUnitRange{Int} (and to an OrdinalRange{Int,Int} on Julia v"1.6") are necessary
123123
# to evaluate CartesianIndices for BigInt ranges, as their axes are also BigInt ranges
124-
AbstractUnitRange{T}(r::IdOffsetRange) where {T<:Integer} = IdOffsetRange{T}(r)
124+
Base.AbstractUnitRange{T}(r::IdOffsetRange) where {T<:Integer} = IdOffsetRange{T}(r)
125125

126126
# A version upper bound on this may be set after https://github.com/JuliaLang/julia/pull/40038 is merged
127127
if v"1.6" <= VERSION
128-
OrdinalRange{T,T}(r::IdOffsetRange) where {T<:Integer} = IdOffsetRange{T}(r)
128+
Base.OrdinalRange{T,T}(r::IdOffsetRange) where {T<:Integer} = IdOffsetRange{T}(r)
129129
end
130130

131131
# TODO: uncomment these when Julia is ready
@@ -172,15 +172,19 @@ end
172172
return (ret[1] + r.offset, ret[2])
173173
end
174174

175-
@propagate_inbounds Base.getindex(r::IdOffsetRange, i::Integer) = r.parent[i - r.offset] + r.offset
176-
@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::AbstractUnitRange{<:Integer})
177-
offset_s = first(axes(s,1)) - 1
178-
pr = r.parent[s .- r.offset] .+ (r.offset - offset_s)
179-
_maybewrapoffset(pr, offset_s, axes(s,1))
175+
@inline function Base.getindex(r::IdOffsetRange, i::Integer)
176+
@boundscheck checkbounds(r, i)
177+
@inbounds r.parent[i - r.offset] + r.offset
178+
end
179+
@inline function Base.getindex(r::IdOffsetRange, s::AbstractUnitRange{<:Integer})
180+
@boundscheck checkbounds(r, s)
181+
@inbounds pr = r.parent[_subtractoffset(s, r.offset)] .+ r.offset
182+
_maybewrapoffset(pr, axes(s,1))
180183
end
181184
# The following method is required to avoid falling back to getindex(::AbstractUnitRange, ::StepRange{<:Integer})
182-
@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::StepRange{<:Integer})
183-
rs = r.parent[s .- r.offset] .+ r.offset
185+
@inline function Base.getindex(r::IdOffsetRange, s::StepRange{<:Integer})
186+
@boundscheck checkbounds(r, s)
187+
@inbounds rs = r.parent[s .- r.offset] .+ r.offset
184188
return no_offset_view(rs)
185189
end
186190

src/utils.jl

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ _offset(axparent::AbstractUnitRange, ::Union{Integer, Colon}) = 1 - first(axpare
1616
"""
1717
OffsetArrays.AxisConversionStyle(typeof(indices))
1818
19-
`AxisConversionStyle` declares if `indices` should be converted to a single `AbstractUnitRange{Int}`
20-
or to a `Tuple{Vararg{AbstractUnitRange{Int}}}` while flattening custom types into indices.
21-
This method is called after `to_indices(A::Array, axes(A), indices)` to provide
19+
`AxisConversionStyle` declares if `indices` should be converted to a single `AbstractUnitRange{Int}`
20+
or to a `Tuple{Vararg{AbstractUnitRange{Int}}}` while flattening custom types into indices.
21+
This method is called after `to_indices(A::Array, axes(A), indices)` to provide
2222
further information in case `to_indices` does not return a `Tuple` of `AbstractUnitRange{Int}`.
2323
24-
Custom index types should extend `AxisConversionStyle` and return either `OffsetArray.SingleRange()`,
25-
which is the default, or `OffsetArray.TupleOfRanges()`. In the former case, the type `T` should
26-
define `Base.convert(::Type{AbstractUnitRange{Int}}, ::T)`, whereas in the latter it should define
27-
`Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, ::T)`.
24+
Custom index types should extend `AxisConversionStyle` and return either `OffsetArray.SingleRange()`,
25+
which is the default, or `OffsetArray.TupleOfRanges()`. In the former case, the type `T` should
26+
define `Base.convert(::Type{AbstractUnitRange{Int}}, ::T)`, whereas in the latter it should define
27+
`Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, ::T)`.
2828
29-
An example of the latter is `CartesianIndices`, which is converted to a `Tuple` of
29+
An example of the latter is `CartesianIndices`, which is converted to a `Tuple` of
3030
`AbstractUnitRange{Int}` while flattening the indices.
3131
3232
# Example
@@ -75,10 +75,23 @@ function _checkindices(N::Integer, indices, label)
7575
N == length(indices) || throw_argumenterror(N, indices, label)
7676
end
7777

78-
_maybewrapaxes(A::AbstractVector, ::Base.OneTo) = no_offset_view(A)
79-
_maybewrapaxes(A::AbstractVector, ax) = OffsetArray(A, ax)
80-
81-
_maybewrapoffset(r::AbstractUnitRange, of, ::Base.OneTo) = no_offset_view(r)
82-
_maybewrapoffset(r::AbstractVector, of, ::Base.OneTo) = no_offset_view(r)
83-
_maybewrapoffset(r::AbstractUnitRange, of, ::Any) = IdOffsetRange(UnitRange(r), of)
84-
_maybewrapoffset(r::AbstractVector, of, axs) = OffsetArray(r .+ of, axs)
78+
@inline _maybewrapoffset(r::AbstractVector, ax::Tuple{Any}) = _maybewrapoffset(r, ax[1])
79+
@inline _maybewrapoffset(r::AbstractUnitRange{<:Integer}, ::Base.OneTo) = no_offset_view(r)
80+
@inline _maybewrapoffset(r::AbstractVector, ::Base.OneTo) = no_offset_view(r)
81+
@inline function _maybewrapoffset(r::AbstractUnitRange{<:Integer}, ax::AbstractUnitRange)
82+
of = first(ax) - 1
83+
IdOffsetRange(_subtractoffset(r, of), of)
84+
end
85+
@inline _maybewrapoffset(r::AbstractVector, ax::AbstractUnitRange) = OffsetArray(r, ax)
86+
87+
# These functions are equivalent to the broadcasted operation r .- of
88+
# However these ensure that the result is an AbstractRange even if a specific
89+
# broadcasting behavior is not defined for a custom type
90+
_subtractoffset(r::AbstractUnitRange, of) = UnitRange(first(r) - of, last(r) - of)
91+
_subtractoffset(r::AbstractRange, of) = range(first(r) - of, stop = last(r) - of, step = step(r))
92+
93+
if VERSION <= v"1.7.0-DEV.1039"
94+
_contiguousindexingtype(r::AbstractUnitRange{<:Integer}) = UnitRange{Int}(r)
95+
else
96+
_contiguousindexingtype(r::AbstractUnitRange{<:Integer}) = r
97+
end

0 commit comments

Comments
 (0)