Skip to content

Commit

Permalink
supports @inline/@noinline annotations within a function body
Browse files Browse the repository at this point in the history
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](#40754 (comment)),
this commit implements the easiest solution.

Co-authored-by: Joseph Tan <[email protected]>
  • Loading branch information
aviatesk and dghosef committed Jun 22, 2021
1 parent a4dd5e5 commit 6eb20a8
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 10 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------
Expand Down
47 changes: 37 additions & 10 deletions base/expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
117 changes: 117 additions & 0 deletions test/compiler/inline.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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...) = (firstfirst)(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

0 comments on commit 6eb20a8

Please sign in to comment.