Skip to content

Commit

Permalink
Remove @inbounds in tuple iteration (#48297)
Browse files Browse the repository at this point in the history
* Remove @inbounds in tuple iteration

LLVM can prove this inbounds and the annotation weakens the
inferable effects for tuple iteration, which has a surprisingly
large inference performance and precision impact.

Unfortunately, my previous changes to :inbounds tainting weren't
quite strong enough yet, because `getfield` was still tainting
consistency on unknown boundscheck arguments. To fix that, we
pass through the fargs into the fetfield effects to check if
we're getting a literal `:boundscheck`, in which case the
`:noinbounds` consistency-tainting logic I added in #48246
is sufficient to not require additional consistency tainting.

Also add a test for both effects and codegen to make sure this doens't regress.

* Int64 -> Int

* fixup typo
  • Loading branch information
Keno authored Jan 17, 2023
1 parent 36007b7 commit 9582937
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 58 deletions.
5 changes: 3 additions & 2 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1975,7 +1975,7 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
return abstract_finalizer(interp, argtypes, sv)
end
rt = abstract_call_builtin(interp, f, arginfo, sv, max_methods)
effects = builtin_effects(𝕃ᵢ, f, argtypes[2:end], rt)
effects = builtin_effects(𝕃ᵢ, f, arginfo, rt)
return CallMeta(rt, effects, NoCallInfo())
elseif isa(f, Core.OpaqueClosure)
# calling an OpaqueClosure about which we have no information returns no information
Expand All @@ -1994,7 +1994,8 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f),
ub_var = argtypes[3]
end
pT = typevar_tfunc(𝕃ᵢ, n, lb_var, ub_var)
effects = builtin_effects(𝕃ᵢ, Core._typevar, Any[n, lb_var, ub_var], pT)
effects = builtin_effects(𝕃ᵢ, Core._typevar, ArgInfo(nothing,
Any[Const(Core._typevar), n, lb_var, ub_var]), pT)
return CallMeta(pT, effects, NoCallInfo())
elseif f === UnionAll
return abstract_call_unionall(interp, argtypes)
Expand Down
4 changes: 2 additions & 2 deletions base/compiler/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,8 @@ function stmt_effect_flags(𝕃ₒ::AbstractLattice, @nospecialize(stmt), @nospe
isa(f, Builtin) || return (false, false, false)
# Needs to be handled in inlining to look at the callee effects
f === Core._apply_iterate && return (false, false, false)
argtypes = Any[argextype(args[arg], src) for arg in 2:length(args)]
effects = builtin_effects(𝕃ₒ, f, argtypes, rt)
argtypes = Any[argextype(args[arg], src) for arg in 1:length(args)]
effects = builtin_effects(𝕃ₒ, f, ArgInfo(args, argtypes), rt)
consistent = is_consistent(effects)
effect_free = is_effect_free(effects)
nothrow = is_nothrow(effects)
Expand Down
110 changes: 64 additions & 46 deletions base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -911,41 +911,47 @@ function try_compute_fieldidx(typ::DataType, @nospecialize(field))
return field
end

function getfield_boundscheck(argtypes::Vector{Any}) # ::Union{Bool, Nothing}
if length(argtypes) == 2
return true
elseif length(argtypes) == 3
boundscheck = argtypes[3]
isvarargtype(boundscheck) && return nothing
if widenconst(boundscheck) === Symbol
return true
end
function getfield_boundscheck((; fargs, argtypes)::ArgInfo) # Symbol
farg = nothing
if length(argtypes) == 3
return :on
elseif length(argtypes) == 4
fargs !== nothing && (farg = fargs[4])
boundscheck = argtypes[4]
isvarargtype(boundscheck) && return :unknown
if widenconst(boundscheck) === Symbol
return :on
end
elseif length(argtypes) == 5
fargs !== nothing && (farg = fargs[5])
boundscheck = argtypes[5]
else
return nothing
return :unknown
end
isvarargtype(boundscheck) && return nothing
widenconst(boundscheck) === Bool || return nothing
isvarargtype(boundscheck) && return :unknown
boundscheck = widenconditional(boundscheck)
if isa(boundscheck, Const)
return boundscheck.val::Bool
else
return nothing
if widenconst(boundscheck) === Bool
if isa(boundscheck, Const)
return boundscheck.val::Bool ? :on : :off
elseif farg !== nothing && isexpr(farg, :boundscheck)
return :boundscheck
end
end
return :unknown
end

function getfield_nothrow(argtypes::Vector{Any}, boundscheck::Union{Bool,Nothing}=getfield_boundscheck(argtypes))
boundscheck === nothing && return false
function getfield_nothrow(arginfo::ArgInfo, boundscheck::Symbol=getfield_boundscheck(arginfo))
(;argtypes) = arginfo
boundscheck === :unknown && return false
ordering = Const(:not_atomic)
if length(argtypes) == 3
isvarargtype(argtypes[3]) && return false
if widenconst(argtypes[3]) !== Bool
ordering = argtypes[3]
end
elseif length(argtypes) == 4
ordering = argtypes[4]
elseif length(argtypes) != 2
if length(argtypes) == 4
isvarargtype(argtypes[4]) && return false
if widenconst(argtypes[4]) !== Bool
ordering = argtypes[4]
end
elseif length(argtypes) == 5
ordering = argtypes[5]
elseif length(argtypes) != 3
return false
end
isvarargtype(ordering) && return false
Expand All @@ -955,7 +961,7 @@ function getfield_nothrow(argtypes::Vector{Any}, boundscheck::Union{Bool,Nothing
if ordering !== :not_atomic # TODO: this is assuming not atomic
return false
end
return getfield_nothrow(argtypes[1], argtypes[2], !(boundscheck === false))
return getfield_nothrow(argtypes[2], argtypes[3], !(boundscheck === :off))
else
return false
end
Expand Down Expand Up @@ -1037,7 +1043,9 @@ end
end
return getfield_tfunc(𝕃, s00, name)
end
@nospecs getfield_tfunc(𝕃::AbstractLattice, s00, name) = _getfield_tfunc(𝕃, s00, name, false)
@nospecs function getfield_tfunc(𝕃::AbstractLattice, s00, name)
_getfield_tfunc(𝕃, s00, name, false)
end

function _getfield_fieldindex(s::DataType, name::Const)
nv = name.val
Expand Down Expand Up @@ -2021,7 +2029,7 @@ end
elseif f === invoke
return false
elseif f === getfield
return getfield_nothrow(argtypes)
return getfield_nothrow(ArgInfo(nothing, Any[Const(f), argtypes...]))
elseif f === setfield!
if na == 3
return setfield!_nothrow(𝕃, argtypes[1], argtypes[2], argtypes[3])
Expand Down Expand Up @@ -2179,10 +2187,11 @@ function isdefined_effects(𝕃::AbstractLattice, argtypes::Vector{Any})
return Effects(EFFECTS_TOTAL; consistent, nothrow)
end

function getfield_effects(argtypes::Vector{Any}, @nospecialize(rt))
function getfield_effects(arginfo::ArgInfo, @nospecialize(rt))
(;argtypes) = arginfo
# consistent if the argtype is immutable
isempty(argtypes) && return EFFECTS_THROWS
obj = argtypes[1]
length(argtypes) < 3 && return EFFECTS_THROWS
obj = argtypes[2]
isvarargtype(obj) && return Effects(EFFECTS_THROWS; consistent=ALWAYS_FALSE)
consistent = (is_immutable_argtype(obj) || is_mutation_free_argtype(obj)) ?
ALWAYS_TRUE : CONSISTENT_IF_INACCESSIBLEMEMONLY
Expand All @@ -2191,20 +2200,26 @@ function getfield_effects(argtypes::Vector{Any}, @nospecialize(rt))
# throws `UndefRefError` so doesn't need to taint it
# NOTE `getfield_notundefined` conservatively checks if this field is never initialized
# with undefined value so that we don't taint `:consistent`-cy too aggressively here
if !(length(argtypes) 2 && getfield_notundefined(obj, argtypes[2]))
if !(length(argtypes) 3 && getfield_notundefined(obj, argtypes[3]))
consistent = ALWAYS_FALSE
end
nothrow = getfield_nothrow(argtypes, true)
if !nothrow && getfield_boundscheck(argtypes) !== true
# If we cannot independently prove inboundsness, taint consistency.
# The inbounds-ness assertion requires dynamic reachability, while
# :consistent needs to be true for all input values.
# N.B. We do not taint for `--check-bounds=no` here that happens in
# InferenceState.
consistent = ALWAYS_FALSE
nothrow = getfield_nothrow(arginfo, :on)
if !nothrow
bcheck = getfield_boundscheck(arginfo)
if !(bcheck === :on || bcheck === :boundscheck)
# If we cannot independently prove inboundsness, taint consistency.
# The inbounds-ness assertion requires dynamic reachability, while
# :consistent needs to be true for all input values.
# However, as a special exception, we do allow literal `:boundscheck`.
# `:consistent`-cy will be tainted in any caller using `@inbounds` based
# on the `:noinbounds` effect.
# N.B. We do not taint for `--check-bounds=no` here. That is handled
# in concrete evaluation.
consistent = ALWAYS_FALSE
end
end
if hasintersect(widenconst(obj), Module)
inaccessiblememonly = getglobal_effects(argtypes, rt).inaccessiblememonly
inaccessiblememonly = getglobal_effects(argtypes[2:end], rt).inaccessiblememonly
elseif is_mutation_free_argtype(obj)
inaccessiblememonly = ALWAYS_TRUE
else
Expand Down Expand Up @@ -2233,17 +2248,20 @@ function getglobal_effects(argtypes::Vector{Any}, @nospecialize(rt))
return Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly)
end

function builtin_effects(𝕃::AbstractLattice, @nospecialize(f::Builtin), argtypes::Vector{Any}, @nospecialize(rt))
function builtin_effects(𝕃::AbstractLattice, @nospecialize(f::Builtin), arginfo::ArgInfo, @nospecialize(rt))
if isa(f, IntrinsicFunction)
return intrinsic_effects(f, argtypes)
return intrinsic_effects(f, arginfo.argtypes[2:end])
end

@assert !contains_is(_SPECIAL_BUILTINS, f)

if f === getfield
return getfield_effects(arginfo, rt)
end
argtypes = arginfo.argtypes[2:end]

if f === isdefined
return isdefined_effects(𝕃, argtypes)
elseif f === getfield
return getfield_effects(argtypes, rt)
elseif f === getglobal
return getglobal_effects(argtypes, rt)
elseif f === Core.get_binding_type
Expand Down
7 changes: 4 additions & 3 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1493,9 +1493,10 @@ function infer_effects(@nospecialize(f), @nospecialize(types=default_tt(f));
ccall(:jl_is_in_pure_context, Bool, ()) && error("code reflection cannot be used from generated functions")
if isa(f, Core.Builtin)
types = to_tuple_type(types)
argtypes = Any[types.parameters...]
rt = Core.Compiler.builtin_tfunction(interp, f, argtypes, nothing)
return Core.Compiler.builtin_effects(Core.Compiler.typeinf_lattice(interp), f, argtypes, rt)
argtypes = Any[Core.Compiler.Const(f), types.parameters...]
rt = Core.Compiler.builtin_tfunction(interp, f, argtypes[2:end], nothing)
return Core.Compiler.builtin_effects(Core.Compiler.typeinf_lattice(interp), f,
Core.Compiler.ArgInfo(nothing, argtypes), rt)
end
tt = signature_type(f, types)
result = Core.Compiler.findall(tt, Core.Compiler.method_table(interp))
Expand Down
2 changes: 1 addition & 1 deletion base/tuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ end

function iterate(@nospecialize(t::Tuple), i::Int=1)
@inline
return (1 <= i <= length(t)) ? (@inbounds t[i], i + 1) : nothing
return (1 <= i <= length(t)) ? (t[i], i + 1) : nothing
end

keys(@nospecialize t::Tuple) = OneTo(length(t))
Expand Down
3 changes: 3 additions & 0 deletions test/compiler/codegen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -794,3 +794,6 @@ end
f48085(@nospecialize x...) = length(x)
@test Core.Compiler.get_compileable_sig(which(f48085, (Vararg{Any},)), Tuple{typeof(f48085), Vararg{Int}}, Core.svec()) === nothing
@test Core.Compiler.get_compileable_sig(which(f48085, (Vararg{Any},)), Tuple{typeof(f48085), Int, Vararg{Int}}, Core.svec()) === Tuple{typeof(f48085), Any, Vararg{Any}}

# Make sure that the bounds check is elided in tuple iteration
@test !occursin("call void @", get_llvm(iterate, Tuple{NTuple{4, Float64}, Int}))
9 changes: 5 additions & 4 deletions test/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -462,9 +462,9 @@ end |> Core.Compiler.is_consistent
end |> Core.Compiler.is_effect_free

# `getfield_effects` handles access to union object nicely
@test Core.Compiler.is_consistent(Core.Compiler.getfield_effects(Any[Some{String}, Core.Const(:value)], String))
@test Core.Compiler.is_consistent(Core.Compiler.getfield_effects(Any[Some{Symbol}, Core.Const(:value)], Symbol))
@test Core.Compiler.is_consistent(Core.Compiler.getfield_effects(Any[Union{Some{Symbol},Some{String}}, Core.Const(:value)], Union{Symbol,String}))
@test Core.Compiler.is_consistent(Core.Compiler.getfield_effects(Core.Compiler.ArgInfo(nothing, Any[Core.Const(getfield), Some{String}, Core.Const(:value)]), String))
@test Core.Compiler.is_consistent(Core.Compiler.getfield_effects(Core.Compiler.ArgInfo(nothing, Any[Core.Const(getfield), Some{Symbol}, Core.Const(:value)]), Symbol))
@test Core.Compiler.is_consistent(Core.Compiler.getfield_effects(Core.Compiler.ArgInfo(nothing, Any[Core.Const(getfield), Union{Some{Symbol},Some{String}}, Core.Const(:value)]), Union{Symbol,String}))
@test Base.infer_effects((Bool,)) do c
obj = c ? Some{String}("foo") : Some{Symbol}(:bar)
return getfield(obj, :value)
Expand Down Expand Up @@ -688,7 +688,8 @@ end # @testset "effects analysis on array construction" begin
end # @testset "effects analysis on array ops" begin

# Test that builtin_effects handles vararg correctly
@test !Core.Compiler.is_nothrow(Core.Compiler.builtin_effects(Core.Compiler.fallback_lattice, Core.isdefined, Any[String, Vararg{Any}], Bool))
@test !Core.Compiler.is_nothrow(Core.Compiler.builtin_effects(Core.Compiler.fallback_lattice, Core.isdefined,
Core.Compiler.ArgInfo(nothing, Any[Core.Compiler.Const(Core.isdefined), String, Vararg{Any}]), Bool))

# Test that :new can be eliminated even if an sparam is unknown
struct SparamUnused{T}
Expand Down
3 changes: 3 additions & 0 deletions test/tuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -779,3 +779,6 @@ namedtup = (;a=1, b=2, c=3)
NamedTuple{(:a, :b), Tuple{Int, Int}},
NamedTuple{(:c,), Tuple{Int}},
}

# Make sure that tuple iteration is foldable
@test Core.Compiler.is_foldable(Base.infer_effects(iterate, Tuple{NTuple{4, Float64}, Int}))

0 comments on commit 9582937

Please sign in to comment.