diff --git a/NEWS.md b/NEWS.md index f9fb0835afffc9..f7f25a5b6a54b5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,7 +6,10 @@ New language features --------------------- * `Module(:name, false, false)` can be used to create a `module` that does not import `Core`. ([#40110]) -* `@inline` and `@noinline` annotations may now be used in function bodies. ([#41312]) +* `@inline` and `@noinline` annotations can be used within a function body to give an extra + hint about the inlining cost to the compiler. ([#41312]) +* `@inline` and `@noinline` annotations can now be applied to a function callsite or block + to enforce the involved function calls to be (or not to be) inlined. ([#41312]) * The default behavior of observing `@inbounds` declarations is now an option via `auto` in `--check-bounds=yes|no|auto` ([#41551]) Language changes @@ -39,7 +42,7 @@ New library features * `@test_throws "some message" triggers_error()` can now be used to check whether the displayed error text contains "some message" regardless of the specific exception type. - Regular expressions, lists of strings, and matching functions are also supported. ([#41888) + Regular expressions, lists of strings, and matching functions are also supported. ([#41888]) Standard library changes ------------------------ diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 4c3f7d46fd98b7..ede05572edc3a8 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -592,7 +592,7 @@ function maybe_get_const_prop_profitable(interp::AbstractInterpreter, result::Me return nothing end mi = mi::MethodInstance - if !force && !const_prop_methodinstance_heuristic(interp, method, mi) + if !force && !const_prop_methodinstance_heuristic(interp, match, mi, sv) add_remark!(interp, sv, "[constprop] Disabled by method instance heuristic") return nothing end @@ -696,7 +696,8 @@ end # This is a heuristic to avoid trying to const prop through complicated functions # where we would spend a lot of time, but are probably unlikely to get an improved # result anyway. -function const_prop_methodinstance_heuristic(interp::AbstractInterpreter, method::Method, mi::MethodInstance) +function const_prop_methodinstance_heuristic(interp::AbstractInterpreter, match::MethodMatch, mi::MethodInstance, sv::InferenceState) + method = match.method if method.is_for_opaque_closure # Not inlining an opaque closure can be very expensive, so be generous # with the const-prop-ability. It is quite possible that we can't infer @@ -714,7 +715,8 @@ function const_prop_methodinstance_heuristic(interp::AbstractInterpreter, method if isdefined(code, :inferred) && !cache_inlineable cache_inf = code.inferred if !(cache_inf === nothing) - cache_inlineable = inlining_policy(interp)(cache_inf) !== nothing + src = inlining_policy(interp, cache_inf, get_curr_ssaflag(sv)) + cache_inlineable = src !== nothing end end if !cache_inlineable @@ -1908,7 +1910,9 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) if isa(fname, SlotNumber) changes = StateUpdate(fname, VarState(Any, false), changes, false) end - elseif hd === :inbounds || hd === :meta || hd === :loopinfo || hd === :code_coverage_effect + elseif hd === :code_coverage_effect || + (hd !== :boundscheck && # :boundscheck can be narrowed to Bool + hd !== nothing && is_meta_expr_head(hd)) # these do not generate code else t = abstract_eval_statement(interp, stmt, changes, frame) diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index d76a4ebd18f6e0..216c397af31e44 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -296,3 +296,5 @@ function print_callstack(sv::InferenceState) sv = sv.parent end end + +get_curr_ssaflag(sv::InferenceState) = sv.src.ssaflags[sv.currpc] diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 1898aa8b757782..09c778d57d695f 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -21,23 +21,28 @@ function push!(et::EdgeTracker, ci::CodeInstance) push!(et, ci.def) end -struct InliningState{S <: Union{EdgeTracker, Nothing}, T, P} +struct InliningState{S <: Union{EdgeTracker, Nothing}, T, I<:AbstractInterpreter} params::OptimizationParams et::S mi_cache::T - policy::P + interp::I end -function default_inlining_policy(@nospecialize(src)) +function inlining_policy(interp::AbstractInterpreter, @nospecialize(src), stmt_flag::UInt8) if isa(src, CodeInfo) || isa(src, Vector{UInt8}) src_inferred = ccall(:jl_ir_flag_inferred, Bool, (Any,), src) - src_inlineable = ccall(:jl_ir_flag_inlineable, Bool, (Any,), src) + src_inlineable = is_stmt_inline(stmt_flag) || ccall(:jl_ir_flag_inlineable, Bool, (Any,), src) return src_inferred && src_inlineable ? src : nothing + elseif isa(src, OptimizationState) && isdefined(src, :ir) + return (is_stmt_inline(stmt_flag) || src.src.inlineable) ? src.ir : nothing + else + # maybe we want to make inference keep the source in a local cache if a statement is going to inlined + # and re-optimize it here with disabling further inlining to avoid infinite optimization loop + # (we can even naively try to re-infer it entirely) + # but it seems like that "single-level-inlining" is more trouble and complex than it's worth + # see https://github.com/JuliaLang/julia/pull/41328/commits/0fc0f71a42b8c9d04b0dafabf3f1f17703abf2e7 + return nothing end - if isa(src, OptimizationState) && isdefined(src, :ir) - return src.src.inlineable ? src.ir : nothing - end - return nothing end include("compiler/ssair/driver.jl") @@ -57,7 +62,7 @@ mutable struct OptimizationState inlining = InliningState(params, EdgeTracker(s_edges, frame.valid_worlds), WorldView(code_cache(interp), frame.world), - inlining_policy(interp)) + interp) return new(frame.linfo, frame.src, nothing, frame.stmt_info, frame.mod, frame.sptypes, frame.slottypes, false, @@ -86,7 +91,7 @@ mutable struct OptimizationState inlining = InliningState(params, nothing, WorldView(code_cache(interp), get_world_counter()), - inlining_policy(interp)) + interp) return new(linfo, src, nothing, stmt_info, mod, sptypes_from_meth_instance(linfo), slottypes, false, @@ -125,9 +130,15 @@ const SLOT_ASSIGNEDONCE = 16 # slot is assigned to only once const SLOT_USEDUNDEF = 32 # slot has uses that might raise UndefVarError # const SLOT_CALLED = 64 -# This statement was marked as @inbounds by the user. If replaced by inlining, -# any contained boundschecks may be removed -const IR_FLAG_INBOUNDS = 0x01 +# NOTE make sure to sync the flag definitions below with julia.h and `jl_code_info_set_ir` in method.c + +# This statement is marked as @inbounds by user. +# Ff replaced by inlining, any contained boundschecks may be removed. +const IR_FLAG_INBOUNDS = 0x01 << 0 +# This statement is marked as @inline by user +const IR_FLAG_INLINE = 0x01 << 1 +# This statement is marked as @noinline by user +const IR_FLAG_NOINLINE = 0x01 << 2 # This statement may be removed if its result is unused. In particular it must # thus be both pure and effect free. const IR_FLAG_EFFECT_FREE = 0x01 << 4 @@ -173,6 +184,9 @@ function isinlineable(m::Method, me::OptimizationState, params::OptimizationPara return inlineable end +is_stmt_inline(stmt_flag::UInt8) = stmt_flag & IR_FLAG_INLINE != 0 +is_stmt_noinline(stmt_flag::UInt8) = stmt_flag & IR_FLAG_NOINLINE != 0 + # These affect control flow within the function (so may not be removed # if there is no usage within the function), but don't affect the purity # of the function as a whole. @@ -358,42 +372,22 @@ function convert_to_ircode(ci::CodeInfo, code::Vector{Any}, coverage::Bool, sv:: end renumber_ir_elements!(code, changemap, labelmap) - inbounds_depth = 0 # Number of stacked inbounds meta = Any[] - flags = fill(0x00, length(code)) for i = 1:length(code) - stmt = code[i] - if isexpr(stmt, :inbounds) - arg1 = stmt.args[1] - if arg1 === true # push - inbounds_depth += 1 - elseif arg1 === false # clear - inbounds_depth = 0 - elseif inbounds_depth > 0 # pop - inbounds_depth -= 1 - end - stmt = nothing - else - stmt = normalize(stmt, meta) - end - code[i] = stmt - if !(stmt === nothing) - if inbounds_depth > 0 - flags[i] |= IR_FLAG_INBOUNDS - end - end + code[i] = remove_meta!(code[i], meta) end - strip_trailing_junk!(ci, code, stmtinfo, flags) + strip_trailing_junk!(ci, code, stmtinfo) cfg = compute_basic_blocks(code) types = Any[] - stmts = InstructionStream(code, types, stmtinfo, ci.codelocs, flags) + stmts = InstructionStream(code, types, stmtinfo, ci.codelocs, ci.ssaflags) ir = IRCode(stmts, cfg, collect(LineInfoNode, ci.linetable::Union{Vector{LineInfoNode},Vector{Any}}), sv.slottypes, meta, sv.sptypes) return ir end -function normalize(@nospecialize(stmt), meta::Vector{Any}) +function remove_meta!(@nospecialize(stmt), meta::Vector{Any}) if isa(stmt, Expr) - if stmt.head === :meta + head = stmt.head + if head === :meta args = stmt.args if length(args) > 0 push!(meta, stmt) diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 7e7baff1367415..c76e74e50c6d08 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -604,6 +604,7 @@ function rewrite_apply_exprargs!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx:: argexprs::Vector{Any}, atypes::Vector{Any}, arginfos::Vector{Any}, arg_start::Int, istate::InliningState) + flag = ir.stmts[idx][:flag] new_argexprs = Any[argexprs[arg_start]] new_atypes = Any[atypes[arg_start]] # loop over original arguments and flatten any known iterators @@ -659,8 +660,8 @@ function rewrite_apply_exprargs!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx:: info = call.info handled = false if isa(info, ConstCallInfo) - if maybe_handle_const_call!(ir, state1.id, new_stmt, info, new_sig, - call.rt, istate, false, todo) + if !is_stmt_noinline(flag) && maybe_handle_const_call!( + ir, state1.id, new_stmt, info, new_sig,call.rt, istate, flag, false, todo) handled = true else info = info.call @@ -671,7 +672,7 @@ function rewrite_apply_exprargs!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx:: MethodMatchInfo[info] : info.matches # See if we can inline this call to `iterate` analyze_single_call!(ir, todo, state1.id, new_stmt, - new_sig, call.rt, info, istate) + new_sig, call.rt, info, istate, flag) end if i != length(thisarginfo.each) valT = getfield_tfunc(call.rt, Const(1)) @@ -719,7 +720,8 @@ function compileable_specialization(et::Union{EdgeTracker, Nothing}, (; linfo):: return mi end -function resolve_todo(todo::InliningTodo, state::InliningState) +function resolve_todo(todo::InliningTodo, state::InliningState, flag::UInt8) + mi = todo.mi (; match) = todo.spec::DelayedInliningSpec #XXX: update_valid_age!(min_valid[1], max_valid[1], sv) @@ -735,7 +737,7 @@ function resolve_todo(todo::InliningTodo, state::InliningState) isconst, src = false, inferred_src end else - linfo = get(state.mi_cache, todo.mi, nothing) + linfo = get(state.mi_cache, mi, nothing) if linfo isa CodeInstance if invoke_api(linfo) == 2 # in this case function can be inlined to a constant @@ -751,13 +753,11 @@ function resolve_todo(todo::InliningTodo, state::InliningState) et = state.et if isconst && et !== nothing - push!(et, todo.mi) + push!(et, mi) return ConstantCase(src) end - if src !== nothing - src = state.policy(src) - end + src = inlining_policy(state.interp, src, flag) if src === nothing return compileable_specialization(et, match) @@ -767,13 +767,13 @@ function resolve_todo(todo::InliningTodo, state::InliningState) src = copy(src) end - et !== nothing && push!(et, todo.mi) - return InliningTodo(todo.mi, src) + et !== nothing && push!(et, mi) + return InliningTodo(mi, src) end -function resolve_todo(todo::UnionSplit, state::InliningState) +function resolve_todo(todo::UnionSplit, state::InliningState, flag::UInt8) UnionSplit(todo.fully_covered, todo.atype, - Pair{Any,Any}[sig=>resolve_todo(item, state) for (sig, item) in todo.cases]) + Pair{Any,Any}[sig=>resolve_todo(item, state, flag) for (sig, item) in todo.cases]) end function validate_sparams(sparams::SimpleVector) @@ -784,7 +784,7 @@ function validate_sparams(sparams::SimpleVector) end function analyze_method!(match::MethodMatch, atypes::Vector{Any}, - state::InliningState, @nospecialize(stmttyp)) + state::InliningState, @nospecialize(stmttyp), flag::UInt8) method = match.method methsig = method.sig @@ -804,7 +804,7 @@ function analyze_method!(match::MethodMatch, atypes::Vector{Any}, et = state.et - if !state.params.inlining + if !state.params.inlining || is_stmt_noinline(flag) return compileable_specialization(et, match) end @@ -818,7 +818,7 @@ function analyze_method!(match::MethodMatch, atypes::Vector{Any}, # If we don't have caches here, delay resolving this MethodInstance # until the batch inlining step (or an external post-processing pass) state.mi_cache === nothing && return todo - return resolve_todo(todo, state) + return resolve_todo(todo, state, flag) end function InliningTodo(mi::MethodInstance, ir::IRCode) @@ -1043,7 +1043,7 @@ is_builtin(s::Signature) = s.ft ⊑ Builtin function inline_invoke!(ir::IRCode, idx::Int, sig::Signature, (; match, result)::InvokeCallInfo, - state::InliningState, todo::Vector{Pair{Int, Any}}) + state::InliningState, todo::Vector{Pair{Int, Any}}, flag::UInt8) stmt = ir.stmts[idx][:inst] calltype = ir.stmts[idx][:type] @@ -1057,17 +1057,17 @@ function inline_invoke!(ir::IRCode, idx::Int, sig::Signature, (; match, result): atypes = atypes[4:end] pushfirst!(atypes, atype0) - if isa(result, InferenceResult) + if isa(result, InferenceResult) && !is_stmt_noinline(flag) (; mi) = item = InliningTodo(result, atypes, calltype) validate_sparams(mi.sparam_vals) || return nothing if argtypes_to_type(atypes) <: mi.def.sig - state.mi_cache !== nothing && (item = resolve_todo(item, state)) + state.mi_cache !== nothing && (item = resolve_todo(item, state, flag)) handle_single_case!(ir, stmt, idx, item, true, todo) return nothing end end - result = analyze_method!(match, atypes, state, calltype) + result = analyze_method!(match, atypes, state, calltype, flag) handle_single_case!(ir, stmt, idx, result, true, todo) return nothing end @@ -1162,7 +1162,7 @@ end function analyze_single_call!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx::Int, @nospecialize(stmt), sig::Signature, @nospecialize(calltype), infos::Vector{MethodMatchInfo}, - state::InliningState) + state::InliningState, flag::UInt8) cases = Pair{Any, Any}[] signature_union = Union{} only_method = nothing # keep track of whether there is one matching method @@ -1196,7 +1196,7 @@ function analyze_single_call!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx::Int fully_covered = false continue end - case = analyze_method!(match, sig.atypes, state, calltype) + case = analyze_method!(match, sig.atypes, state, calltype, flag) if case === nothing fully_covered = false continue @@ -1223,7 +1223,7 @@ function analyze_single_call!(ir::IRCode, todo::Vector{Pair{Int, Any}}, idx::Int match = meth[1] end fully_covered = true - case = analyze_method!(match, sig.atypes, state, calltype) + case = analyze_method!(match, sig.atypes, state, calltype, flag) case === nothing && return push!(cases, Pair{Any,Any}(match.spec_types, case)) end @@ -1245,7 +1245,7 @@ end function maybe_handle_const_call!(ir::IRCode, idx::Int, stmt::Expr, info::ConstCallInfo, sig::Signature, @nospecialize(calltype), - state::InliningState, + state::InliningState, flag::UInt8, isinvoke::Bool, todo::Vector{Pair{Int, Any}}) # when multiple matches are found, bail out and later inliner will union-split this signature # TODO effectively use multiple constant analysis results here @@ -1257,7 +1257,7 @@ function maybe_handle_const_call!(ir::IRCode, idx::Int, stmt::Expr, validate_sparams(mi.sparam_vals) || return true mthd_sig = mi.def.sig mistypes = mi.specTypes - state.mi_cache !== nothing && (item = resolve_todo(item, state)) + state.mi_cache !== nothing && (item = resolve_todo(item, state, flag)) if sig.atype <: mthd_sig handle_single_case!(ir, stmt, idx, item, isinvoke, todo) return true @@ -1295,6 +1295,8 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState) info = info.info end + flag = ir.stmts[idx][:flag] + # Inference determined this couldn't be analyzed. Don't question it. if info === false continue @@ -1304,7 +1306,8 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState) # it'll have performed a specialized analysis for just this case. Use its # result. if isa(info, ConstCallInfo) - if maybe_handle_const_call!(ir, idx, stmt, info, sig, calltype, state, sig.f === Core.invoke, todo) + if !is_stmt_noinline(flag) && maybe_handle_const_call!( + ir, idx, stmt, info, sig, calltype, state, flag, sig.f === Core.invoke, todo) continue else info = info.call @@ -1312,7 +1315,7 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState) end if isa(info, OpaqueClosureCallInfo) - result = analyze_method!(info.match, sig.atypes, state, calltype) + result = analyze_method!(info.match, sig.atypes, state, calltype, flag) handle_single_case!(ir, stmt, idx, result, false, todo) continue end @@ -1320,7 +1323,7 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState) # Handle invoke if sig.f === Core.invoke if isa(info, InvokeCallInfo) - inline_invoke!(ir, idx, sig, info, state, todo) + inline_invoke!(ir, idx, sig, info, state, todo, flag) end continue end @@ -1334,7 +1337,7 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState) continue end - analyze_single_call!(ir, todo, idx, stmt, sig, calltype, infos, state) + analyze_single_call!(ir, todo, idx, stmt, sig, calltype, infos, state, flag) end todo end @@ -1399,7 +1402,8 @@ end function late_inline_special_case!(ir::IRCode, sig::Signature, idx::Int, stmt::Expr, params::OptimizationParams) f, ft, atypes = sig.f, sig.ft, sig.atypes typ = ir.stmts[idx][:type] - if params.inlining && length(atypes) == 3 && istopfunction(f, :!==) + isinlining = params.inlining + if isinlining && length(atypes) == 3 && istopfunction(f, :!==) # special-case inliner for !== that precedes _methods_by_ftype union splitting # and that works, even though inference generally avoids inferring the `!==` Method if isa(typ, Const) @@ -1411,7 +1415,7 @@ function late_inline_special_case!(ir::IRCode, sig::Signature, idx::Int, stmt::E not_call = Expr(:call, GlobalRef(Core.Intrinsics, :not_int), cmp_call_ssa) ir[SSAValue(idx)] = not_call return true - elseif params.inlining && length(atypes) == 3 && istopfunction(f, :(>:)) + elseif isinlining && length(atypes) == 3 && istopfunction(f, :(>:)) # special-case inliner for issupertype # that works, even though inference generally avoids inferring the `>:` Method if isa(typ, Const) && _builtin_nothrow(<:, Any[atypes[3], atypes[2]], typ) @@ -1421,7 +1425,7 @@ function late_inline_special_case!(ir::IRCode, sig::Signature, idx::Int, stmt::E subtype_call = Expr(:call, GlobalRef(Core, :(<:)), stmt.args[3], stmt.args[2]) ir[SSAValue(idx)] = subtype_call return true - elseif params.inlining && f === TypeVar && 2 <= length(atypes) <= 4 && (atypes[2] ⊑ Symbol) + elseif isinlining && f === TypeVar && 2 <= length(atypes) <= 4 && (atypes[2] ⊑ Symbol) ir[SSAValue(idx)] = Expr(:call, GlobalRef(Core, :_typevar), stmt.args[2], length(stmt.args) < 4 ? Bottom : stmt.args[3], length(stmt.args) == 2 ? Any : stmt.args[end]) diff --git a/base/compiler/ssair/slot2ssa.jl b/base/compiler/ssair/slot2ssa.jl index 777e7e2bb0af7f..2e3d1da1c168fa 100644 --- a/base/compiler/ssair/slot2ssa.jl +++ b/base/compiler/ssair/slot2ssa.jl @@ -183,7 +183,7 @@ function rename_uses!(ir::IRCode, ci::CodeInfo, idx::Int, @nospecialize(stmt), r return fixemup!(stmt->true, stmt->renames[slot_id(stmt)], ir, ci, idx, stmt) end -function strip_trailing_junk!(ci::CodeInfo, code::Vector{Any}, info::Vector{Any}, flags::Vector{UInt8}) +function strip_trailing_junk!(ci::CodeInfo, code::Vector{Any}, info::Vector{Any}) # Remove `nothing`s at the end, we don't handle them well # (we expect the last instruction to be a terminator) ssavaluetypes = ci.ssavaluetypes::Vector{Any} @@ -193,7 +193,7 @@ function strip_trailing_junk!(ci::CodeInfo, code::Vector{Any}, info::Vector{Any} resize!(ssavaluetypes, i) resize!(ci.codelocs, i) resize!(info, i) - resize!(flags, i) + resize!(ci.ssaflags, i) break end end @@ -205,7 +205,7 @@ function strip_trailing_junk!(ci::CodeInfo, code::Vector{Any}, info::Vector{Any} push!(ssavaluetypes, Union{}) push!(ci.codelocs, 0) push!(info, nothing) - push!(flags, 0x00) + push!(ci.ssaflags, 0x00) end nothing end diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index a9ab2d231c13c5..5d3325a2a5e3c6 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -343,7 +343,7 @@ function maybe_compress_codeinfo(interp::AbstractInterpreter, linfo::MethodInsta nslots = length(ci.slotflags) resize!(ci.slottypes::Vector{Any}, nslots) resize!(ci.slotnames, nslots) - return ccall(:jl_compress_ir, Any, (Any, Any), def, ci) + return ccall(:jl_compress_ir, Vector{UInt8}, (Any, Any), def, ci) else return ci end diff --git a/base/compiler/types.jl b/base/compiler/types.jl index 677450f288c707..5f8f6563124584 100644 --- a/base/compiler/types.jl +++ b/base/compiler/types.jl @@ -217,7 +217,6 @@ may_discard_trees(::AbstractInterpreter) = true verbose_stmt_info(::AbstractInterpreter) = false method_table(interp::AbstractInterpreter) = InternalMethodTable(get_world_counter(interp)) -inlining_policy(::AbstractInterpreter) = default_inlining_policy """ By default `AbstractInterpreter` implements the following inference bail out logic: diff --git a/base/compiler/utilities.jl b/base/compiler/utilities.jl index ed09d5316473a4..8dfe1f65f0d539 100644 --- a/base/compiler/utilities.jl +++ b/base/compiler/utilities.jl @@ -59,7 +59,7 @@ end # Meta expression head, these generally can't be deleted even when they are # in a dead branch but can be ignored when analyzing uses/liveness. -is_meta_expr_head(head::Symbol) = (head === :inbounds || head === :boundscheck || head === :meta || head === :loopinfo) +is_meta_expr_head(head::Symbol) = head === :boundscheck || head === :meta || head === :loopinfo sym_isless(a::Symbol, b::Symbol) = ccall(:strcmp, Int32, (Ptr{UInt8}, Ptr{UInt8}), a, b) < 0 @@ -196,7 +196,7 @@ function specialize_method(method::Method, @nospecialize(atypes), sparams::Simpl if preexisting # check cached specializations # for an existing result stored there - return ccall(:jl_specializations_lookup, Any, (Any, Any), method, atypes) + return ccall(:jl_specializations_lookup, Any, (Any, Any), method, atypes)::Union{Nothing,MethodInstance} end return ccall(:jl_specializations_get_linfo, Ref{MethodInstance}, (Any, Any, Any), method, atypes, sparams) end diff --git a/base/compiler/validation.jl b/base/compiler/validation.jl index c152dfb9fa6a54..6e05c96cd79369 100644 --- a/base/compiler/validation.jl +++ b/base/compiler/validation.jl @@ -16,6 +16,8 @@ const VALID_EXPR_HEADS = IdDict{Symbol,UnitRange{Int}}( :leave => 1:1, :pop_exception => 1:1, :inbounds => 1:1, + :inline => 1:1, + :noinline => 1:1, :boundscheck => 0:0, :copyast => 1:1, :meta => 0:typemax(Int), @@ -141,7 +143,7 @@ function validate_code!(errors::Vector{>:InvalidCodeError}, c::CodeInfo, is_top_ head === :const || head === :enter || head === :leave || head === :pop_exception || head === :method || head === :global || head === :static_parameter || head === :new || head === :splatnew || head === :thunk || head === :loopinfo || - head === :throw_undef_if_not || head === :code_coverage_effect + head === :throw_undef_if_not || head === :code_coverage_effect || head === :inline || head === :noinline validate_val!(x) else # TODO: nothing is actually in statement position anymore diff --git a/base/expr.jl b/base/expr.jl index f583a58f78dc5c..1af1e9486068ea 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -209,9 +209,53 @@ end !!! compat "Julia 1.8" The usage within a function body requires at least Julia 1.8. + +--- + @inline block + +Give a hint to the compiler that calls within `block` are worth inlining. + +```julia +# The compiler will try to inline `f` +@inline f(...) + +# The compiler will try to inline `f`, `g` and `+` +@inline f(...) + g(...) +``` + +!!! note + A callsite annotation always has the precedence over the annotation applied to the + definition of the called function: + ```julia + @noinline function explicit_noinline(args...) + # body + end + + let + @inline explicit_noinline(args...) # will be inlined + end + ``` + +!!! note + When there are nested callsite annotations, the innermost annotation has the precedence: + ```julia + @noinline let a0, b0 = ... + a = @inline f(a0) # the compiler will try to inline this call + b = f(b0) # the compiler will NOT try to inline this call + return a, b + end + ``` + +!!! warning + Although a callsite annotation will try to force inlining in regardless of the cost model, + there are still chances it can't succeed in it. Especially, recursive calls can not be + inlined even if they are annotated as `@inline`d. + +!!! compat "Julia 1.8" + The callsite annotation requires at least Julia 1.8. """ -macro inline(ex) - esc(isa(ex, Expr) ? pushmeta!(ex, :inline) : ex) +macro inline(x) + return annotate_meta_def_or_block(x, :inline) end """ @@ -244,11 +288,51 @@ end !!! compat "Julia 1.8" The usage within a function body requires at least Julia 1.8. +--- + @noinline block + +Give a hint to the compiler that it should not inline the calls within `block`. + +```julia +# The compiler will try to not inline `f` +@noinline f(...) + +# The compiler will try to not inline `f`, `g` and `+` +@noinline f(...) + g(...) +``` + +!!! note + A callsite annotation always has the precedence over the annotation applied to the + definition of the called function: + ```julia + @inline function explicit_inline(args...) + # body + end + + let + @noinline explicit_inline(args...) # will not be inlined + end + ``` + +!!! note + When there are nested callsite annotations, the innermost annotation has the precedence: + ```julia + @inline let a0, b0 = ... + a = @noinline f(a0) # the compiler will NOT try to inline this call + b = f(b0) # the compiler will try to inline this call + return a, b + end + ``` + +!!! compat "Julia 1.8" + The callsite annotation requires at least Julia 1.8. + +--- !!! note If the function is trivial (for example returning a constant) it might get inlined anyway. """ -macro noinline(ex) - esc(isa(ex, Expr) ? pushmeta!(ex, :noinline) : ex) +macro noinline(x) + return annotate_meta_def_or_block(x, :noinline) end """ @@ -301,6 +385,15 @@ end ## some macro utilities ## +unwrap_macrocalls(@nospecialize(x)) = x +function unwrap_macrocalls(ex::Expr) + inner = ex + while inner.head === :macrocall + inner = inner.args[end]::Expr + end + return inner +end + function pushmeta!(ex::Expr, sym::Symbol, args::Any...) if isempty(args) tag = sym @@ -308,10 +401,7 @@ function pushmeta!(ex::Expr, sym::Symbol, args::Any...) tag = Expr(sym, args...)::Expr end - inner = ex - while inner.head === :macrocall - inner = inner.args[end]::Expr - end + inner = unwrap_macrocalls(ex) idx, exargs = findmeta(inner) if idx != 0 @@ -361,8 +451,23 @@ function findmetaarg(metaargs, sym) return 0 end -function is_short_function_def(ex) - ex.head === :(=) || return false +function annotate_meta_def_or_block(@nospecialize(ex), meta::Symbol) + inner = unwrap_macrocalls(ex) + if is_function_def(inner) + # annotation on a definition + return esc(pushmeta!(ex, meta)) + else + # annotation on a block + return Expr(:block, + Expr(meta, true), + Expr(:local, Expr(:(=), :val, esc(ex))), + Expr(meta, false), + :val) + end +end + +function is_short_function_def(@nospecialize(ex)) + isexpr(ex, :(=)) || return false while length(ex.args) >= 1 && isa(ex.args[1], Expr) (ex.args[1].head === :call) && return true (ex.args[1].head === :where || ex.args[1].head === :(::)) || return false @@ -370,9 +475,11 @@ function is_short_function_def(ex) end return false end +is_function_def(@nospecialize(ex)) = + return isexpr(ex, :function) || is_short_function_def(ex) || isexpr(ex, :->) function findmeta(ex::Expr) - if ex.head === :function || is_short_function_def(ex) || ex.head === :-> + if is_function_def(ex) body = ex.args[2]::Expr body.head === :block || error(body, " is not a block expression") return findmeta_block(ex.args) diff --git a/base/meta.jl b/base/meta.jl index b483630a92f8fd..649ffe9d1a19c3 100644 --- a/base/meta.jl +++ b/base/meta.jl @@ -440,7 +440,7 @@ function _partially_inline!(@nospecialize(x), slot_replacements::Vector{Any}, @assert isa(arg, Union{GlobalRef, Symbol}) return x end - elseif !is_meta_expr_head(head) + elseif !Core.Compiler.is_meta_expr_head(head) partially_inline!(x.args, slot_replacements, type_signature, static_param_values, slot_offset, statement_offset, boundscheck) end @@ -450,6 +450,4 @@ end _instantiate_type_in_env(x, spsig, spvals) = ccall(:jl_instantiate_type_in_env, Any, (Any, Any, Ptr{Any}), x, spsig, spvals) -is_meta_expr_head(head::Symbol) = (head === :inbounds || head === :boundscheck || head === :meta || head === :loopinfo) - end # module diff --git a/src/ast.scm b/src/ast.scm index bc8d847279fc91..e5148a507a4fdb 100644 --- a/src/ast.scm +++ b/src/ast.scm @@ -289,7 +289,7 @@ ;; predicates and accessors (define (quoted? e) - (memq (car e) '(quote top core globalref outerref line break inert meta inbounds loopinfo))) + (memq (car e) '(quote top core globalref outerref line break inert meta inbounds inline noinline loopinfo))) (define (quotify e) `',e) (define (unquote e) (if (and (pair? e) (memq (car e) '(quote inert))) diff --git a/src/codegen.cpp b/src/codegen.cpp index 65702955691e2a..e0b86c51bf7d15 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -4434,7 +4434,7 @@ static void emit_stmtpos(jl_codectx_t &ctx, jl_value_t *expr, int ssaval_result) jl_value_t **args = (jl_value_t**)jl_array_data(ex->args); jl_sym_t *head = ex->head; if (head == meta_sym || head == inbounds_sym || head == coverageeffect_sym - || head == aliasscope_sym || head == popaliasscope_sym) { + || head == aliasscope_sym || head == popaliasscope_sym || head == inline_sym || head == noinline_sym) { // some expression types are metadata and can be ignored // in statement position return; @@ -4869,7 +4869,7 @@ static jl_cgval_t emit_expr(jl_codectx_t &ctx, jl_value_t *expr, ssize_t ssaval) } else if (head == leave_sym || head == coverageeffect_sym || head == pop_exception_sym || head == enter_sym || head == inbounds_sym - || head == aliasscope_sym || head == popaliasscope_sym) { + || head == aliasscope_sym || head == popaliasscope_sym || head == inline_sym || head == noinline_sym) { jl_errorf("Expr(:%s) in value position", jl_symbol_name(head)); } else if (head == boundscheck_sym) { diff --git a/src/interpreter.c b/src/interpreter.c index 4686efe054edf3..f999542d68c4f3 100644 --- a/src/interpreter.c +++ b/src/interpreter.c @@ -312,7 +312,7 @@ static jl_value_t *eval_value(jl_value_t *e, interpreter_state *s) return jl_true; } else if (head == meta_sym || head == coverageeffect_sym || head == inbounds_sym || head == loopinfo_sym || - head == aliasscope_sym || head == popaliasscope_sym) { + head == aliasscope_sym || head == popaliasscope_sym || head == inline_sym || head == noinline_sym) { return jl_nothing; } else if (head == gc_preserve_begin_sym || head == gc_preserve_end_sym) { diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index f00ea0c9ba6d9d..428b0513b7e526 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -3498,7 +3498,7 @@ f(x) = yt(x) thunk with-static-parameters toplevel-only global globalref outerref const-if-global thismodule const atomic null true false ssavalue isdefined toplevel module lambda - error gc_preserve_begin gc_preserve_end import using export))) + error gc_preserve_begin gc_preserve_end import using export inline noinline))) (define (local-in? s lam) (or (assq s (car (lam:vinfo lam))) @@ -4592,7 +4592,7 @@ f(x) = yt(x) (cons (car e) args))) ;; metadata expressions - ((line meta inbounds loopinfo gc_preserve_end aliasscope popaliasscope) + ((line meta inbounds loopinfo gc_preserve_end aliasscope popaliasscope inline noinline) (let ((have-ret? (and (pair? code) (pair? (car code)) (eq? (caar code) 'return)))) (cond ((eq? (car e) 'line) (set! current-loc e) @@ -4737,7 +4737,7 @@ f(x) = yt(x) (begin (set! linetable (cons (make-lineinfo name file line) linetable)) (set! current-loc 1))) (if (or reachable - (and (pair? e) (memq (car e) '(meta inbounds gc_preserve_begin gc_preserve_end aliasscope popaliasscope)))) + (and (pair? e) (memq (car e) '(meta inbounds gc_preserve_begin gc_preserve_end aliasscope popaliasscope inline noinline)))) (begin (set! code (cons e code)) (set! i (+ i 1)) (set! locs (cons current-loc locs))))) diff --git a/src/julia.h b/src/julia.h index e6edc71b42dfa1..e53b33bef674da 100644 --- a/src/julia.h +++ b/src/julia.h @@ -255,7 +255,8 @@ typedef struct _jl_code_info_t { jl_value_t *ssavaluetypes; // types of ssa values (or count of them) jl_array_t *ssaflags; // flags associated with each statement: // 0 = inbounds - // 1,2 = inlinehint,always-inline,noinline + // 1 = inline + // 2 = noinline // 3 = strict-ieee (strictfp) // 4 = effect-free (may be deleted if unused) // 5-6 = diff --git a/src/macroexpand.scm b/src/macroexpand.scm index 5e55c7bbb29c16..f17f4d3510dc60 100644 --- a/src/macroexpand.scm +++ b/src/macroexpand.scm @@ -352,7 +352,7 @@ ,(resolve-expansion-vars-with-new-env (caddr arg) env m parent-scope inarg)))) (else `(global ,(resolve-expansion-vars-with-new-env arg env m parent-scope inarg)))))) - ((using import export meta line inbounds boundscheck loopinfo) (map unescape e)) + ((using import export meta line inbounds boundscheck loopinfo inline noinline) (map unescape e)) ((macrocall) e) ; invalid syntax anyways, so just act like it's quoted. ((symboliclabel) e) ((symbolicgoto) e) diff --git a/src/method.c b/src/method.c index 9c255f786b74da..c17566ac6f9361 100644 --- a/src/method.c +++ b/src/method.c @@ -84,7 +84,8 @@ static jl_value_t *resolve_globals(jl_value_t *expr, jl_module_t *module, jl_sve e->head == quote_sym || e->head == inert_sym || e->head == meta_sym || e->head == inbounds_sym || e->head == boundscheck_sym || e->head == loopinfo_sym || - e->head == aliasscope_sym || e->head == popaliasscope_sym) { + e->head == aliasscope_sym || e->head == popaliasscope_sym || + e->head == inline_sym || e->head == noinline_sym) { // ignore these } else { @@ -253,6 +254,11 @@ JL_DLLEXPORT void jl_resolve_globals_in_ir(jl_array_t *stmts, jl_module_t *m, jl } } +jl_value_t *expr_arg1(jl_value_t *expr) { + jl_array_t *args = ((jl_expr_t*)expr)->args; + return jl_array_ptr_ref(args, 0); +} + // copy a :lambda Expr into its CodeInfo representation, // including popping of known meta nodes static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) @@ -274,8 +280,17 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) jl_gc_wb(li, li->code); size_t n = jl_array_len(body); jl_value_t **bd = (jl_value_t**)jl_array_ptr_data((jl_array_t*)li->code); + li->ssaflags = jl_alloc_array_1d(jl_array_uint8_type, n); + jl_gc_wb(li, li->ssaflags); + int inbounds_depth = 0; // number of stacked inbounds + // isempty(inline_flags): no user annotation + // last(inline_flags) == 1: inline region + // last(inline_flags) == 0: noinline region + arraylist_t *inline_flags = arraylist_new((arraylist_t*)malloc_s(sizeof(arraylist_t)), 0); for (j = 0; j < n; j++) { jl_value_t *st = bd[j]; + int is_flag_stmt = 0; + // check :meta expression if (jl_is_expr(st) && ((jl_expr_t*)st)->head == meta_sym) { size_t k, ins = 0, na = jl_expr_nargs(st); jl_array_t *meta = ((jl_expr_t*)st)->args; @@ -297,10 +312,60 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) else jl_array_del_end(meta, na - ins); } + // check other flag expressions + else if (jl_is_expr(st) && ((jl_expr_t*)st)->head == inbounds_sym) { + is_flag_stmt = 1; + jl_value_t *arg1 = expr_arg1(st); + if (arg1 == (jl_value_t*)jl_true) // push + inbounds_depth += 1; + else if (arg1 == (jl_value_t*)jl_false) // clear + inbounds_depth = 0; + else if (inbounds_depth > 0) // pop + inbounds_depth -= 1; + bd[j] = jl_nothing; + } + else if (jl_is_expr(st) && ((jl_expr_t*)st)->head == inline_sym) { + is_flag_stmt = 1; + jl_value_t *arg1 = expr_arg1(st); + if (arg1 == (jl_value_t*)jl_true) // enter inline region + arraylist_push(inline_flags, (void*)1); + else { // exit inline region + assert(arg1 == (jl_value_t*)jl_false); + arraylist_pop(inline_flags); + } + bd[j] = jl_nothing; + } + else if (jl_is_expr(st) && ((jl_expr_t*)st)->head == noinline_sym) { + is_flag_stmt = 1; + jl_value_t *arg1 = expr_arg1(st); + if (arg1 == (jl_value_t*)jl_true) // enter noinline region + arraylist_push(inline_flags, (void*)0); + else { // exit noinline region + assert(arg1 == (jl_value_t*)jl_false); + arraylist_pop(inline_flags); + } + bd[j] = jl_nothing; + } else if (jl_is_expr(st) && ((jl_expr_t*)st)->head == return_sym) { jl_array_ptr_set(body, j, jl_new_struct(jl_returnnode_type, jl_exprarg(st, 0))); } + + if (is_flag_stmt) + jl_array_uint8_set(li->ssaflags, j, 0); + else { + uint8_t flag = 0; + if (inbounds_depth > 0) + flag |= 1 << 0; + if (inline_flags->len > 0) { + void* inline_flag = inline_flags->items[inline_flags->len - 1]; + flag |= 1 << (inline_flag ? 1 : 2); + } + jl_array_uint8_set(li->ssaflags, j, flag); + } } + assert(inline_flags->len == 0); // malformed otherwise + arraylist_free(inline_flags); + free(inline_flags); jl_array_t *vinfo = (jl_array_t*)jl_exprarg(ir, 1); jl_array_t *vis = (jl_array_t*)jl_array_ptr_ref(vinfo, 0); size_t nslots = jl_array_len(vis); @@ -313,7 +378,6 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) jl_gc_wb(li, li->slotflags); li->ssavaluetypes = jl_box_long(nssavalue); jl_gc_wb(li, li->ssavaluetypes); - li->ssaflags = jl_alloc_array_1d(jl_array_uint8_type, 0); // Flags that need to be copied to slotflags const uint8_t vinfo_mask = 8 | 16 | 32 | 64; diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 9358dce3325f75..a04e29e608e6e7 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -389,11 +389,11 @@ function isinvoke(@nospecialize(x), pred) end return false end -code_typed1(args...; kwargs...) = (first∘first)(code_typed(args...; kwargs...))::Core.CodeInfo +code_typed1(args...; kwargs...) = (first(only(code_typed(args...; kwargs...)))::Core.CodeInfo).code @testset "@inline/@noinline annotation before definition" begin - m = Module() - @eval m begin + M = Module() + @eval M begin @inline function _def_inline(x) # this call won't be resolved and thus will prevent inlining to happen if we don't # annotate `@inline` at the top of this function body @@ -414,32 +414,32 @@ code_typed1(args...; kwargs...) = (first∘first)(code_typed(args...; kwargs...) def_noinline_noconflict(x) = _def_noinline_noconflict(x) end - let ci = code_typed1(m.def_inline, (Int,)) - @test all(ci.code) do x + let code = code_typed1(M.def_inline, (Int,)) + @test all(code) do x !isinvoke(x, :_def_inline) end end - let ci = code_typed1(m.def_noinline, (Int,)) - @test any(ci.code) do x + let code = code_typed1(M.def_noinline, (Int,)) + @test any(code) do x isinvoke(x, :_def_noinline) end end # test that they don't conflict with other "before-definition" macros - let ci = code_typed1(m.def_inline_noconflict, (Int,)) - @test all(ci.code) do x + let code = code_typed1(M.def_inline_noconflict, (Int,)) + @test all(code) do x !isinvoke(x, :_def_inline_noconflict) end end - let ci = code_typed1(m.def_noinline_noconflict, (Int,)) - @test any(ci.code) do x + let code = code_typed1(M.def_noinline_noconflict, (Int,)) + @test any(code) do x isinvoke(x, :_def_noinline_noconflict) end end end @testset "@inline/@noinline annotation within a function body" begin - m = Module() - @eval m begin + M = Module() + @eval M begin function _body_inline(x) @inline # this call won't be resolved and thus will prevent inlining to happen if we don't @@ -471,36 +471,126 @@ end end end - let ci = code_typed1(m.body_inline, (Int,)) - @test all(ci.code) do x + let code = code_typed1(M.body_inline, (Int,)) + @test all(code) do x !isinvoke(x, :_body_inline) end end - let ci = code_typed1(m.body_noinline, (Int,)) - @test any(ci.code) do x + let code = code_typed1(M.body_noinline, (Int,)) + @test any(code) do x isinvoke(x, :_body_noinline) end end # test annotations for `do` blocks - let ci = code_typed1(m.do_inline, (Int,)) + let code = code_typed1(M.do_inline, (Int,)) # what we test here is that both `simple_caller` and the anonymous function that the # `do` block creates should inlined away, and as a result there is only the unresolved call - @test all(ci.code) do x + @test all(code) do x !isinvoke(x, :simple_caller) && !isinvoke(x, mi->startswith(string(mi.def.name), '#')) end end - let ci = code_typed1(m.do_noinline, (Int,)) + let code = code_typed1(M.do_noinline, (Int,)) # the anonymous function that the `do` block created shouldn't be inlined here - @test any(ci.code) do x + @test any(code) do x isinvoke(x, mi->startswith(string(mi.def.name), '#')) end end end +@testset "callsite @inline/@noinline annotations" begin + M = Module() + @eval M begin + # this global variable prevents inference to fold everything as constant, and/or the optimizer to inline the call accessing to this + g = 0 + + @noinline noinlined_explicit(x) = x + force_inline_explicit(x) = @inline noinlined_explicit(x) + force_inline_block_explicit(x) = @inline noinlined_explicit(x) + noinlined_explicit(x) + noinlined_implicit(x) = g + force_inline_implicit(x) = @inline noinlined_implicit(x) + force_inline_block_implicit(x) = @inline noinlined_implicit(x) + noinlined_implicit(x) + + @inline inlined_explicit(x) = x + force_noinline_explicit(x) = @noinline inlined_explicit(x) + force_noinline_block_explicit(x) = @noinline inlined_explicit(x) + inlined_explicit(x) + inlined_implicit(x) = x + force_noinline_implicit(x) = @noinline inlined_implicit(x) + force_noinline_block_implicit(x) = @noinline inlined_implicit(x) + inlined_implicit(x) + + # test callsite annotations for constant-prop'ed calls + + @noinline Base.@aggressive_constprop noinlined_constprop_explicit(a) = a+g + force_inline_constprop_explicit() = @inline noinlined_constprop_explicit(0) + Base.@aggressive_constprop noinlined_constprop_implicit(a) = a+g + force_inline_constprop_implicit() = @inline noinlined_constprop_implicit(0) + + @inline Base.@aggressive_constprop inlined_constprop_explicit(a) = a+g + force_noinline_constprop_explicit() = @noinline inlined_constprop_explicit(0) + @inline Base.@aggressive_constprop inlined_constprop_implicit(a) = a+g + force_noinline_constprop_implicit() = @noinline inlined_constprop_implicit(0) + + @noinline notinlined(a) = a + function nested(a0, b0) + @noinline begin + a = @inline notinlined(a0) # this call should be inlined + b = notinlined(b0) # this call should NOT be inlined + return a, b + end + end + end + + let code = code_typed1(M.force_inline_explicit, (Int,)) + @test all(x->!isinvoke(x, :noinlined_explicit), code) + end + let code = code_typed1(M.force_inline_block_explicit, (Int,)) + @test all(code) do x + !isinvoke(x, :noinlined_explicit) && + !isinvoke(x, :(+)) + end + end + let code = code_typed1(M.force_inline_implicit, (Int,)) + @test all(x->!isinvoke(x, :noinlined_implicit), code) + end + let code = code_typed1(M.force_inline_block_implicit, (Int,)) + @test all(x->!isinvoke(x, :noinlined_explicit), code) + end + + let code = code_typed1(M.force_noinline_explicit, (Int,)) + @test any(x->isinvoke(x, :inlined_explicit), code) + end + let code = code_typed1(M.force_noinline_block_explicit, (Int,)) + @test count(x->isinvoke(x, :inlined_explicit), code) == 2 + end + let code = code_typed1(M.force_noinline_implicit, (Int,)) + @test any(x->isinvoke(x, :inlined_implicit), code) + end + let code = code_typed1(M.force_noinline_block_implicit, (Int,)) + @test count(x->isinvoke(x, :inlined_implicit), code) == 2 + end + + let code = code_typed1(M.force_inline_constprop_explicit) + @test all(x->!isinvoke(x, :noinlined_constprop_explicit), code) + end + let code = code_typed1(M.force_inline_constprop_implicit) + @test all(x->!isinvoke(x, :noinlined_constprop_implicit), code) + end + + let code = code_typed1(M.force_noinline_constprop_explicit) + @test any(x->isinvoke(x, :inlined_constprop_explicit), code) + end + let code = code_typed1(M.force_noinline_constprop_implicit) + @test any(x->isinvoke(x, :inlined_constprop_implicit), code) + end + + let code = code_typed1(M.nested, (Int,Int)) + @test count(x->isinvoke(x, :notinlined), code) == 1 + end +end + # force constant-prop' for `setproperty!` -let m = Module() - ci = @eval m begin +# https://github.com/JuliaLang/julia/pull/41882 +let code = @eval Module() begin # if we don't force constant-prop', `T = fieldtype(Foo, ::Symbol)` will be union-split to # `Union{Type{Any},Type{Int}` and it will make `convert(T, nothing)` too costly # and it leads to inlining failure @@ -518,7 +608,7 @@ let m = Module() $code_typed1(setter, (Vector{Foo},)) end - @test !any(x->isinvoke(x, :setproperty!), ci.code) + @test !any(x->isinvoke(x, :setproperty!), code) end # Issue #41299 - inlining deletes error check in :>