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

Port EscapeAnalysis to Core.Compiler with Optimization #3

Open
wants to merge 32 commits into
base: master
Choose a base branch
from

Conversation

TH3CHARLie
Copy link
Owner

No description provided.

@TH3CHARLie
Copy link
Owner Author

TH3CHARLie commented Jul 26, 2021

A status report of this PR:

after disabling the isbitstype related feature (due to assertion failure in argextype) and disabling the global cache of invoke (therefore analyzing every statically resolved call when encountered), escape analysis pass is now turned on by default and can successfully pass the bootstrapping.

My goal is first to observe the IR_FLAG_NO_ESCAPE is successfully set and can be observed on C++ side because this is critical to decide on what LLVM we should special-case.

First we verify the result using EscapeAnalysis.jl, to make the results in sync with the porting version, I disabled some features in the package (see above for description):

julia> using EscapeAnalysis
[ Info: Precompiling EscapeAnalysis [c6a77b2f-5d97-4bf0-b260-35fa9cc0f3d0]

julia> function dummy_ref(s::String, b::Base.RefValue{String})
                  a = Ref(s)
                  a === b
              end
dummy_ref (generic function with 1 method)

julia> @analyze_escapes dummy_ref("haha", Ref("haha"))
typeof(dummy_ref)(_2::String ↑, _3::Base.RefValue{String} ↑, _4::Base.RefValue{String} ↓)
1 ↓ 1 ─ %1 = %new(Base.RefValue{String}, _2)::Base.RefValue{String}
2 ↑ │   %2 = (%1 === _3)::Bool
3 ◌ └──      return %2

As we expected, the %1 is marked as NoEscape, therefore the statement will be added IR_FLAG_NO_ESCAPE.

We use the following code to observe the flag on C++ side (see below), but it never triggers, notice here we don't need to use Revise, we just start the REPL compiled using this PR's code.

julia> function dummy_ref(s::String, b::Base.RefValue{String})
                  a = Ref(s)
                  a === b
              end
dummy_ref (generic function with 1 method)

julia> @code_typed optimize=true dummy_ref("haha", Ref("haha"))
CodeInfo(
1 ─ %1 = %new(Base.RefValue{String}, s)::Base.RefValue{String}
│   %2 = (%1 === b)::Bool
└──      return %2
) => Bool

src/codegen.cpp Outdated Show resolved Hide resolved
@TH3CHARLie TH3CHARLie marked this pull request as draft July 27, 2021 14:32
printf("llvm-alloc-opt: find a metadata tag %p Function %p size %d\n", orig, &F, sz);
}
}
if (use_info.escaped && !has_metadata_tag) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main changes here!

@TH3CHARLie
Copy link
Owner Author

TH3CHARLie commented Aug 3, 2021

A status report of the latest progress:

I find the instruction will make the use_info.escaped and will therefore go to the optimizeTag call. So I changed the logic to make Instruction tagged with our metadata go to the moveToStack call even if it is marked as escaped in the alloc-opt pass. The main code change is shown in the comment above.

However, this leads to an abort error:

signal (6): Abort trap: 6
in expression starting at REPL[2]:1
__pthread_kill at /usr/lib/system/libsystem_kernel.dylib (unknown line)
Allocations: 1156428 (Pool: 1152371; Big: 4057); GC: 1
[1]    76587 abort      ./julia

cc @vtjnash

@vtjnash
Copy link

vtjnash commented Aug 3, 2021

It is unfortunate that it seems not to have unwind info for abort yet (Apple may have a fix in the next release), but catching that happen in lldb will likely show you which assert is failing.

TH3CHARLie and others added 4 commits August 4, 2021 16:16
So EscapeAnalysis.jl has a serious performance problem, which appears
only in bootstrapping.

I made some inspections using the following `print` debug:
> within `find_escapes!`
```julia
if length(argtypes) > 0
    Core.println(argtypes[1], " ", length(argtypes), " ", nstmts)
end
```
... and it seems like the performance of `find_escapes!` will be really
awful when there is a large number of statements, say, `nstmts > 100`.

The worse case I confirmed so far happens when analyzing
`construct_ssa!(::CodeInfo, ::IRCode, ::DomTree, ::Any, ::Vector{Any})`,
where I got the print `Core.Const(val=Core.Compiler.construct_ssa!) 182 
4262`,
and it took more than 100 sec on my machine (!!!).
Note that `find_escapes!` works on after-inlining IR, so we often such
many statements.

