From ed011983507ada69f8d235a9a21dba62709d9b26 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Fri, 21 Jul 2023 17:18:50 +0900 Subject: [PATCH] inference: continue const-prop' when concrete-eval returns non-inlineable This commit fixes the regression in the following example: ```julia julia> stritr() = iterate(("1", '2'), 1); julia> @time stritr(); 0.000001 seconds (2 allocations: 64 bytes) # on master 0.000000 seconds # on 1.9 ``` The problem is that currently we don't inline result of concrete-eval when its result is not inlineable, although const-prop' or semi-concrete eval may be able to optimize and inline the method body. To improve the situation, this commit simply allows `abstract_call_method_with_const_args` to continue to const-prop' when concrete-eval returned non-inlineable result. --- base/compiler/abstractinterpretation.jl | 57 ++++++++++++++++++------- test/compiler/inference.jl | 11 +++++ test/compiler/inline.jl | 14 ++++++ 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 6e552423cc25ee..7543167f43825f 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -767,11 +767,21 @@ struct ConstCallResults const_result::ConstResult effects::Effects edge::MethodInstance - ConstCallResults(@nospecialize(rt), - const_result::ConstResult, - effects::Effects, - edge::MethodInstance) = - new(rt, const_result, effects, edge) + function ConstCallResults( + @nospecialize(rt), + const_result::ConstResult, + effects::Effects, + edge::MethodInstance) + return new(rt, const_result, effects, edge) + end + function ConstCallResults( + result::ConstCallResults, + @nospecialize(rt = result.rt); + const_result::ConstResult = result.const_result, + effects::Effects = result.effects, + edge::MethodInstance = result.edge) + return new(rt, const_result, effects, edge) + end end function abstract_call_method_with_const_args(interp::AbstractInterpreter, @@ -785,24 +795,33 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, return nothing end eligibility = concrete_eval_eligible(interp, f, result, arginfo, sv) + concrete_eval_result = nothing if eligibility === :concrete_eval - return concrete_eval_call(interp, f, result, arginfo, sv; invokecall) + concrete_eval_result = concrete_eval_call(interp, f, result, arginfo, sv, invokecall) + # if we don't inline the result of this concrete evaluation, + # give const-prop' a chance to inline a better method body + if !may_optimize(interp) || ( + may_inline_concrete_result(concrete_eval_result.const_result::ConcreteResult) || + concrete_eval_result.rt === Bottom) # unless this call deterministically throws and thus is non-inlineable + return concrete_eval_result + end + # TODO allow semi-concrete interp for this call? end mi = maybe_get_const_prop_profitable(interp, result, f, arginfo, si, match, sv) - mi === nothing && return nothing + mi === nothing && return concrete_eval_result if is_constprop_recursed(result, mi, sv) add_remark!(interp, sv, "[constprop] Edge cycle encountered") return nothing end # try semi-concrete evaluation if eligibility === :semi_concrete_eval - res = semi_concrete_eval_call(interp, mi, result, arginfo, sv) - if res !== nothing - return res + irinterp_result = semi_concrete_eval_call(interp, mi, result, arginfo, sv) + if irinterp_result !== nothing + return irinterp_result end end # try constant prop' - return const_prop_call(interp, mi, result, arginfo, sv) + return const_prop_call(interp, mi, result, arginfo, sv, concrete_eval_result) end function const_prop_enabled(interp::AbstractInterpreter, sv::AbsIntState, match::MethodMatch) @@ -881,7 +900,7 @@ function collect_const_args(argtypes::Vector{Any}, start::Int) end function concrete_eval_call(interp::AbstractInterpreter, - @nospecialize(f), result::MethodCallResult, arginfo::ArgInfo, sv::AbsIntState; + @nospecialize(f), result::MethodCallResult, arginfo::ArgInfo, sv::AbsIntState, invokecall::Union{InvokeCall,Nothing}=nothing) args = collect_const_args(arginfo, #=start=#2) if invokecall !== nothing @@ -895,7 +914,7 @@ function concrete_eval_call(interp::AbstractInterpreter, Core._call_in_world_total(world, f, args...) catch # The evaluation threw. By :consistent-cy, we're guaranteed this would have happened at runtime - return ConstCallResults(Union{}, ConcreteResult(edge, result.effects), result.effects, edge) + return ConstCallResults(Bottom, ConcreteResult(edge, result.effects), result.effects, edge) end return ConstCallResults(Const(value), ConcreteResult(edge, EFFECTS_TOTAL, value), EFFECTS_TOTAL, edge) end @@ -1153,8 +1172,8 @@ function semi_concrete_eval_call(interp::AbstractInterpreter, # that are newly resovled by irinterp # state = InliningState(interp) # ir = ssa_inlining_pass!(irsv.ir, state, propagate_inbounds(irsv)) - new_effects = Effects(result.effects; nothrow) - return ConstCallResults(rt, SemiConcreteResult(mi, ir, new_effects), new_effects, mi) + effects = Effects(result.effects; nothrow) + return ConstCallResults(rt, SemiConcreteResult(mi, ir, effects), effects, mi) end end end @@ -1162,7 +1181,8 @@ function semi_concrete_eval_call(interp::AbstractInterpreter, end function const_prop_call(interp::AbstractInterpreter, - mi::MethodInstance, result::MethodCallResult, arginfo::ArgInfo, sv::AbsIntState) + mi::MethodInstance, result::MethodCallResult, arginfo::ArgInfo, sv::AbsIntState, + concrete_eval_result::Union{Nothing,ConstCallResults}=nothing) inf_cache = get_inference_cache(interp) 𝕃ᵢ = typeinf_lattice(interp) inf_result = cache_lookup(𝕃ᵢ, mi, arginfo.argtypes, inf_cache) @@ -1185,6 +1205,11 @@ function const_prop_call(interp::AbstractInterpreter, return nothing end @assert inf_result.result !== nothing + if concrete_eval_result !== nothing + # override return type and effects with concrete evaluation result if available + inf_result.result = concrete_eval_result.rt + inf_result.ipo_effects = concrete_eval_result.effects + end else # found the cache for this constant prop' if inf_result.result === nothing diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index 4f5cdea59da447..f1b1e95b9622de 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -5099,3 +5099,14 @@ end refine_partial_struct2(42, s) end |> only === String # JET.test_call(s::AbstractString->Base._string(s, 'c')) + +Base.@assume_effects :foldable function continue_const_prop(i, j) + chars = map(Char, i:j) + String(chars) +end +@test Base.return_types() do + length(continue_const_prop(1, 5)) == 5 ? :ok : nothing +end |> only === Symbol +@test fully_eliminated() do + length(continue_const_prop(1, 5)) +end diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 3bc7ab5ccbc828..8be3e0241224f7 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -2065,3 +2065,17 @@ end # https://github.com/JuliaLang/julia/issues/50612 f50612(x) = UInt32(x) @test all(!isinvoke(:UInt32),get_code(f50612,Tuple{Char})) + +# continue const-prop' when concrete-eval result is too big +const THE_BIG_TUPLE_2 = ntuple(identity, 1024) +return_the_big_tuple2(a) = (a, THE_BIG_TUPLE_2) +let src = code_typed1() do + return return_the_big_tuple2(42)[2] + end + @test count(isinvoke(:return_the_big_tuple2), src.code) == 0 +end +let src = code_typed1() do + return iterate(("1", '2'), 1) + end + @test count(isinvoke(:iterate), src.code) == 0 +end