-
-
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
eagerly do boundscheck when indexing into CartesianIndices #42119
Conversation
fce2daf
to
68caeec
Compare
base/multidimensional.jl
Outdated
@inline _get_cartesianindex(::OneTo, i::Int) = i | ||
@inline _get_cartesianindex(::Base.IdentityUnitRange, i::Int) = i | ||
@inline _get_cartesianindex(r, i) = getindex(r, i) |
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.
A perhaps more appropriate place to make modifications is adding @propagate_inbounds
in
Lines 879 to 914 in 876df79
function getindex(v::UnitRange{T}, i::Integer) where T | |
@inline | |
i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) | |
val = convert(T, v.start + (i - 1)) | |
@boundscheck _in_unit_range(v, val, i) || throw_boundserror(v, i) | |
val | |
end | |
const OverflowSafe = Union{Bool,Int8,Int16,Int32,Int64,Int128, | |
UInt8,UInt16,UInt32,UInt64,UInt128} | |
function getindex(v::UnitRange{T}, i::Integer) where {T<:OverflowSafe} | |
@inline | |
i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) | |
val = v.start + (i - 1) | |
@boundscheck _in_unit_range(v, val, i) || throw_boundserror(v, i) | |
val % T | |
end | |
function getindex(v::OneTo{T}, i::Integer) where T | |
@inline | |
i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) | |
@boundscheck ((i > 0) & (i <= v.stop)) || throw_boundserror(v, i) | |
convert(T, i) | |
end | |
function getindex(v::AbstractRange{T}, i::Integer) where T | |
@inline | |
i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) | |
ret = convert(T, first(v) + (i - 1)*step_hp(v)) | |
ok = ifelse(step(v) > zero(step(v)), | |
(ret <= last(v)) & (ret >= first(v)), | |
(ret <= first(v)) & (ret >= last(v))) | |
@boundscheck ((i > 0) & ok) || throw_boundserror(v, i) | |
ret | |
end |
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 root issue here is that throw_boundserror(v, i)
in getindex(v::OneTo{T}, i::Integer)
is marked as @noinline
and thus disables the propagation of @inbounds
Line 691 in 8812c5c
throw_boundserror(A, I) = (@noinline; throw(BoundsError(A, I))) |
I now think the current version of this PR is a wrong fix; we can't unconditionally skip the boundscheck here.
@chriselrod Do you have ideas on how to bypass this boundscheck?
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 don't understand why an error path would do this. But shouldn't the whole expression be in the @boundscheck
? That is, instead of
@boundscheck ((i > 0) & ok) || throw_boundserror(v, i)
you just need an extra set of parentheses:
@boundscheck(((i > 0) & ok) || throw_boundserror(v, i))
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.
You're right, this @boundscheck
does get eliminated if we check individual getindex(::OneTo, i)
, but it doesn't get eliminated if we check getindex(::CartesianIndices, I...)
getindex_inbounds(r, i...) = @inbounds getindex(r, i...)
R = CartesianIndices((2, 3))
julia> @code_llvm getindex_inbounds(R.indices[1], 1)
; @ REPL[18]:1 within `getindex_inbounds`
define i64 @julia_getindex_inbounds_2636([1 x i64]* nocapture nonnull readonly align 8 dereferenceable(8) %0, i64 signext %1) #0 {
top:
ret i64 %1
}
julia> @code_llvm getindex_inbounds(R, 1, 1)
...
L12: ; preds = %top
; │││││└└└
; │││││┌ @ broadcast.jl:642 within `_broadcast_getindex`
; ││││││┌ @ broadcast.jl:666 within `_getindex`
; │││││││┌ @ broadcast.jl:621 within `_broadcast_getindex`
; ││││││││┌ @ tuple.jl:29 within `getindex`
%9 = getelementptr inbounds [1 x [2 x [1 x i64]]], [1 x [2 x [1 x i64]]]* %1, i64 0, i64 0, i64 0
; │││││└└└└
; │││││┌ @ broadcast.jl:643 within `_broadcast_getindex`
; ││││││┌ @ broadcast.jl:670 within `_broadcast_getindex_evalf`
; │││││││┌ @ range.jl:901 within `getindex`
%10 = call nonnull {}* @j_throw_boundserror_2682([1 x i64]* nocapture nonnull readonly %9, i64 signext %2) #0
call void @llvm.trap()
unreachable
...
L26: ; preds = %L18
%17 = call nonnull {}* @j_throw_boundserror_2681([1 x i64]* nocapture readonly %11, i64 signext %3) #0
call void @llvm.trap()
unreachable
...
Notice the two throw_boundserror
call. Thus I did an eager check so that we can pass @inbounds
hint through the broadcasting/map. After 20c4be5:
julia> @code_llvm getindex_inbounds(R, 1, 1)
; @ REPL[18]:1 within `getindex_inbounds`
define void @julia_getindex_inbounds_2685([1 x [2 x i64]]* noalias nocapture sret([1 x [2 x i64]]) %0, [1 x [2 x [1 x i64]]]* nocapture nonnull readonly align 8 dereferenceable(16) %1, i64 signext %2, i64 signext %3) #0 {
top:
%.sroa.0.sroa.0.0..sroa.0.0..sroa_cast4.sroa_idx = getelementptr inbounds [1 x [2 x i64]], [1 x [2 x i64]]* %0, i64 0, i64 0, i64 0
store i64 %2, i64* %.sroa.0.sroa.0.0..sroa.0.0..sroa_cast4.sroa_idx, align 8
%.sroa.0.sroa.2.0..sroa.0.0..sroa_cast4.sroa_idx8 = getelementptr inbounds [1 x [2 x i64]], [1 x [2 x i64]]* %0, i64 0, i64 0, i64 1
store i64 %3, i64* %.sroa.0.sroa.2.0..sroa.0.0..sroa_cast4.sroa_idx8, align 8
ret void
}
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.
Thanks for doing this!
f737824
to
defc214
Compare
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.
LGTM. Can you add a test in test/boundscheck_exec.jl
?
Codecov Report
@@ Coverage Diff @@
## master #42119 +/- ##
==========================================
+ Coverage 79.62% 79.66% +0.03%
==========================================
Files 351 351
Lines 77565 77567 +2
==========================================
+ Hits 61759 61790 +31
+ Misses 15806 15777 -29
Continue to review full report at Codecov.
|
base/multidimensional.jl
Outdated
@inline function Base.getindex(iter::CartesianIndices{N,R}, | ||
I::Vararg{Union{OrdinalRange{<:Integer, <:Integer}, Colon}, N}) where {N,R} | ||
CartesianIndices(getindex.(iter.indices, I)) | ||
@boundscheck checkbounds(iter, I...) | ||
indices = map(iter.indices, I) do r, i | ||
@inbounds getindex(r, i) | ||
end | ||
CartesianIndices(indices) | ||
end | ||
@propagate_inbounds function Base.getindex(iter::CartesianIndices{N}, | ||
C::CartesianIndices{N}) where {N} | ||
CartesianIndices(getindex.(iter.indices, C.indices)) | ||
getindex(iter, C.indices...) | ||
end | ||
@inline Base.getindex(iter::CartesianIndices{0}, ::CartesianIndices{0}) = iter |
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.
If we want to backport to 1.6 or 1.7, then these changes should be separated to another PR.
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.
What's the justification here?
julia> ()[CartesianIndex()]
ERROR: MethodError: no method matching getindex(::Tuple{}, ::CartesianIndex{0})
Closest candidates are:
getindex(::Tuple, ::Int64) at tuple.jl:29
getindex(::Tuple, ::Real) at tuple.jl:30
getindex(::Tuple, ::Colon) at tuple.jl:33
...
Stacktrace:
[1] top-level scope
@ REPL[12]:1
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 extra line is for the @test c[c] === c
test case. Without this line getindex(iter, C.indices...)
will call the getindex(::CartesianIndices{0})
method, which returns CartesianIndex()
instead of [CartesianIndex()]
Lines 150 to 156 in 8812c5c
@testset "0D array" begin | |
a = zeros() | |
c = CartesianIndices(a) | |
@test a[c] == a | |
@test c[c] === c | |
@test c[] == CartesianIndex() | |
end |
Edit: I've removed these methods in this PR because I want to backport this PR to 1.6 release. I'll submit a new PR for these after this is merged.
This enables us to backport to 1.6 and 1.7 branch
814f3ff
to
d58ce6a
Compare
The Edit: I added the backport labels because I think this worths living in our LTS release. If there are concerns please feel free to remove these labels. |
Thanks! |
Aha, this makes sense! Thank you! |
(cherry picked from commit 15b9851)
(cherry picked from commit 15b9851)
(cherry picked from commit 15b9851)
(cherry picked from commit 15b9851)
This PR eagerly does boundscheck when doing
getindex(::CartesianIndices, ...)
, this generates simpler inlined codes and furthermore enables SIMDThe earliest version that I obverse this performance regression is in Julia 1.4.0
A related benchmark:
fixes #42115