-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
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
base: master
Are you sure you want to change the base?
Conversation
Compiler/test/inline.jl
Outdated
There was a problem hiding this comment.
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!
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.
Co-authored-by: adienes <51664769+adienes@users.noreply.github.com>
2fb251c
to
91876ea
Compare
end | ||
_free_vars(s::Symbol) = [s] | ||
_free_vars(_) = [] | ||
_free_vars(e::Expr) = isempty(e.args) ? [] : unique!(mapreduce(_free_vars, vcat, e.args)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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.
This implements #21925 I think. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_free_vars
extracts all vars, not just free vars. This is an issue when the outlined body defines bound variables:
julia> function bar(n)
x = rand(Bool) ? 7 : 7.0
@outline sum(x^2 for _ in 1:n)
end
ERROR: syntax: all-underscore identifiers are write-only and their values cannot be used in expressions around REPL[1]:6
Stacktrace:
[1] top-level scope
@ REPL[13]:1
julia> function bar(n)
x = rand(Bool) ? 7 : 7.0
@outline sum(x^2 for this_name_not_defined_in_main in 1:n)
end
bar (generic function with 1 method)
julia> bar(4)
ERROR: UndefVarError: `this_name_not_defined_in_main` not defined in `Main`
Suggestion: check for spelling errors or missing imports.
Stacktrace:
[1] macro expansion
@ ./REPL[1]:6 [inlined]
[2] bar(n::Int64)
@ Main ./REPL[40]:3
[3] top-level scope
@ REPL[41]:1
Also tagging triage to discuss a significant new feature.
Co-authored-by: Lilith Orion Hafner <lilithhafner@gmail.com>
@@ -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)))) |
There was a problem hiding this comment.
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?
|
Triage likes the idea; nice feature to have if we can get a good implementation. |
Thanks all!
Ah, yeah interesting. Thanks for the explanation of the issue here, @LilithHafner and @JeffBezanson. It makes sense to me now. I originally considered a 0-arg closure, like @adienes suggested, but as discussed above, julia closures currently have a bunch of perf landmines that makes this not a great option. I'm not sure if this is something that can be resolved alone by a macro without perf impact, then. Maybe this would need compiler support, to be able to identify truly free variables? EDIT: @LilithHafner 🤔 is this not something that is statically determinable from the syntax? I.e. is this not something that |
@JeffBezanson i'm not 100% sure, but i don't think so: The goal of this change is to (as @StefanKarpinski suggested in #29688) outline not just the actual throw statement itself, but to also outline all the logic used to generate the exception message. As a simple example, if the user writes: @assert x > 0 && x < len(foo) && foo[x] < 0 "Either $x is out of bounds or $(foo)[$x] = $(foo[x]) is not negative" this generates: if x > 0 && x < len(foo) && foo[x] < 0
throw(AssertionError("Either $x is out of bounds or $(foo)[$x] = $(foo[x]) is not negative"))
end and the function body is going to blow up with all the code to pretty-print So we are already going to compile all this code anyway, but currently we're going to compile it into the callsite. |
OK, I was thinking the goal was just to outline the allocation and throw. It's different if the assertion has a complex message expression, which not every assertion does. But I still think it's too much to make a function for every single assertion. Outlining this code is a micro-optimization that is not always needed. Doing it where needed is fine. But scaling this over every assertion expression can indeed put a lot of strain on the compiler and just feels very bulky for something so simple. I think a good compromise is to evaluate the message expression (which is often a constant!) in-line and call a |
What we would ideally do is move all the code on the error path to a part of the assembly such that it didn't get branch predicted which should be the best of both worlds. Doing so, of course would require teaching LLVM a lot more about Julia exceptions. |
Oscar that would definitely help, although in practice I think some of the perf issues also come from just having a bloated LLVM IR from the error message string construction and the throw, rather than having a nice tight few-instructions function, which can get inlined, and is friendlier on instruction caches, etc. For example, |
Adds a new macro
@outline
, and uses it in@assert
.This implements the simplest suggestion in #29688.
It would still be excellent if the compiler would automatically perform this optimization for any
throw()
statement, but it sounds like introducing automatic outlining for such cases would be a difficult implementation task in the Compiler.And in either case, even if we had such an optimization, there may be cases where a human will want to perform this optimization themselves for a number of reasons, including code-size reduction, improving type stability, outlining rare branches, etc, and having this in a macro is a nice way to automate it, and make this optimization more accessible. 😊
Example macro usage:
This commit applies this new macro
@outline
to@assert
, producing essentially:or in a more complete example:
This can improve performance for fast code that uses assertions. For example, take this definition of
getindex
forWithoutMissingVector
:julia/base/sort.jl
Lines 597 to 601 in 5058dba
Before:
After:
The number of instructions in that function according to
@code_native
(on an aarch64 M2 MacBook) reduced from ~90 to ~40.