diff --git a/Compiler/test/inline.jl b/Compiler/test/inline.jl index 5f95fb761859e..0fc00de457f24 100644 --- a/Compiler/test/inline.jl +++ b/Compiler/test/inline.jl @@ -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 diff --git a/NEWS.md b/NEWS.md index 71014e1e57695..6fdded316fb3c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 -------------------- diff --git a/base/error.jl b/base/error.jl index 276555033443a..6e0cdeea09fd3 100644 --- a/base/error.jl +++ b/base/error.jl @@ -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)))) + 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 diff --git a/base/exports.jl b/base/exports.jl index 56cd58ce269e7..547cc802c28eb 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -1067,6 +1067,7 @@ export @simd, @inline, @noinline, + @outline, @nospecialize, @specialize, @polly, diff --git a/base/expr.jl b/base/expr.jl index 84078829f77ed..3d963fc37d439 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -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) + # 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)) + """ Base.@constprop setting [ex]