diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index efcebf8c1408e..23ee50dbef0ae 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -700,17 +700,20 @@ function const_prop_methodinstance_heuristic(interp::AbstractInterpreter, method # isn't particularly helpful here. return true end - # Peek at the inferred result for the function to determine if the optimizer - # was able to cut it down to something simple (inlineable in particular). - # If so, there's a good chance we might be able to const prop all the way - # through and learn something new. - code = get(code_cache(interp), mi, nothing) - declared_inline = isdefined(method, :source) && ccall(:jl_ir_flag_inlineable, Bool, (Any,), method.source) - cache_inlineable = declared_inline - if isdefined(code, :inferred) && !cache_inlineable - cache_inf = code.inferred - if !(cache_inf === nothing) - cache_inlineable = inlining_policy(interp)(cache_inf) !== nothing + # we approximate the profitability of the const-prop by inlineability below, + # check if the method is declared or already analyzed to be inlined first + cache_inlineable = is_inlineable(method) + if !cache_inlineable + # Peek at the inferred result for the function to determine if the optimizer + # was able to cut it down to something simple (inlineable in particular). + # If so, there's a good chance we might be able to const prop all the way + # through and learn something new. + code = get(code_cache(interp), mi, nothing) + if isdefined(code, :inferred) + cache_inf = code.inferred + if !(cache_inf === nothing) + cache_inlineable = inlining_policy(interp)(cache_inf) !== nothing + end end end if !cache_inlineable diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 1898aa8b75778..15c3cd9a47032 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -195,7 +195,7 @@ function finish(interp::AbstractInterpreter, opt::OptimizationState, params::Opt (; src, linfo) = opt (; def, specTypes) = linfo - force_noinline = _any(@nospecialize(x) -> isexpr(x, :meta) && x.args[1] === :noinline, ir.meta) + force_noinline = isa(def, Method) && is_declared_noinline(def) # compute inlining and other related optimizations if (isa(result, Const) || isconstType(result)) diff --git a/base/compiler/utilities.jl b/base/compiler/utilities.jl index 5d4d52172a300..ad15309097b89 100644 --- a/base/compiler/utilities.jl +++ b/base/compiler/utilities.jl @@ -96,6 +96,34 @@ function is_inlineable_constant(@nospecialize(x)) return count_const_size(x) <= MAX_INLINE_CONST_SIZE end +""" + is_inlineable(method::Method) -> Bool + +Check if `method` is eligible for inlining. +""" +function is_inlineable(method::Method) + isdefined(method, :source) || return false + return ccall(:jl_ir_flag_inlineable, Bool, (Any,), method.source) +end + +""" + is_declared_noinline(method::Method) -> Bool + +Check if `method` is declared as `@noinline`. +""" +function is_declared_noinline(method::Method) + isdefined(method, :source) || return false + source = method.source + if isa(source, Vector{UInt8}) + return source[1] & 1 << 3 ≠ 0 + elseif isa(source, CodeInfo) + return _any(source.code) do @nospecialize stmt + isexpr(stmt, :meta) && stmt.args[1] === :noinline + end + end + return false +end + ########################### # MethodInstance/CodeInfo # ########################### diff --git a/src/ircode.c b/src/ircode.c index 212febe121a75..82a8176a0bfc0 100644 --- a/src/ircode.c +++ b/src/ircode.c @@ -702,9 +702,14 @@ JL_DLLEXPORT jl_array_t *jl_compress_ir(jl_method_t *m, jl_code_info_t *code) jl_current_task->ptls }; - uint8_t flags = (code->aggressive_constprop << 4) - | (code->inferred << 3) - | (code->inlineable << 2) + uint8_t flags = (code->aggressive_constprop << 5) + | (code->inferred << 4) + // `code->inlineable` requires a special treatment: + // `code->inlineable == 0`: default + // `code->inlineable == 1`: declared as `@inline` + // `code->inlineable == 2`: declared as `@noinline` + | ((code->inlineable == 2) << 3) + | ((code->inlineable == 1) << 2) | (code->propagate_inbounds << 1) | (code->pure << 0); write_uint8(s.s, flags); @@ -788,8 +793,8 @@ JL_DLLEXPORT jl_code_info_t *jl_uncompress_ir(jl_method_t *m, jl_code_instance_t jl_code_info_t *code = jl_new_code_info_uninit(); uint8_t flags = read_uint8(s.s); - code->aggressive_constprop = !!(flags & (1 << 4)); - code->inferred = !!(flags & (1 << 3)); + code->aggressive_constprop = !!(flags & (1 << 5)); + code->inferred = !!(flags & (1 << 4)); code->inlineable = !!(flags & (1 << 2)); code->propagate_inbounds = !!(flags & (1 << 1)); code->pure = !!(flags & (1 << 0)); @@ -848,7 +853,7 @@ JL_DLLEXPORT uint8_t jl_ir_flag_inferred(jl_array_t *data) return ((jl_code_info_t*)data)->inferred; assert(jl_typeis(data, jl_array_uint8_type)); uint8_t flags = ((uint8_t*)data->data)[0]; - return !!(flags & (1 << 3)); + return !!(flags & (1 << 4)); } JL_DLLEXPORT uint8_t jl_ir_flag_inlineable(jl_array_t *data) diff --git a/src/method.c b/src/method.c index 48b074e800904..0ff265cc873ba 100644 --- a/src/method.c +++ b/src/method.c @@ -289,8 +289,11 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) li->propagate_inbounds = 1; else if (ma == (jl_value_t*)aggressive_constprop_sym) li->aggressive_constprop = 1; - else + else { + if (ma == (jl_value_t*)noinline_sym) + li->inlineable = 2; jl_array_ptr_set(meta, ins++, ma); + } } if (ins == 0) bd[j] = jl_nothing; diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 00797304ce5c0..fbf998fae7af7 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -4,6 +4,16 @@ using Test using Base.Meta using Core: ReturnNode +# check if `x` is a statically-resolved call of a function whose name is `sym` +isinvoke(@nospecialize(x), sym::Symbol) = isinvoke(x, mi->mi.def.name===sym) +function isinvoke(@nospecialize(x), pred) + if Meta.isexpr(x, :invoke) + return pred(x.args[1]::Core.MethodInstance) + end + return false +end +code_typed1(args...; kwargs...) = first(only(code_typed(args...; kwargs...)))::Core.CodeInfo + """ Helper to walk the AST and call a function on every node. """ @@ -116,7 +126,7 @@ end # issue #29083 f29083(;μ,σ) = μ + σ*randn() g29083() = f29083(μ=2.0,σ=0.1) -let c = code_typed(g29083, ())[1][1].code +let c = code_typed1(g29083, ()).code # make sure no call to kwfunc remains @test !any(e->(isa(e,Expr) && ((e.head === :invoke && e.args[1].def.name === :kwfunc) || (e.head === :foreigncall && e.args[1] === QuoteNode(:jl_get_keyword_sorter)))), @@ -151,13 +161,13 @@ end end function fully_eliminated(f, args) - let code = code_typed(f, args)[1][1].code + let code = code_typed1(f, args).code return length(code) == 1 && isa(code[1], ReturnNode) end end function fully_eliminated(f, args, retval) - let code = code_typed(f, args)[1][1].code + let code = code_typed1(f, args).code return length(code) == 1 && isa(code[1], ReturnNode) && code[1].val == retval end end @@ -173,7 +183,7 @@ function f_ifelse(x) return b ? x + 1 : x end # 2 for now because the compiler leaves a GotoNode around -@test_broken length(code_typed(f_ifelse, (String,))[1][1].code) <= 2 +@test_broken length(code_typed1(f_ifelse, (String,)).code) <= 2 # Test that inlining of _apply_iterate properly hits the inference cache @noinline cprop_inline_foo1() = (1, 1) @@ -210,7 +220,7 @@ end function cprop_inline_baz2() return cprop_inline_bar(cprop_inline_foo2()..., cprop_inline_foo2()...) end -@test length(code_typed(cprop_inline_baz2, ())[1][1].code) == 2 +@test length(code_typed1(cprop_inline_baz2, ()).code) == 2 # Check that apply_type/TypeVar can be fully eliminated function f_apply_typevar(T) @@ -230,7 +240,7 @@ function f_div(x, y) div(x, y) return x end -@test length(code_typed(f_div, (Int, Int))[1][1].code) > 1 +@test length(code_typed1(f_div, (Int, Int)).code) > 1 f_identity_splat(t) = (t...,) @test fully_eliminated(f_identity_splat, (Tuple{Int,Int},)) @@ -250,7 +260,7 @@ end # check that pointerref gets deleted if unused f_pointerref(T::Type{S}) where S = Val(length(T.parameters)) -let code = code_typed(f_pointerref, Tuple{Type{Int}})[1][1].code +let code = code_typed1(f_pointerref, Tuple{Type{Int}}).code any_ptrref = false for i = 1:length(code) stmt = code[i] @@ -286,16 +296,26 @@ f34900(x, y::Int) = y f34900(x::Int, y::Int) = invoke(f34900, Tuple{Int, Any}, x, y) @test fully_eliminated(f34900, Tuple{Int, Int}, Core.Argument(2)) -@testset "check jl_ir_flag_inlineable for inline macro" begin - @test ccall(:jl_ir_flag_inlineable, Bool, (Any,), first(methods(@inline x -> x)).source) - @test !ccall(:jl_ir_flag_inlineable, Bool, (Any,), first(methods( x -> x)).source) - @test ccall(:jl_ir_flag_inlineable, Bool, (Any,), first(methods(@inline function f(x) x end)).source) - @test !ccall(:jl_ir_flag_inlineable, Bool, (Any,), first(methods(function f(x) x end)).source) +@testset "check `@inline`/`@noinline` declaration" begin + import Core.Compiler: is_inlineable, is_declared_noinline + @test is_inlineable(only(methods(@inline x -> x))) + @test is_inlineable(only(methods(x -> (@inline; x)))) + @test !is_inlineable(only(methods(x -> x))) + @test is_inlineable(only(methods(@inline function f(x) x end))) + @test is_inlineable(only(methods(function f(x) @inline; x end))) + @test !is_inlineable(only(methods(function f(x) x end))) + + @test is_declared_noinline(only(methods(@noinline x -> x))) + @test is_declared_noinline(only(methods(x -> (@noinline; x)))) + @test !is_declared_noinline(only(methods(x -> x))) + @test is_declared_noinline(only(methods(@noinline function f(x) x end))) + @test is_declared_noinline(only(methods(function f(x) @noinline; x end))) + @test !is_declared_noinline(only(methods(function f(x) x end))) end const _a_global_array = [1] f_inline_global_getindex() = _a_global_array[1] -let ci = code_typed(f_inline_global_getindex, Tuple{})[1].first +let ci = code_typed1(f_inline_global_getindex, Tuple{}) @test any(x->(isexpr(x, :call) && x.args[1] === GlobalRef(Base, :arrayref)), ci.code) end @@ -303,11 +323,11 @@ end f_29115(x) = (x...,) @test @allocated(f_29115(1)) == 0 @test @allocated(f_29115(1=>2)) == 0 -let ci = code_typed(f_29115, Tuple{Int64})[1].first +let ci = code_typed1(f_29115, Tuple{Int64}) @test length(ci.code) == 2 && isexpr(ci.code[1], :call) && ci.code[1].args[1] === GlobalRef(Core, :tuple) end -let ci = code_typed(f_29115, Tuple{Pair{Int64, Int64}})[1].first +let ci = code_typed1(f_29115, Tuple{Pair{Int64, Int64}}) @test length(ci.code) == 4 && isexpr(ci.code[1], :call) && ci.code[end-1].args[1] === GlobalRef(Core, :tuple) end @@ -324,7 +344,7 @@ struct NonIsBitsDims dims::NTuple{N, Int} where N end NonIsBitsDims() = NonIsBitsDims(()) -let ci = code_typed(NonIsBitsDims, Tuple{})[1].first +let ci = code_typed1(NonIsBitsDims, Tuple{}) @test length(ci.code) == 1 && isa(ci.code[1], ReturnNode) && ci.code[1].val.value == NonIsBitsDims() end @@ -365,8 +385,8 @@ end # Union splitting of convert f_convert_missing(x) = convert(Int64, x) -let ci = code_typed(f_convert_missing, Tuple{Union{Int64, Missing}})[1][1], - ci_unopt = code_typed(f_convert_missing, Tuple{Union{Int64, Missing}}; optimize=false)[1][1] +let ci = code_typed1(f_convert_missing, Tuple{Union{Int64, Missing}}), + ci_unopt = code_typed1(f_convert_missing, Tuple{Union{Int64, Missing}}; optimize=false) # We want to check that inlining was able to union split this, but we don't # want to make the test too specific to the exact structure that inlining # generates, so instead, we just check that the compiler made it bigger. @@ -381,16 +401,6 @@ using Base.Experimental: @opaque f_oc_getfield(x) = (@opaque ()->x)() @test fully_eliminated(f_oc_getfield, Tuple{Int}) -# check if `x` is a statically-resolved call of a function whose name is `sym` -isinvoke(@nospecialize(x), sym::Symbol) = isinvoke(x, mi->mi.def.name===sym) -function isinvoke(@nospecialize(x), pred) - if Meta.isexpr(x, :invoke) - return pred(x.args[1]::Core.MethodInstance) - end - return false -end -code_typed1(args...; kwargs...) = (first∘first)(code_typed(args...; kwargs...))::Core.CodeInfo - @testset "@inline/@noinline annotation before definition" begin m = Module() @eval m begin