The interesting property is, this performance problem seems really
specific to bootstrapping. I confirmed that `find_escapes!` runs in
0.34 sec when I enabled it later using Revise.
```julia
julia> @time code_typed(Core.Compiler.construct_ssa!, 
(Core.Compiler.CodeInfo, Core.Compiler.IRCode,
Core.Compiler.DomTree, Any, Vector{Any}));
...
Core.Const(val=Core.Compiler.setindex!) 4 5
Core.Const(val=Core.Compiler.construct_ssa!) 182 4262
0.342626 seconds (723.68 k allocations: 177.828 MiB, 15.69% gc time, 
99.94% compilation time)
```
fix performance problem in bootstrapping
@TH3CHARLie

This comment has been minimized.

@TH3CHARLie
Copy link
Owner Author

Like this? @vchuravy

julia> @noinline f_no_escape(@nospecialize(x)) = typeof(x)
f_no_escape (generic function with 1 method)

julia> function g(x)
           aaa = Ref("foo")
           a = f_no_escape(aaa)
           nothing
       end
g (generic function with 1 method)

julia> @code_llvm g("haha")
;  @ REPL[5]:1 within `g`
; Function Attrs: sspstrong
define nonnull {}* @japi1_g_152({}* %0, {}** %1, i32 %2) #0 {
top:
  %3 = alloca {}**, align 8
  store volatile {}** %1, {}*** %3, align 8
;  @ REPL[5]:4 within `g`
  ret {}* inttoptr (i64 4557434888 to {}*)
}

@vchuravy
Copy link

What's the LLVM for no_escape? And does it run?

@TH3CHARLie
Copy link
Owner Author

the LLVM for f_no_escape

julia> @code_llvm f_no_escape(Ref("foo"))
;  @ REPL[4]:1 within `f_no_escape`
; Function Attrs: sspstrong
define nonnull {}* @japi1_f_no_escape_153({}* %0, {}** %1, i32 %2) #0 {
top:
  %3 = alloca {}**, align 8
  store volatile {}** %1, {}*** %3, align 8
  ret {}* inttoptr (i64 4772411712 to {}*)
}

Given this, I think it's inlined inside g, am I correct?

@vchuravy
Copy link

vchuravy commented Aug 15, 2021 via email

@vchuravy
Copy link

vchuravy commented Aug 15, 2021 via email

@TH3CHARLie TH3CHARLie marked this pull request as ready for review August 16, 2021 13:09
@TH3CHARLie
Copy link
Owner Author

With the below example, I think the analysis pass can look through the Base.inferencebarrier call

julia> @noinline f_no_escape(x) = Base.inferencebarrier(nothing)
f_no_escape (generic function with 1 method)

julia> @analyze_escapes f_no_escape(Ref("hah"))
typeof(f_no_escape)(_2::Base.RefValue{String} ↑)
1 ◌ 1 ─     return nothing


julia> @noinline f_no_escape(x) = (Base.inferencebarrier(nothing); x)
f_no_escape (generic function with 1 method)

julia> @analyze_escapes f_no_escape(Ref("hah"))
typeof(f_no_escape)(_2::Base.RefValue{String} ↑)
1 ◌ 1 ─     return _2

@vtjnash
Copy link

vtjnash commented Aug 16, 2021

@vchuravy is suggesting something like this:

julia> @noinline f(x) = typeof(x.contents)

julia> @code_llvm f(Core.Box(3))
define nonnull {}* @japi1_f_366({}* %0, {}** %1, i32 %2) #0 {
top:
  %3 = alloca {}**, align 8
  store volatile {}** %1, {}*** %3, align 8
  %4 = bitcast {}** %1 to {}***
  %5 = load {}**, {}*** %4, align 8
; ┌ @ Base.jl:42 within `getproperty`
   %6 = load atomic {}*, {}** %5 unordered, align 8
   %.not = icmp eq {}* %6, null
   br i1 %.not, label %fail, label %pass

fail:                                             ; preds = %top
   call void @jl_throw({}* inttoptr (i64 140632140761280 to {}*))
   unreachable

pass:                                             ; preds = %top
; └
  %7 = bitcast {}* %6 to i64*
  %8 = getelementptr inbounds i64, i64* %7, i64 -1
  %9 = load atomic i64, i64* %8 unordered, align 8
  %10 = and i64 %9, -16
  %11 = inttoptr i64 %10 to {}*
  ret {}* %11
}

@TH3CHARLie
Copy link
Owner Author

Then in this case, x of the function f will be marked as Escape after the analysis pass.

@vtjnash
Copy link

vtjnash commented Aug 16, 2021

Where does it escape from? It can't be the typeof call, since the compiler / runtime sometimes injects though just for convenience

@TH3CHARLie
Copy link
Owner Author

