Skip to content

Commit

Permalink
Remove @inbounds in tuple iteration
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Keno committed Jan 16, 2023
1 parent 1bfdf98 commit 79d6e0d
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 55 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
109 changes: 65 additions & 44 deletions base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -911,41 +911,50 @@ 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 :unknown
if widenconst(boundscheck) === Bool
if farg !== nothing && isexpr(farg, :boundscheck)
return :boundscheck
end
return :unknown
end
isvarargtype(boundscheck) && return nothing
widenconst(boundscheck) === Bool || return nothing
boundscheck = widenconditional(boundscheck)
if isa(boundscheck, Const)
return boundscheck.val::Bool
return boundscheck.val::Bool ? :on : :off
else
return nothing
return :unknown
end
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 +964,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 +1046,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 +2032,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 +2190,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 +2203,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`.
# `:consitency`-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 +2251,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
5 changes: 3 additions & 2 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1488,9 +1488,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...]
argtypes = Any[Core.Compiler.Const(f), types.parameters...]
rt = Core.Compiler.builtin_tfunction(interp, f, argtypes, nothing)
return Core.Compiler.builtin_effects(Core.Compiler.typeinf_lattice(interp), f, argtypes, rt)
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}, Int64}))
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}, Int64}))

0 comments on commit 79d6e0d

Please sign in to comment.