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 3 commits into
base: master
Choose a base branch
from

Conversation

NHDaly
Copy link
Member

@NHDaly NHDaly commented Jan 21, 2025

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:

  @boundscheck if !(i > 1 && i <= length(x))
      @outline throw(BoundsError(x, i))
  end

This commit applies this new macro @outline to @assert, producing essentially:

# @assert x == y
x == y ? nothing : @outline(throw(AssertionError("x == y"))

or in a more complete example:

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. For example, take this definition of getindex for WithoutMissingVector:

julia/base/sort.jl

Lines 597 to 601 in 5058dba

Base.@propagate_inbounds function Base.getindex(v::WithoutMissingVector, i::Integer)
out = v.data[i]
@assert !(out isa Missing)
out::eltype(v)
end

Before:

julia> @btime Base.Sort.WithoutMissingVector($(Any[1]))[$1]
  3.041 ns (0 allocations: 0 bytes)
1

After:

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.

base/expr.jl Outdated Show resolved Hide resolved
base/expr.jl Outdated Show resolved Hide resolved
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!

NHDaly and others added 3 commits January 21, 2025 18:06
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.
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants