From 6eb20a8b96dbe96e76cca876eefe637dbf551696 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Tue, 22 Jun 2021 17:49:57 +0900 Subject: [PATCH] supports `@inline`/`@noinline` annotations within a function body Separated from #40754 for the sake of easier review. The primary motivation for this change is to annotate `@inline`/`@noinline` to anonymous functions created from `do` block: ```julia f() do @inline # makes this anonymous function to be inlined ... # function body end ``` We can extend the grammar so that we have special "declaration-macro" supports for `do`-block functions like: ```julia f() @inline do # makes this anonymous function to be inlined ... # function body end ``` but I'm not sure which one is better. Following [the earlier discussion](https://github.com/JuliaLang/julia/pull/40754#issuecomment-839401667), this commit implements the easiest solution. Co-authored-by: Joseph Tan --- NEWS.md | 1 + base/expr.jl | 47 ++++++++++++---- test/compiler/inline.jl | 117 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 10 deletions(-) diff --git a/NEWS.md b/NEWS.md index 6961710affa5a..53552fb1f50b1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,7 @@ 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. ([#40754]) Language changes ---------------- diff --git a/base/expr.jl b/base/expr.jl index 84b521543111b..c4c5e8da39b27 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -188,19 +188,32 @@ Give a hint to the compiler that this function is worth inlining. Small functions typically do not need the `@inline` annotation, as the compiler does it automatically. By using `@inline` on bigger functions, an extra nudge can be given to the compiler to inline it. -This is shown in the following example: + +`@inline` can be applied immediately before the definition or in its function body. ```julia -@inline function bigfunction(x) - #= - Function Definition - =# +# annotate long-form definition +@inline function longdef(x) + ... +end + +# annotate short-form definition +@inline shortdef(x) = ... + +# annotate anonymous function that a `do` block creates +f() do + @inline + ... end ``` + +!!! compat "Julia 1.7" + The usage within a function body requires at least Julia 1.7. """ macro inline(ex) esc(isa(ex, Expr) ? pushmeta!(ex, :inline) : ex) end +macro inline() Expr(:meta, :inline) end """ @noinline @@ -209,22 +222,36 @@ Give a hint to the compiler that it should not inline a function. Small functions are typically inlined automatically. By using `@noinline` on small functions, auto-inlining can be -prevented. This is shown in the following example: +prevented. + +`@noinline` can be applied immediately before the definition or in its function body. ```julia -@noinline function smallfunction(x) - #= - Function Definition - =# +# annotate long-form definition +@noinline function longdef(x) + ... +end + +# annotate short-form definition +@noinline shortdef(x) = ... + +# annotate anonymous function that a `do` block creates +f() do + @noinline + ... end ``` +!!! compat "Julia 1.7" + The usage within a function body requires at least Julia 1.7. + !!! 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) end +macro noinline() Expr(:meta, :noinline) end """ @pure ex diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 1ffd012acb755..48906ab3e632c 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -380,3 +380,120 @@ end 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 + @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 + return unresolved_call(x) + end + def_inline(x) = _def_inline(x) + @noinline _def_noinline(x) = x # obviously will be inlined otherwise + def_noinline(x) = _def_noinline(x) + + # test that they don't conflict with other "before-definition" macros + @inline Base.@aggressive_constprop function _def_inline_noconflict(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 + return unresolved_call(x) + end + def_inline_noconflict(x) = _def_inline_noconflict(x) + @noinline Base.@aggressive_constprop _def_noinline_noconflict(x) = x # obviously will be inlined otherwise + def_noinline_noconflict(x) = _def_noinline_noconflict(x) + end + + let ci = code_typed1(m.def_inline, (Int,)) + @test all(ci.code) do x + !isinvoke(x, :_def_inline) + end + end + let ci = code_typed1(m.def_noinline, (Int,)) + @test any(ci.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 + !isinvoke(x, :_def_inline_noconflict) + end + end + let ci = code_typed1(m.def_noinline_noconflict, (Int,)) + @test any(ci.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 + function _body_inline(x) + @inline + # 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 + return unresolved_call(x) + end + body_inline(x) = _body_inline(x) + function _body_noinline(x) + @noinline + return x # obviously will be inlined otherwise + end + body_noinline(x) = _body_noinline(x) + + # test annotations for `do` blocks + @inline simple_caller(a) = a() + function do_inline(x) + simple_caller() do + @inline + # this call won't be resolved and thus will prevent inlining to happen if we don't + # annotate `@inline` at the top of this anonymous function body + return unresolved_call(x) + end + end + function do_noinline(x) + simple_caller() do + @noinline + return x # obviously will be inlined otherwise + end + end + end + + let ci = code_typed1(m.body_inline, (Int,)) + @test all(ci.code) do x + !isinvoke(x, :_body_inline) + end + end + let ci = code_typed1(m.body_noinline, (Int,)) + @test any(ci.code) do x + isinvoke(x, :_body_noinline) + end + end + # test annotations for `do` blocks + let ci = 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 + !isinvoke(x, :simple_caller) && + !isinvoke(x, mi->startswith(string(mi.def.name), '#')) + end + end + let ci = 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 + isinvoke(x, mi->startswith(string(mi.def.name), '#')) + end + end +end