Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new macro @outline, and use it in @assert. #57122

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Compiler/test/inline.jl
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this file was the right place to put the @outline tests. It seems decent, but a bit oxymoronic. :D Please let me know if you have any better suggestions!

Original file line number Diff line number Diff line change
Expand Up @@ -2309,3 +2309,42 @@ g_noinline_invoke(x) = f_noinline_invoke(x)
let src = code_typed1(g_noinline_invoke, (Union{Symbol,Nothing},))
@test !any(@nospecialize(x)->isa(x,GlobalRef), src.code)
end

@testset "@outline" begin
@testset "basic" begin
@test @outline(2) == 2
@test @outline(2 + 2) == 4

x = 10
@test @outline(x + 1) == 11
@test @outline(x + x) == 20

negate(x) = -x
@test @outline(negate(+(1, 2))) == -3
end

@testset "throw exception" begin
@test_throws BoundsError((), 1) @outline(throw(BoundsError((), 1)))
a = []
@test_throws BoundsError(a, 1) @outline(throw(BoundsError(a, 1)))

@test_throws AssertionError("false") @outline @assert false
@test_throws AssertionError("violated") @outline @assert false "violated"

x = 10
@test_throws AssertionError("x == 0") @outline @assert x == 0
@test_throws AssertionError("x: 10") @outline @assert x == 0 "x: $x"
end

@testset "in a function" begin
function get_first(tup)
if isempty(tup)
@outline(throw(BoundsError(tup, 1)))
end
return first(tup)
end
@test get_first((1,)) == 1
@test get_first((1,2)) == 1
@test_throws BoundsError((), 1) get_first(())
end
end
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ New library functions
* `insertdims(array; dims)` allows to insert singleton dimensions into an array which is the inverse operation to `dropdims`. ([#45793])
* The new `Fix` type is a generalization of `Fix1/Fix2` for fixing a single argument ([#54653]).
* `Sys.detectwsl()` allows to testing if Julia is running inside WSL at runtime. ([#57069])
* `@outline expr` moves `expr` out to a separate, noinlined function -- a common performance optimization for error-throwing parts of code. ([#57122])

New library features
--------------------
Expand Down
6 changes: 3 additions & 3 deletions base/error.jl
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,14 @@ macro assert(ex, msgs...)
# message is an expression needing evaluating
# N.B. To reduce the risk of invalidation caused by the complex callstack involved
# with `string`, use `inferencebarrier` here to hide this `string` from the compiler.
msg = :(Main.Base.inferencebarrier(Main.Base.string)($(esc(msg))))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this esc needed?

msg = :($Main.Base.inferencebarrier($Main.Base.string)($(msg)))
elseif isdefined(Main, :Base) && isdefined(Main.Base, :string) && applicable(Main.Base.string, msg)
msg = Main.Base.string(msg)
else
# string() might not be defined during bootstrap
msg = :(Main.Base.inferencebarrier(_assert_tostring)($(Expr(:quote,msg))))
msg = :($Main.Base.inferencebarrier($_assert_tostring)($(Expr(:quote,msg))))
end
return :($(esc(ex)) ? $(nothing) : throw(AssertionError($msg)))
return esc(:($(ex) ? $(nothing) : $Base.@outline($throw($AssertionError($msg)))))
end

# this may be overridden in contexts where `string(::Expr)` doesn't work
Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,7 @@ export
@simd,
@inline,
@noinline,
@outline,
@nospecialize,
@specialize,
@polly,
Expand Down
43 changes: 43 additions & 0 deletions base/expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,49 @@ macro noinline(x)
return annotate_meta_def_or_block(x, :noinline)
end

"""
@outline expr

Outline an expression into its own function, and call that function.

This macro introduces a "function barrier", which can be helpful in some code optimization
scenarios. The expr is extracted into an outlined function, which is marked `@noinline`.

Outlining an expr can be used to make a function smaller, e.g. by outlining an unlikely
branch, which could help with runtime performance by improving instruction cache locality,
and could help with compilation performance since two smaller functions can sometimes compile
faster than one larger function. Finally, outlining can be useful for type-stability, by
outlining a type unstable block within a hot loop, where the outlined function could be type
stable.

A common use case is to `@outline` the code that throws exceptions, since this should be a
rare case, but it can introduce a lot of complexity to the generated code, which can
sometimes harm the compiler's ability to optimize.

# Examples
```julia
function getindex(container, index)
if index < 1 || index > length(container)
NHDaly marked this conversation as resolved.
Show resolved Hide resolved
# Outline this throw, since constructing a BoundsError requires boxing
# the arguments, which produces a lot of code.
@outline throw(BoundsError(container, index))
end
return container.data[index]
end
```
"""
macro outline(expr)
vars = esc.(_free_vars(expr))
quote
@noinline outline($(vars...)) = $(esc(expr))

outline($(vars...))
end
end
_free_vars(s::Symbol) = [s]
_free_vars(_) = []
_free_vars(e::Expr) = isempty(e.args) ? [] : unique!(mapreduce(_free_vars, vcat, e.args))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think unique! is not defined yet so this fails to compile for me

although, what happens if you use outline as a zero-arg closure and don't pass the vars in explicitly at all? like

macro outline2(expr)
    local fname = gensym(:outlined_expr)
    quote
        @noinline $(fname)() = $(esc(expr))
        $(fname)()
    end
end

comparing the @code_native of

foo1() = @outline rand() > 0.5 || throw(AssertionError("abc"))
foo2() = @outline2 rand() > 0.5 || throw(AssertionError("abc"))

it seems a lot simpler, but I'm not an expert at these things so maybe I'm missing something

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One difference is in how they handle type instability:

julia> function bar()
           x = rand(Bool) ? 7 : 7.0
           @outline x*x+x*x+x*x*x+x/x-x+x
       end
bar (generic function with 1 method)

julia> function bar2()
           x = rand(Bool) ? 7 : 7.0
           @outline2 x*x+x*x+x*x*x+x/x-x+x
       end
bar2 (generic function with 1 method)

julia> @b bar
6.373 ns

julia> @b bar2
75.544 ns (2.40 allocs: 38.346 bytes)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the issue with 0-arg closures is that they can box their arguments in a bunch of caess. I was hoping to avoid that by taking advantage of macro-magic. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the bug report though - shouldn't be too hard to fix. I didn't notice working locally with Revise.


"""
Base.@constprop setting [ex]

Expand Down
Loading