TH3CHARLie commented Aug 16, 2021

In your example, contents is accessed by a getfield, and it's here that the argument get marked as escaped.

On current master of EscapeAnalysis.jl

julia> @analyze_escapes f_no_escape(Core.Box(3))
typeof(f_no_escape)(_2::Core.Box *)
1 ◌ 1 ─ %1 = Base.getfield(_2, :contents)::Any
2 ↑ │   %2 = Main.typeof(%1)::DataType
3 ◌ └──      return %2

@vtjnash

This comment has been minimized.

@TH3CHARLie
Copy link
Owner Author

Yeah I've noticed the difference and have updated the result in the comment

@vtjnash
Copy link

vtjnash commented Aug 16, 2021

I suppose because it could throw? Does the Some{Any}().value type work for this example?

@TH3CHARLie
Copy link
Owner Author

I think anything involving in a getfield is conservatively handled because we cannot prove its effect-freeness now: https://github.com/aviatesk/EscapeAnalysis.jl/blob/34449a763b14a90f0eebe0ea842f88f248e524c6/test/runtests.jl#L129-L131

@aviatesk
Copy link

The throwness check depends on minimum number of initialized fields, and that's why Jameson asked whether Some{Any}().value works or not (and further, this is why we use MutableSome in our test suite, etc.).
Follow the link in the comment and check the definitions of those structs to understand the differences.

@TH3CHARLie
Copy link
Owner Author

I am still pretty confused about this. By "depends on minimum number of initialized fields" do you mean the check will produce different results given the different numbers of fields inside the struct (for example, MutableSome only has one)?

For jameson's comment, are we looking for something like this:

julia> using EscapeAnalysis

julia> @noinline f_no_escape(x::Some{Any}) = typeof(x.value)
f_no_escape (generic function with 1 method)

julia> @analyze_escapes f_no_escape(Some(1))
ERROR: TypeError: in typeassert, expected Core.Compiler.IRCode, got a value of type Nothing
Stacktrace:
 [1] #analyze_escapes#7
   @ ~/dev/EscapeAnalysis.jl/src/EscapeAnalysis.jl:637 [inlined]
 [2] analyze_escapes(f::Any, types::Any)
   @ EscapeAnalysis ~/dev/EscapeAnalysis.jl/src/EscapeAnalysis.jl:635
 [3] top-level scope
   @ REPL[3]:1

@aviatesk
Copy link

check the type of Some(1)

@TH3CHARLie
Copy link
Owner Author

Oh yeah I added a signature to it now it becomes:

julia> @analyze_escapes f_no_escape(Some{Any}(1))
typeof(f_no_escape)(_2::Some{Any} ↑)
1 ◌ 1 ─ %1 = Base.getfield(_2, :value)::Any
2 ↑ │   %2 = Main.typeof(%1)::DataType
3 ◌ └──      return %2

@vchuravy
Copy link

vchuravy commented Aug 20, 2021 via email

@TH3CHARLie TH3CHARLie changed the title Port EscapeAnalysis to Core.Compiler Port EscapeAnalysis to Core.Compiler with Optimization Aug 22, 2021
TH3CHARLie pushed a commit that referenced this pull request Oct 13, 2021
…tional` (JuliaLang#42434)

Otherwise, in rare cases, we may see this sort of weird behavior:
```julia
julia> @eval edgecase(_) = $(Core.Compiler.InterConditional(2, Int, Any))
edgecase (generic function with 1 method)

julia> code_typed((Any,)) do x
           edgecase(x) ? x : nothing
       end
1-element Vector{Any}:
 CodeInfo(
1 ─     goto #3 if not $(QuoteNode(Core.InterConditional(2, Int64, Any)))
2 ─     return x
3 ─     return Main.nothing
) => Any

julia> code_typed((Any,)) do x
           edgecase(x) ? x : nothing
       end
1-element Vector{Any}:
 CodeInfo(
1 ─      goto #3 if not $(QuoteNode(Core.InterConditional(2, Int64, Any)))
2 ─ %2 = π (x, Int64)
└──      return %2
3 ─      return Main.nothing
) => Union{Nothing, Int64}
```
@LilithHafner
Copy link

Escape analysis is super useful for a variety of reasons. I came here from JuliaLang#7721, where better escape analysis means better automatic closing of resources.

Has this work made its way into JuliaLang/julia yet?

@aviatesk
Copy link

aviatesk commented Nov 9, 2022

It's already merge into the Julia base. We aren't enabling it (nor using it for any optimizations) at this moment because of a latency problem though.

xref: JuliaLang#43800

@aviatesk
Copy link

aviatesk commented Nov 9, 2022

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