Skip to content

Commit d64da8d

Browse files
committed
Add a new macro @outline, and use it in @assert.
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.
1 parent f91436e commit d64da8d

File tree

4 files changed

+86
-3
lines changed

4 files changed

+86
-3
lines changed

Compiler/test/inline.jl

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2309,3 +2309,42 @@ g_noinline_invoke(x) = f_noinline_invoke(x)
23092309
let src = code_typed1(g_noinline_invoke, (Union{Symbol,Nothing},))
23102310
@test !any(@nospecialize(x)->isa(x,GlobalRef), src.code)
23112311
end
2312+
2313+
@testset "@outline" begin
2314+
@testset "basic" begin
2315+
@test @outline(2) == 2
2316+
@test @outline(2 + 2) == 4
2317+
2318+
x = 10
2319+
@test @outline(x + 1) == 11
2320+
@test @outline(x + x) == 20
2321+
2322+
negate(x) = -x
2323+
@test @outline(negate(+(1, 2))) == -3
2324+
end
2325+
2326+
@testset "throw exception" begin
2327+
@test_throws BoundsError((), 1) @outline(throw(BoundsError((), 1)))
2328+
a = []
2329+
@test_throws BoundsError(a, 1) @outline(throw(BoundsError(a, 1)))
2330+
2331+
@test_throws AssertionError("false") @outline @assert false
2332+
@test_throws AssertionError("violated") @outline @assert false "violated"
2333+
2334+
x = 10
2335+
@test_throws AssertionError("x == 0") @outline @assert x == 0
2336+
@test_throws AssertionError("x: 10") @outline @assert x == 0 "x: $x"
2337+
end
2338+
2339+
@testset "in a function" begin
2340+
function get_first(tup)
2341+
if isempty(tup)
2342+
@outline(throw(BoundsError(tup, 1)))
2343+
end
2344+
return first(tup)
2345+
end
2346+
@test get_first((1,)) == 1
2347+
@test get_first((1,2)) == 1
2348+
@test_throws BoundsError((), 1) get_first(())
2349+
end
2350+
end

base/error.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,14 @@ macro assert(ex, msgs...)
235235
# message is an expression needing evaluating
236236
# N.B. To reduce the risk of invalidation caused by the complex callstack involved
237237
# with `string`, use `inferencebarrier` here to hide this `string` from the compiler.
238-
msg = :(Main.Base.inferencebarrier(Main.Base.string)($(esc(msg))))
238+
msg = :($Main.Base.inferencebarrier($Main.Base.string)($(msg)))
239239
elseif isdefined(Main, :Base) && isdefined(Main.Base, :string) && applicable(Main.Base.string, msg)
240240
msg = Main.Base.string(msg)
241241
else
242242
# string() might not be defined during bootstrap
243-
msg = :(Main.Base.inferencebarrier(_assert_tostring)($(Expr(:quote,msg))))
243+
msg = :($Main.Base.inferencebarrier($_assert_tostring)($(Expr(:quote,msg))))
244244
end
245-
return :($(esc(ex)) ? $(nothing) : throw(AssertionError($msg)))
245+
return esc(:($(ex) ? $(nothing) : $Base.@outline($throw($AssertionError($msg)))))
246246
end
247247

248248
# this may be overridden in contexts where `string(::Expr)` doesn't work

base/exports.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,7 @@ export
10671067
@simd,
10681068
@inline,
10691069
@noinline,
1070+
@outline,
10701071
@nospecialize,
10711072
@specialize,
10721073
@polly,

base/expr.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,49 @@ macro noinline(x)
355355
return annotate_meta_def_or_block(x, :noinline)
356356
end
357357

358+
"""
359+
@outline expr
360+
361+
Outline an expression into its own function, and call that function.
362+
363+
This macro introduces a "function barrier", which can be helpful in some code optimization
364+
scenarios. The expr is extracted into an outlined function, which is marked `@noinline`.
365+
366+
Outlining an expr can be used to make a function smaller, e.g. by outlining an unlikely
367+
branch, which could help with runtime performance by improving instruction cache locality,
368+
and could help with compilation performance since 2 smaller functions can sometimes compile
369+
faster than one larger function. Finally, outlining can be useful for type-stability, by
370+
outlining a type unstable block within a hot loop, where the outlined function could be type
371+
stable.
372+
373+
A common use case is to @outline the code that throws exceptions, since this should be a
374+
rare case, but it can introduce a lot of complexity to the generated code, which can
375+
sometimes harm the compiler's ability to optimize.
376+
377+
# Examples
378+
```julia
379+
function getindex(container, index)
380+
if index < 1 || index > length(container)
381+
# Outline this throw, since constructing a BoundsError requires boxing
382+
# the arguments, which produces a lot of code.
383+
@outline throw(BoundsError(container, index))
384+
end
385+
return container.data[index]
386+
end
387+
```
388+
"""
389+
macro outline(expr)
390+
vars = esc.(_free_vars(expr))
391+
quote
392+
@noinline outline($(vars...)) = $(esc(expr))
393+
394+
outline($(vars...))
395+
end
396+
end
397+
_free_vars(s::Symbol) = [s]
398+
_free_vars(_) = []
399+
_free_vars(e::Expr) = isempty(e.args) ? [] : unique!(mapreduce(_free_vars, vcat, e.args))
400+
358401
"""
359402
Base.@constprop setting [ex]
360403

0 commit comments

Comments
 (0)