Skip to content

Commit

Permalink
Add a new macro @outline, and use it in @assert.
Browse files Browse the repository at this point in the history
Macro usage:
```julia
  @BoundsCheck i > 1 && i <= len || @outline throw(BoundsError(x, i))
```

This commit applies the above to Assertions, e.g.:
```julia
julia> @macroexpand @Assert x != x "x == x: $x"
:(if x != x
      nothing
  else
      #= REPL[3]:36 =#
      var"#17#outline"(x) = begin
              $(Expr(:meta, :noinline))
              #= REPL[3]:36 =#
              (throw)((AssertionError)(((Main).Base.inferencebarrier((Main).Base.string))("x == x: $(x)")))
          end
      #= REPL[3]:38 =#
      var"#17#outline"(x)
  end)
```

This can improve performance for fast code that uses assertions, e.g.:

Before:
```julia
julia> @Btime Base.Sort.WithoutMissingVector($(Any[1]))[$1]
  3.041 ns (0 allocations: 0 bytes)
1
```
After:
```julia
julia> @Btime Base.Sort.WithoutMissingVector($(Any[1]))[$1]
  2.250 ns (0 allocations: 0 bytes)
1
```

The number of instructions in that function according to `@code_native`
(on an aarch64 M2 MacBook) reduced from ~90 to ~40.
  • Loading branch information
NHDaly committed Jan 21, 2025
1 parent 316f0fe commit 1ac71fe
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 3 deletions.
39 changes: 39 additions & 0 deletions Compiler/test/inline.jl
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
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))))
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 2 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]
Expand Down

0 comments on commit 1ac71fe

Please sign in to comment.