# Escape Analysis MTG @ 2021/06/07
# From abstract interpretation point of view
## Current States of pointer/alias analysis
Some of pointer/alias analysis is already performed, but very limitedly.
Within our current lattice implementation, "must-alias" information is essentially same as object identity,
which can be only exploited for `Const` objects,
and so exact pointer value information is known only for `Const` and `PartialStruct`.
```julia
julia> # constant folding
struct Foo
foo
end
julia> code_typed(; optimize=false) do
f1 = Foo(1) # Const(Foo(1))
f2 = f1 # Const(Foo(1))
# alias information
r1 = f1 === f2 # Const(true), i.e. "must-alias"
r2 = f1.foo === f2.foo # (Const(1), Const(1)), pointers are fully known
return r1, r2
end |> first
CodeInfo(
1 ─ (f1 = Main.Foo(1))::Core.Const(Foo(1))
│ (f2 = f1::Core.Const(Foo(1)))::Core.Const(Foo(1))
│ (r1 = f1::Core.Const(Foo(1)) === f2::Core.Const(Foo(1)))::Core.Const(true)
│ %4 = Base.getproperty(f1::Core.Const(Foo(1)), :foo)::Core.Const(1)
│ %5 = Base.getproperty(f2::Core.Const(Foo(1)), :foo)::Core.Const(1)
│ (r2 = %4 === %5)::Core.Const(true)
│ %7 = Core.tuple(r1::Core.Const(true), r2::Core.Const(true))::Core.Const((true, true))
└── return %7
) => Tuple{Bool, Bool}
julia> # partial constant folding
struct FooPartial
foo1
foo2
end
julia> code_typed((Any,); optimize=false) do foo2
f1 = FooPartial(1, foo2) # PartialStruct(FooPartial, Any[Const(1), Any])
f2 = f1 # PartialStruct(FooPartial, Any[Const(1), Any])
# alias information
r1 = f1 === f2 # Bool, not unknown
r21 = f1.foo1 === f2.foo1 # Const(true), pointers can be partially known
r22 = f1.foo2 === f2.foo2 # Bool, not known
@assert f1.foo2 === f2.foo2
r23 = f1.foo2 === f2.foo2 # Bool, not known even if we manually asserted
return r1, r21, r22, r23
end |> first
CodeInfo(
1 ─ Core.NewvarNode(:(r23))::Any
│ (f1 = Main.FooPartial(1, foo2))::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any])
│ (f2 = f1::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]))::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any])
│ (r1 = f1::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]) === f2::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]))::Bool
│ %5 = Base.getproperty(f1::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]), :foo1)::Core.Const(1)
│ %6 = Base.getproperty(f2::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]), :foo1)::Core.Const(1)
│ (r21 = %5 === %6)::Core.Const(true)
│ %8 = Base.getproperty(f1::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]), :foo2)::Any
│ %9 = Base.getproperty(f2::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]), :foo2)::Any
│ (r22 = %8 === %9)::Bool
│ %11 = Base.getproperty(f1::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]), :foo2)::Any
│ %12 = Base.getproperty(f2::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]), :foo2)::Any
│ %13 = (%11 === %12)::Bool
└── goto #3 if not %13
2 ─ goto #4
3 ─ %16 = Base.AssertionError("f1.foo2 === f2.foo2")::Any
└── Base.throw(%16)::Union{}
4 ┄ %18 = Base.getproperty(f1::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]), :foo2)::Any
│ %19 = Base.getproperty(f2::Core.PartialStruct(FooPartial, Any[Core.Const(1), Any]), :foo2)::Any
│ (r23 = %18 === %19)::Bool
│ %21 = Core.tuple(r1, r21::Core.Const(true), r22, r23)::Core.PartialStruct(NTuple{4, Bool}, Any[Bool, Core.Const(true), Bool, Bool])
└── return %21
) => NTuple{4, Bool}
```
Currently `Const` and `PartialStruct` don't handle `mutable struct` objects because we don't track scope of pointers (this is an escape information ?)
(especially, handled [here](https://github.com/JuliaLang/julia/blob/0a246488f87203ffe11f60f3a506b9db83298b6c/base/compiler/abstractinterpretation.jl#L1431)).
```julia
julia> mutable struct FooMutable
foo
end
julia> code_typed(; optimize=false) do
f1 = FooMutable(1) # FooMutable
f2 = f1 # FooMutable
# alias information
r1 = f1 === f2 # Bool, not known
r2 = f1.foo === f2.foo # Bool, not known
return r1, r2
end |> first
CodeInfo(
1 ─ (f1 = Main.FooMutable(1))::FooMutable
│ (f2 = f1)::FooMutable
│ (r1 = f1 === f2)::Bool
│ %4 = Base.getproperty(f1, :foo)::Any
│ %5 = Base.getproperty(f2, :foo)::Any
│ (r2 = %4 === %5)::Bool
│ %7 = Core.tuple(r1, r2)::Tuple{Bool, Bool}
└── return %7
) => Tuple{Bool, Bool}
```
## Improvement ideas
### Local pointer analysis improvement
- We can introduce new lattice like `ConstrainableField` and `ConstainableStruct`,
to improve local pointer information a bit more
E.g.:
```julia
julia> struct Foo
foo
end
julia> code_typed((Foo,); optimize=false) do foo
# foo :: ConstainableStruct(Foo, Any[])
if isa(foo.foo, Int) # foo.foo :: ConstrainableField(SlotNumber(2), :foo, Int)
# foo :: ConstainableStruct(Foo, Any[(:foo, Int)])
return foo.foo # :: Int
end
end |> first
CodeInfo(
1 ─ %1 = Base.getproperty(foo, :foo)::Any
│ %2 = (%1 isa Main.Int)::Bool
└── goto #3 if not %2
2 ─ %4 = Base.getproperty(foo, :foo)::Any
└── return %4
3 ─ return nothing
) => Any
```
- Still limited to `struct`, because we lack escape information
- probably we can't use these lattices for "must-alias" information
## Escape analysis
Once escape analysis is landed, we may be able to enable (partial) constant-folding on `mutable struct`s
and do something like `ConstrainableField/Struct` stuff for them.
```julia
julia> mutable struct FooMutable
foo
end
julia> code_typed(; optimize=false) do
# now we know both `f1` and `f2` doesn't escape this scope, we can constant-fold them
f1 = FooMutable(1) # Const(FooMutable(1))
f2 = f1 # Const(FooMutable(1))
# alias information
r1 = f1 === f2 # Const(true)
r2 = f1.foo === f2.foo # Const(true)
return r1, r2
end |> first
CodeInfo(
1 ─ (f1 = Main.FooMutable(1))::FooMutable
│ (f2 = f1)::FooMutable
│ (r1 = f1 === f2)::Bool
│ %4 = Base.getproperty(f1, :foo)::Any
│ %5 = Base.getproperty(f2, :foo)::Any
│ (r2 = %4 === %5)::Bool
│ %7 = Core.tuple(r1, r2)::Tuple{Bool, Bool}
└── return %7
) => Tuple{Bool, Bool}
```
Considerations:
- this motivates doing escape analysis _during_ inference
- but escape analysis is probably better to be implemented as a post-inference process, so we may not be able to exploit the information during abstract interpret..
* but escape information might be hard to drive without type information
# Questions
- is it reasonable to think of usages of escape information for optimizaitons, _fully on Julia-level_ ?
- some possible optimizations (from [wiki](https://en.wikipedia.org/wiki/Escape_analysis)):
* **scalar replacement:**: will be possible as an optimization pass
* **heap allocations to stack allocations**: how to enable this on Julia level ? introduce meta label ?
* synchronization elision: almost sure this is out of our scope for the time being
# Discussions
- target `mutable struct`s
* finalizers ?
* no escape, then unbox ?
- recursive calls
* self-recursive isn't a probrem
* mutually-recursive is an evil
* but the complicated cycle handling is only necessary for inference, and for a separated escape analysis pass, we can just write a simple code to handle cycles ?
* two different sources of alias approach
* find alias bindings: by abstract interpretation
* use allocation sites: what LLVM does it already