EscapeAnalysis · The Julia Language
Description: Documentation for The Julia Language.

# `EscapeAnalysis`

`Compiler.EscapeAnalysis` is a compiler utility module that aims to analyze escape information of Julia's SSA-form IR a.k.a. `IRCode`.

This escape analysis aims to:

- leverage Julia's high-level semantics, especially reason about escapes and aliasing via inter-procedural calls

- be versatile enough to be used for various optimizations including alias-aware SROA, early `finalize` insertion, copy-free `ImmutableArray` construction, stack allocation of mutable objects, and so on.

- achieve a simple implementation based on a fully backward data-flow analysis implementation as well as a new lattice design that combines orthogonal lattice properties

## Try it out!

You can give a try to the escape analysis by loading the `EAUtils.jl` utility script that defines the convenience entries `code_escapes` and `@code_escapes` for testing and debugging purposes:

    julia> # InteractiveUtils.@activate Compiler # to use the stdlib version of the Compiler

           let JULIA_DIR = normpath(Sys.BINDIR, Base.DATAROOTDIR, "julia")
               include(normpath(JULIA_DIR, "Compiler", "test", "EAUtils.jl"))
               using .EAUtils
           end
    julia> mutable struct SafeRef{T}
               x::T
           end
    julia> Base.getindex(x::SafeRef) = x.x;
    julia> Base.setindex!(x::SafeRef, v) = x.x = v;
    julia> Base.isassigned(x::SafeRef) = true;
    julia> get′(x) = isassigned(x) ? x[] : throw(x);
    julia> result = code_escapes((Base.RefValue{String},String,String,)) do r1, s2, s3
               r2 = Ref(s2)
               r3 = SafeRef(s3)
               try
                   s1 = get′(r1)
                   ret = sizeof(s1)
               catch err
                   global GV = err # `r1` may escape
               end
               s2 = get′(r2)       # `r2` doesn't escape
               s3 = get′(r3)       # `r3` doesn't escape
               return s2, s3, s4
           end#2(X r1::Base.RefValue{String}, ✓ s2::String, ✓ s3::String) in Main at REPL[7]:2
    ✓′ 1 ── %1  = %new(Base.RefValue{String}, _3)::Base.RefValue{String}
    ✓′ └─── %2  = %new(Main.SafeRef{String}, _4)::Main.SafeRef{String}
    ✓′ 2 ── %3  = ϒ (%2)::Main.SafeRef{String}
    ✓′ │    %4  = ϒ (%1)::Core.PartialStruct(Base.RefValue{String}, Union{Nothing, Bool}[0], Any[String])
    ◌  └─── %5  = enter #8
    ◌  3 ── %6  =   builtin Base.isdefined(_2, :x)::Bool
    ◌  └───       goto #5 if not %6
    ◌  4 ──       goto #6
    ◌  5 ──         builtin Main.throw(_2)::Union{}
    ◌  └───       unreachable
    ◌  6 ──       $(Expr(:leave, :(%5)))
    ◌  7 ──       goto #11
    ✓′ 8 ┄─ %13 = φᶜ (%3)::Main.SafeRef{String}
    ✓′ │    %14 = φᶜ (%4)::Core.PartialStruct(Base.RefValue{String}, Union{Nothing, Bool}[0], Any[String])
    X  └─── %15 = $(Expr(:the_exception))::Base.RefValue{String}
    ◌  9 ──       nothing::Nothing
    X  10 ─         builtin Core.setglobal!(Main, :GV, %15)::Base.RefValue{String}
    ◌  └───       $(Expr(:pop_exception, :(%5)))::Core.Const(nothing)
    ✓′ 11 ┄ %19 = φ (#7 => %2, #10 => %13)::Main.SafeRef{String}
    ✓′ │    %20 = φ (#7 => %1, #10 => %14)::Core.PartialStruct(Base.RefValue{String}, Union{Nothing, Bool}[0], Any[String])
    ✓  │    %21 =   builtin Base.getfield(%20, :x)::String
    ✓  │    %22 =   builtin Base.getfield(%19, :x)::String
    X  │    %23 = Main.s4::Any
    ↑′ │    %24 =   builtin Core.tuple(%21, %22, %23)::Tuple{String, String, Any}
    ◌  └───       return %24

The symbols on the side of each call argument and SSA statements represent the following meaning:

- `◌` (plain): this value is not analyzed because escape information of it won't be used anyway (when the object is `isbitstype` for example)

- `✓` (green or cyan): this value never escapes (`has_no_escape(result.state[x])` holds), colored cyan if it has arg escape also (`has_arg_escape(result.state[x])` holds)

- `↑` (blue or yellow): this value can escape to the caller via return (`has_return_escape(result.state[x])` holds), colored yellow if it has unhandled thrown escape also (`has_thrown_escape(result.state[x])` holds)

- `X` (red): this value can escape to somewhere the escape analysis can't reason about like escapes to a global memory (`has_all_escape(result.state[x])` holds)

- `*` (bold): this value's escape state is between the `ReturnEscape` and `AllEscape` in the partial order of `EscapeInfo`, colored yellow if it has unhandled thrown escape also (`has_thrown_escape(result.state[x])` holds)

- `′`: this value has additional object field / array element information in its `AliasInfo` property

Escape information of each call argument and SSA value can be inspected programmatically as like:

    julia> result.state[Core.Argument(3)] # get EscapeInfo of `s2`ArgEscape
    julia> result.state[Core.SSAValue(3)] # get EscapeInfo of `r3`NoEscape′

## Analysis Design

### Lattice Design

`EscapeAnalysis` is implemented as a data-flow analysis that works on a lattice of `x::EscapeInfo`, which is composed of the following properties:

- `x.Analyzed::Bool`: not formally part of the lattice, only indicates whether `x` has been analyzed or not

- `x.ReturnEscape::BitSet`: records SSA statements where `x` can escape to the caller via return

- `x.ThrownEscape::BitSet`: records SSA statements where `x` can be thrown as exception (used for the exception handling described below)

- `x.AliasInfo`: maintains all possible values that can be aliased to fields or array elements of `x` (used for the alias analysis described below)

- `x.ArgEscape::Int` (not implemented yet): indicates it will escape to the caller through `setfield!` on argument(s)

These attributes can be combined to create a partial lattice that has a finite height, given the invariant that an input program has a finite number of statements, which is assured by Julia's semantics. The clever part of this lattice design is that it enables a simpler implementation of lattice operations by allowing them to handle each lattice property separately[LatticeDesign].

### Backward Escape Propagation

This escape analysis implementation is based on the data-flow algorithm described in the paper[MM02]. The analysis works on the lattice of `EscapeInfo` and transitions lattice elements from the bottom to the top until every lattice element gets converged to a fixed point by maintaining a (conceptual) working set that contains program counters corresponding to remaining SSA statements to be analyzed. The analysis manages a single global state that tracks `EscapeInfo` of each argument and SSA statement, but also note that some flow-sensitivity is encoded as program counters recorded in `EscapeInfo`'s `ReturnEscape` property, which can be combined with domination analysis later to reason about flow-sensitivity if necessary.

One distinctive design of this escape analysis is that it is fully backward, i.e. escape information flows from usages to definitions. For example, in the code snippet below, EA first analyzes the statement `return %1` and imposes `ReturnEscape` on `%1` (corresponding to `obj`), and then it analyzes `%1 = %new(Base.RefValue{Base.RefValue{String}, _2}))` and propagates the `ReturnEscape` imposed on `%1` to the call argument `_2` (corresponding to `s`):

    julia> code_escapes((Base.RefValue{String},)) do s
               obj = Ref(s)
               return obj
           end#5(↑ s::Base.RefValue{String}) in Main at REPL[1]:2
    ↑′ 1 ─ %1 = %new(Base.RefValue{Base.RefValue{String}}, _2)::Base.RefValue{Base.RefValue{String}}
    ◌  └──      return %1

The key observation here is that this backward analysis allows escape information to flow naturally along the use-def chain rather than control-flow[BackandForth]. As a result this scheme enables a simple implementation of escape analysis, e.g. `PhiNode` for example can be handled simply by propagating escape information imposed on a `PhiNode` to its predecessor values:

    julia> code_escapes((Bool, Base.RefValue{String}, Base.RefValue{String})) do cnd, s, t
               if cnd
                   obj = Ref(s)
               else
                   obj = Ref(t)
               end
               return obj
           end#8(✓ cnd::Bool, ↑ s::Base.RefValue{String}, ↑ t::Base.RefValue{String}) in Main at REPL[1]:2
    ◌  1 ─      goto #3 if not _2
    ↑′ 2 ─ %2 = %new(Base.RefValue{Base.RefValue{String}}, _3)::Base.RefValue{Base.RefValue{String}}
    ◌  └──      goto #4
    ↑′ 3 ─ %4 = %new(Base.RefValue{Base.RefValue{String}}, _4)::Base.RefValue{Base.RefValue{String}}
    ↑′ 4 ┄ %5 = φ (#2 => %2, #3 => %4)::Core.PartialStruct(Base.RefValue{Base.RefValue{String}}, Union{Nothing, Bool}[0], Any[Base.RefValue{String}])
    ◌  └──      return %5

### Alias Analysis

`EscapeAnalysis` implements a backward field analysis in order to reason about escapes imposed on object fields with certain accuracy, and `x::EscapeInfo`'s `x.AliasInfo` property exists for this purpose. It records all possible values that can be aliased to fields of `x` at "usage" sites, and then the escape information of that recorded values are propagated to the actual field values later at "definition" sites. More specifically, the analysis records a value that may be aliased to a field of object by analyzing `getfield` call, and then it propagates its escape information to the field when analyzing `%new(...)` expression or `setfield!` call[Dynamism].

    julia> code_escapes((String,)) do s
               obj = SafeRef("init")
               obj[] = s
               v = obj[]
               return v
           end#11(✓ s::String) in Main at REPL[1]:2
    ◌  1 ─     return _2

In the example above, `ReturnEscape` imposed on `%3` (corresponding to `v`) is not directly propagated to `%1` (corresponding to `obj`) but rather that `ReturnEscape` is only propagated to `_2` (corresponding to `s`). Here `%3` is recorded in `%1`'s `AliasInfo` property as it can be aliased to the first field of `%1`, and then when analyzing `Base.setfield!(%1, :x, _2)::String`, that escape information is propagated to `_2` but not to `%1`.

So `EscapeAnalysis` tracks which IR elements can be aliased across a `getfield`-`%new`/`setfield!` chain in order to analyze escapes of object fields, but actually this alias analysis needs to be generalized to handle other IR elements as well. This is because in Julia IR the same object is sometimes represented by different IR elements and so we should make sure that those different IR elements that actually can represent the same object share the same escape information. IR elements that return the same object as their operand(s), such as `PiNode` and `typeassert`, can cause that IR-level aliasing and thus requires escape information imposed on any of such aliased values to be shared between them. More interestingly, it is also needed for correctly reasoning about mutations on `PhiNode`. Let's consider the following example:

    julia> code_escapes((Bool, String,)) do cond, x
               if cond
                   ϕ2 = ϕ1 = SafeRef("foo")
               else
                   ϕ2 = ϕ1 = SafeRef("bar")
               end
               ϕ2[] = x
               y = ϕ1[]
               return y
           end#14(✓ cond::Bool, ✓ x::String) in Main at REPL[1]:2
    ◌  1 ─      goto #3 if not _2
    ✓′ 2 ─ %2 = %new(Main.SafeRef{String}, "foo")::Main.SafeRef{String}
    ◌  └──      goto #4
    ✓′ 3 ─ %4 = %new(Main.SafeRef{String}, "bar")::Main.SafeRef{String}
    ✓′ 4 ┄ %5 = φ (#2 => %2, #3 => %4)::Main.SafeRef{String}
    ✓′ │   %6 = φ (#2 => %2, #3 => %4)::Main.SafeRef{String}
    ◌  │          builtin Base.setfield!(%5, :x, _3)::String
    ✓  │   %8 =   builtin Base.getfield(%6, :x)::String
    ◌  └──      return %8

`ϕ1 = %5` and `ϕ2 = %6` are aliased and thus `ReturnEscape` imposed on `%8 = Base.getfield(%6, :x)::String` (corresponding to `y = ϕ1[]`) needs to be propagated to `Base.setfield!(%5, :x, _3)::String` (corresponding to `ϕ2[] = x`). In order for such escape information to be propagated correctly, the analysis should recognize that the predecessors of `ϕ1` and `ϕ2` can be aliased as well and equalize their escape information.

One interesting property of such aliasing information is that it is not known at "usage" site but can only be derived at "definition" site (as aliasing is conceptually equivalent to assignment), and thus it doesn't naturally fit in a backward analysis. In order to efficiently propagate escape information between related values, EscapeAnalysis.jl uses an approach inspired by the escape analysis algorithm explained in an old JVM paper[JVM05]. That is, in addition to managing escape lattice elements, the analysis also maintains an "equi"-alias set, a disjoint set of aliased arguments and SSA statements. The alias set manages values that can be aliased to each other and allows escape information imposed on any of such aliased values to be equalized between them.

### Array Analysis

The alias analysis for object fields described above can also be generalized to analyze array operations. `EscapeAnalysis` implements handlings for various primitive array operations so that it can propagate escapes via `arrayref`-`arrayset` use-def chain and does not escape allocated arrays too conservatively:

    julia> code_escapes((String,)) do s
               ary = Any[]
               push!(ary, SafeRef(s))
               return ary[1], length(ary)
           end#17(✓ s::String) in Main at REPL[1]:2
    X  1 ── %1  =   builtin Core.memorynew(Memory{Any}, 0)::Memory{Any}
    X  │    %2  =   builtin Core.memoryrefnew(%1)::MemoryRef{Any}
    X  │    %3  = %new(Vector{Any}, %2, (0,))::Vector{Any}
    X  │    %4  = %new(Main.SafeRef{String}, _2)::Main.SafeRef{String}
    X  │    %5  =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %6  =   builtin Base.getfield(%5, :mem)::Memory{Any}
    X  │    %7  =   builtin Base.getfield(%6, :length)::Int64
    X  │    %8  =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %9  =   builtin Base.getfield(%8, 1)::Int64
    ◌  │    %10 = intrinsic Base.Checked.checked_sadd_int(%9, 1)::Tuple{Int64, Bool}
    ◌  │    %11 =   builtin Base.Checked.getfield(%10, 1)::Int64
    ◌  │    %12 =   builtin Base.Checked.getfield(%10, 2)::Bool
    ◌  └───       goto #3 if not %12
    ✓  2 ──          invoke Base.Checked.throw_overflowerr_binaryop(:+::Symbol, %9::Int64, 1::Int64)::Union{}
    ◌  └───       unreachable
    ◌  3 ── %16 =   builtin Base.memoryrefoffset(%5)::Int64
    ◌  │    %17 = intrinsic Base.sub_int(%16, 1)::Int64
    ◌  │    %18 = intrinsic Base.Checked.checked_sadd_int(%17, %11)::Tuple{Int64, Bool}
    ◌  │    %19 =   builtin Base.Checked.getfield(%18, 1)::Int64
    ◌  │    %20 =   builtin Base.Checked.getfield(%18, 2)::Bool
    ◌  └───       goto #5 if not %20
    ✓  4 ──          invoke Base.Checked.throw_overflowerr_binaryop(:+::Symbol, %17::Int64, %11::Int64)::Union{}
    ◌  └───       unreachable
    ◌  5 ── %24 = intrinsic Base.slt_int(%7, %19)::Bool
    ◌  └───       goto #7 if not %24
    ◌  6 ──          invoke Base._growend_internal!(%3::Vector{Any}, 1::Int64, %9::Int64)::Nothing
    X  7 ┄─ %27 =   builtin Core.tuple(%11)::Tuple{Int64}
    ◌  │            builtin Base.setfield!(%3, :size, %27)::Tuple{Int64}
    ◌  └───       goto #8
    X  8 ── %30 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %31 =   builtin Base.getfield(%30, 1)::Int64
    X  │    %32 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %33 =   builtin Base.memoryrefnew(%32, %31, false)::MemoryRef{Any}
    X  │            builtin Base.memoryrefset!(%33, %4, :not_atomic, false)::Main.SafeRef{String}
    ◌  └───       goto #9
    ◌  9 ── %36 = $(Expr(:boundscheck, true))::Bool
    ◌  └───       goto #14 if not %36
    ◌  10 ─ %38 = intrinsic Base.sub_int(1, 1)::Int64
    ◌  │    %39 = intrinsic Base.bitcast(Base.UInt, %38)::UInt64
    X  │    %40 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %41 =   builtin Base.getfield(%40, 1)::Int64
    ◌  │    %42 = intrinsic Base.bitcast(Base.UInt, %41)::UInt64
    ◌  │    %43 = intrinsic Base.ult_int(%39, %42)::Bool
    ◌  └───       goto #12 if not %43
    ◌  11 ─       goto #13
    ✓  12 ─          invoke Base._throw_boundserror_indices(%3::Vector{Any}, 1::Int64)::Union{}
    ◌  └───       unreachable
    ◌  13 ─       nothing::Nothing
    X  14 ┄ %49 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %50 =   builtin Base.memoryrefnew(%49, 1, false)::MemoryRef{Any}
    X  │    %51 =   builtin Base.memoryrefget(%50, :not_atomic, false)::Any
    ◌  └───       goto #15
    X  15 ─ %53 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %54 =   builtin Base.getfield(%53, 1)::Int64
    ↑′ │    %55 =   builtin Core.tuple(%51, %54)::Tuple{Any, Int64}
    ◌  └───       return %55

In the above example `EscapeAnalysis` understands that `%20` and `%2` (corresponding to the allocated object `SafeRef(s)`) are aliased via the `arrayset`-`arrayref` chain and imposes `ReturnEscape` on them, but not impose it on the allocated array `%1` (corresponding to `ary`). `EscapeAnalysis` still imposes `ThrownEscape` on `ary` since it also needs to account for potential escapes via `BoundsError`, but also note that such unhandled `ThrownEscape` can often be ignored when optimizing the `ary` allocation.

Furthermore, in cases when array index information as well as array dimensions can be known precisely, `EscapeAnalysis` is able to even reason about "per-element" aliasing via `arrayref`-`arrayset` chain, as `EscapeAnalysis` does "per-field" alias analysis for objects:

    julia> code_escapes((String,String)) do s, t
               ary = Vector{Any}(undef, 2)
               ary[1] = SafeRef(s)
               ary[2] = SafeRef(t)
               return ary[1], length(ary)
           end#20(✓ s::String, ✓ t::String) in Main at REPL[1]:2
    X  1 ── %1  =   builtin Core.memorynew(Memory{Any}, 2)::Core.PartialStruct(Memory{Any}, Union{Nothing, Bool}[0, 0], Any[Core.Const(2), Ptr{Nothing}])
    X  │    %2  =   builtin Core.memoryrefnew(%1)::MemoryRef{Any}
    X  │    %3  = %new(Vector{Any}, %2, (2,))::Vector{Any}
    X  │    %4  = %new(Main.SafeRef{String}, _2)::Main.SafeRef{String}
    ◌  │    %5  = $(Expr(:boundscheck, true))::Bool
    ◌  └───       goto #6 if not %5
    ◌  2 ── %7  = intrinsic Base.sub_int(1, 1)::Int64
    ◌  │    %8  = intrinsic Base.bitcast(Base.UInt, %7)::UInt64
    X  │    %9  =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %10 =   builtin Base.getfield(%9, 1)::Int64
    ◌  │    %11 = intrinsic Base.bitcast(Base.UInt, %10)::UInt64
    ◌  │    %12 = intrinsic Base.ult_int(%8, %11)::Bool
    ◌  └───       goto #4 if not %12
    ◌  3 ──       goto #5
    ✓  4 ──          invoke Base._throw_boundserror_indices(%3::Vector{Any}, 1::Int64)::Union{}
    ◌  └───       unreachable
    ◌  5 ──       nothing::Nothing
    X  6 ┄─ %18 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %19 =   builtin Base.memoryrefnew(%18, 1, false)::MemoryRef{Any}
    X  │            builtin Base.memoryrefset!(%19, %4, :not_atomic, false)::Main.SafeRef{String}
    ◌  └───       goto #7
    X  7 ── %22 = %new(Main.SafeRef{String}, _3)::Main.SafeRef{String}
    ◌  │    %23 = $(Expr(:boundscheck, true))::Bool
    ◌  └───       goto #12 if not %23
    ◌  8 ── %25 = intrinsic Base.sub_int(2, 1)::Int64
    ◌  │    %26 = intrinsic Base.bitcast(Base.UInt, %25)::UInt64
    X  │    %27 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %28 =   builtin Base.getfield(%27, 1)::Int64
    ◌  │    %29 = intrinsic Base.bitcast(Base.UInt, %28)::UInt64
    ◌  │    %30 = intrinsic Base.ult_int(%26, %29)::Bool
    ◌  └───       goto #10 if not %30
    ◌  9 ──       goto #11
    ✓  10 ─          invoke Base._throw_boundserror_indices(%3::Vector{Any}, 2::Int64)::Union{}
    ◌  └───       unreachable
    ◌  11 ─       nothing::Nothing
    X  12 ┄ %36 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %37 =   builtin Base.memoryrefnew(%36, 2, false)::MemoryRef{Any}
    X  │            builtin Base.memoryrefset!(%37, %22, :not_atomic, false)::Main.SafeRef{String}
    ◌  └───       goto #13
    ◌  13 ─ %40 = $(Expr(:boundscheck, true))::Bool
    ◌  └───       goto #18 if not %40
    ◌  14 ─ %42 = intrinsic Base.sub_int(1, 1)::Int64
    ◌  │    %43 = intrinsic Base.bitcast(Base.UInt, %42)::UInt64
    X  │    %44 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %45 =   builtin Base.getfield(%44, 1)::Int64
    ◌  │    %46 = intrinsic Base.bitcast(Base.UInt, %45)::UInt64
    ◌  │    %47 = intrinsic Base.ult_int(%43, %46)::Bool
    ◌  └───       goto #16 if not %47
    ◌  15 ─       goto #17
    ✓  16 ─          invoke Base._throw_boundserror_indices(%3::Vector{Any}, 1::Int64)::Union{}
    ◌  └───       unreachable
    ◌  17 ─       nothing::Nothing
    X  18 ┄ %53 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %54 =   builtin Base.memoryrefnew(%53, 1, false)::MemoryRef{Any}
    X  │    %55 =   builtin Base.memoryrefget(%54, :not_atomic, false)::Any
    ◌  └───       goto #19
    X  19 ─ %57 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %58 =   builtin Base.getfield(%57, 1)::Int64
    ↑′ │    %59 =   builtin Core.tuple(%55, %58)::Tuple{Any, Int64}
    ◌  └───       return %59

Note that `ReturnEscape` is only imposed on `%2` (corresponding to `SafeRef(s)`) but not on `%4` (corresponding to `SafeRef(t)`). This is because the allocated array's dimension and indices involved with all `arrayref`/`arrayset` operations are available as constant information and `EscapeAnalysis` can understand that `%6` is aliased to `%2` but never be aliased to `%4`. In this kind of case, the succeeding optimization passes will be able to replace `Base.arrayref(true, %1, 1)::Any` with `%2` (a.k.a. "load-forwarding") and eventually eliminate the allocation of array `%1` entirely (a.k.a. "scalar-replacement").

When compared to object field analysis, where an access to object field can be analyzed trivially using type information derived by inference, array dimension isn't encoded as type information and so we need an additional analysis to derive that information. `EscapeAnalysis` at this moment first does an additional simple linear scan to analyze dimensions of allocated arrays before firing up the main analysis routine so that the succeeding escape analysis can precisely analyze operations on those arrays.

However, such precise "per-element" alias analysis is often hard. Essentially, the main difficulty inherit to array is that array dimension and index are often non-constant:

- loop often produces loop-variant, non-constant array indices

- (specific to vectors) array resizing changes array dimension and invalidates its constant-ness

Let's discuss those difficulties with concrete examples.

In the following example, `EscapeAnalysis` fails the precise alias analysis since the index at the `Base.arrayset(false, %4, %8, %6)::Vector{Any}` is not (trivially) constant. Especially `Any[nothing, nothing]` forms a loop and calls that `arrayset` operation in a loop, where `%6` is represented as a ϕ-node value (whose value is control-flow dependent). As a result, `ReturnEscape` ends up imposed on both `%23` (corresponding to `SafeRef(s)`) and `%25` (corresponding to `SafeRef(t)`), although ideally we want it to be imposed only on `%23` but not on `%25`:

    julia> code_escapes((String,String)) do s, t
               ary = Any[nothing, nothing]
               ary[1] = SafeRef(s)
               ary[2] = SafeRef(t)
               return ary[1], length(ary)
           end#23(✓ s::String, ✓ t::String) in Main at REPL[1]:2
    X  1 ── %1  =   builtin Core.memorynew(Memory{Any}, 2)::Memory{Any}
    X  │    %2  =   builtin Core.memoryrefnew(%1)::MemoryRef{Any}
    X  └─── %3  = %new(Vector{Any}, %2, (2,))::Vector{Any}
    ◌  2 ┄─ %4  = φ (#1 => 1, #6 => %14)::Int64
    ◌  │    %5  = φ (#1 => 1, #6 => %15)::Int64
    X  │    %6  =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %7  =   builtin Base.memoryrefnew(%6, %4, false)::MemoryRef{Any}
    ◌  │            builtin Base.memoryrefset!(%7, nothing, :not_atomic, false)::Nothing
    ◌  │    %9  =   builtin (%5 === 2)::Bool
    ◌  └───       goto #4 if not %9
    ◌  3 ──       goto #5
    ◌  4 ── %12 = intrinsic Base.add_int(%5, 1)::Int64
    ◌  └───       goto #5
    ◌  5 ┄─ %14 = φ (#4 => %12)::Int64
    ◌  │    %15 = φ (#4 => %12)::Int64
    ◌  │    %16 = φ (#3 => true, #4 => false)::Bool
    ◌  │    %17 = intrinsic Base.not_int(%16)::Bool
    ◌  └───       goto #7 if not %17
    ◌  6 ──       goto #2
    ◌  7 ──       goto #8
    X  8 ── %21 = %new(Main.SafeRef{String}, _2)::Main.SafeRef{String}
    ◌  │    %22 = $(Expr(:boundscheck, true))::Bool
    ◌  └───       goto #13 if not %22
    ◌  9 ── %24 = intrinsic Base.sub_int(1, 1)::Int64
    ◌  │    %25 = intrinsic Base.bitcast(Base.UInt, %24)::UInt64
    X  │    %26 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %27 =   builtin Base.getfield(%26, 1)::Int64
    ◌  │    %28 = intrinsic Base.bitcast(Base.UInt, %27)::UInt64
    ◌  │    %29 = intrinsic Base.ult_int(%25, %28)::Bool
    ◌  └───       goto #11 if not %29
    ◌  10 ─       goto #12
    ✓  11 ─          invoke Base._throw_boundserror_indices(%3::Vector{Any}, 1::Int64)::Union{}
    ◌  └───       unreachable
    ◌  12 ─       nothing::Nothing
    X  13 ┄ %35 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %36 =   builtin Base.memoryrefnew(%35, 1, false)::MemoryRef{Any}
    X  │            builtin Base.memoryrefset!(%36, %21, :not_atomic, false)::Main.SafeRef{String}
    ◌  └───       goto #14
    X  14 ─ %39 = %new(Main.SafeRef{String}, _3)::Main.SafeRef{String}
    ◌  │    %40 = $(Expr(:boundscheck, true))::Bool
    ◌  └───       goto #19 if not %40
    ◌  15 ─ %42 = intrinsic Base.sub_int(2, 1)::Int64
    ◌  │    %43 = intrinsic Base.bitcast(Base.UInt, %42)::UInt64
    X  │    %44 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %45 =   builtin Base.getfield(%44, 1)::Int64
    ◌  │    %46 = intrinsic Base.bitcast(Base.UInt, %45)::UInt64
    ◌  │    %47 = intrinsic Base.ult_int(%43, %46)::Bool
    ◌  └───       goto #17 if not %47
    ◌  16 ─       goto #18
    ✓  17 ─          invoke Base._throw_boundserror_indices(%3::Vector{Any}, 2::Int64)::Union{}
    ◌  └───       unreachable
    ◌  18 ─       nothing::Nothing
    X  19 ┄ %53 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %54 =   builtin Base.memoryrefnew(%53, 2, false)::MemoryRef{Any}
    X  │            builtin Base.memoryrefset!(%54, %39, :not_atomic, false)::Main.SafeRef{String}
    ◌  └───       goto #20
    ◌  20 ─ %57 = $(Expr(:boundscheck, true))::Bool
    ◌  └───       goto #25 if not %57
    ◌  21 ─ %59 = intrinsic Base.sub_int(1, 1)::Int64
    ◌  │    %60 = intrinsic Base.bitcast(Base.UInt, %59)::UInt64
    X  │    %61 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %62 =   builtin Base.getfield(%61, 1)::Int64
    ◌  │    %63 = intrinsic Base.bitcast(Base.UInt, %62)::UInt64
    ◌  │    %64 = intrinsic Base.ult_int(%60, %63)::Bool
    ◌  └───       goto #23 if not %64
    ◌  22 ─       goto #24
    ✓  23 ─          invoke Base._throw_boundserror_indices(%3::Vector{Any}, 1::Int64)::Union{}
    ◌  └───       unreachable
    ◌  24 ─       nothing::Nothing
    X  25 ┄ %70 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %71 =   builtin Base.memoryrefnew(%70, 1, false)::MemoryRef{Any}
    X  │    %72 =   builtin Base.memoryrefget(%71, :not_atomic, false)::Any
    ◌  └───       goto #26
    X  26 ─ %74 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %75 =   builtin Base.getfield(%74, 1)::Int64
    ↑′ │    %76 =   builtin Core.tuple(%72, %75)::Tuple{Any, Int64}
    ◌  └───       return %76

The next example illustrates how vector resizing makes precise alias analysis hard. The essential difficulty is that the dimension of allocated array `%1` is first initialized as `0`, but it changes by the two `:jl_array_grow_end` calls afterwards. `EscapeAnalysis` currently simply gives up precise alias analysis whenever it encounters any array resizing operations and so `ReturnEscape` is imposed on both `%2` (corresponding to `SafeRef(s)`) and `%20` (corresponding to `SafeRef(t)`):

    julia> code_escapes((String,String)) do s, t
               ary = Any[]
               push!(ary, SafeRef(s))
               push!(ary, SafeRef(t))
               ary[1], length(ary)
           end#26(✓ s::String, ✓ t::String) in Main at REPL[1]:2
    X  1 ── %1  =   builtin Core.memorynew(Memory{Any}, 0)::Memory{Any}
    X  │    %2  =   builtin Core.memoryrefnew(%1)::MemoryRef{Any}
    X  │    %3  = %new(Vector{Any}, %2, (0,))::Vector{Any}
    X  │    %4  = %new(Main.SafeRef{String}, _2)::Main.SafeRef{String}
    X  │    %5  =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %6  =   builtin Base.getfield(%5, :mem)::Memory{Any}
    X  │    %7  =   builtin Base.getfield(%6, :length)::Int64
    X  │    %8  =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %9  =   builtin Base.getfield(%8, 1)::Int64
    ◌  │    %10 = intrinsic Base.Checked.checked_sadd_int(%9, 1)::Tuple{Int64, Bool}
    ◌  │    %11 =   builtin Base.Checked.getfield(%10, 1)::Int64
    ◌  │    %12 =   builtin Base.Checked.getfield(%10, 2)::Bool
    ◌  └───       goto #3 if not %12
    ✓  2 ──          invoke Base.Checked.throw_overflowerr_binaryop(:+::Symbol, %9::Int64, 1::Int64)::Union{}
    ◌  └───       unreachable
    ◌  3 ── %16 =   builtin Base.memoryrefoffset(%5)::Int64
    ◌  │    %17 = intrinsic Base.sub_int(%16, 1)::Int64
    ◌  │    %18 = intrinsic Base.Checked.checked_sadd_int(%17, %11)::Tuple{Int64, Bool}
    ◌  │    %19 =   builtin Base.Checked.getfield(%18, 1)::Int64
    ◌  │    %20 =   builtin Base.Checked.getfield(%18, 2)::Bool
    ◌  └───       goto #5 if not %20
    ✓  4 ──          invoke Base.Checked.throw_overflowerr_binaryop(:+::Symbol, %17::Int64, %11::Int64)::Union{}
    ◌  └───       unreachable
    ◌  5 ── %24 = intrinsic Base.slt_int(%7, %19)::Bool
    ◌  └───       goto #7 if not %24
    ◌  6 ──          invoke Base._growend_internal!(%3::Vector{Any}, 1::Int64, %9::Int64)::Nothing
    X  7 ┄─ %27 =   builtin Core.tuple(%11)::Tuple{Int64}
    ◌  │            builtin Base.setfield!(%3, :size, %27)::Tuple{Int64}
    ◌  └───       goto #8
    X  8 ── %30 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %31 =   builtin Base.getfield(%30, 1)::Int64
    X  │    %32 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %33 =   builtin Base.memoryrefnew(%32, %31, false)::MemoryRef{Any}
    X  │            builtin Base.memoryrefset!(%33, %4, :not_atomic, false)::Main.SafeRef{String}
    ◌  └───       goto #9
    X  9 ── %36 = %new(Main.SafeRef{String}, _3)::Main.SafeRef{String}
    X  │    %37 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %38 =   builtin Base.getfield(%37, :mem)::Memory{Any}
    X  │    %39 =   builtin Base.getfield(%38, :length)::Int64
    X  │    %40 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %41 =   builtin Base.getfield(%40, 1)::Int64
    ◌  │    %42 = intrinsic Base.Checked.checked_sadd_int(%41, 1)::Tuple{Int64, Bool}
    ◌  │    %43 =   builtin Base.Checked.getfield(%42, 1)::Int64
    ◌  │    %44 =   builtin Base.Checked.getfield(%42, 2)::Bool
    ◌  └───       goto #11 if not %44
    ✓  10 ─          invoke Base.Checked.throw_overflowerr_binaryop(:+::Symbol, %41::Int64, 1::Int64)::Union{}
    ◌  └───       unreachable
    ◌  11 ─ %48 =   builtin Base.memoryrefoffset(%37)::Int64
    ◌  │    %49 = intrinsic Base.sub_int(%48, 1)::Int64
    ◌  │    %50 = intrinsic Base.Checked.checked_sadd_int(%49, %43)::Tuple{Int64, Bool}
    ◌  │    %51 =   builtin Base.Checked.getfield(%50, 1)::Int64
    ◌  │    %52 =   builtin Base.Checked.getfield(%50, 2)::Bool
    ◌  └───       goto #13 if not %52
    ✓  12 ─          invoke Base.Checked.throw_overflowerr_binaryop(:+::Symbol, %49::Int64, %43::Int64)::Union{}
    ◌  └───       unreachable
    ◌  13 ─ %56 = intrinsic Base.slt_int(%39, %51)::Bool
    ◌  └───       goto #15 if not %56
    ◌  14 ─          invoke Base._growend_internal!(%3::Vector{Any}, 1::Int64, %41::Int64)::Nothing
    X  15 ┄ %59 =   builtin Core.tuple(%43)::Tuple{Int64}
    ◌  │            builtin Base.setfield!(%3, :size, %59)::Tuple{Int64}
    ◌  └───       goto #16
    X  16 ─ %62 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %63 =   builtin Base.getfield(%62, 1)::Int64
    X  │    %64 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %65 =   builtin Base.memoryrefnew(%64, %63, false)::MemoryRef{Any}
    X  │            builtin Base.memoryrefset!(%65, %36, :not_atomic, false)::Main.SafeRef{String}
    ◌  └───       goto #17
    ◌  17 ─ %68 = $(Expr(:boundscheck, true))::Bool
    ◌  └───       goto #22 if not %68
    ◌  18 ─ %70 = intrinsic Base.sub_int(1, 1)::Int64
    ◌  │    %71 = intrinsic Base.bitcast(Base.UInt, %70)::UInt64
    X  │    %72 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %73 =   builtin Base.getfield(%72, 1)::Int64
    ◌  │    %74 = intrinsic Base.bitcast(Base.UInt, %73)::UInt64
    ◌  │    %75 = intrinsic Base.ult_int(%71, %74)::Bool
    ◌  └───       goto #20 if not %75
    ◌  19 ─       goto #21
    ✓  20 ─          invoke Base._throw_boundserror_indices(%3::Vector{Any}, 1::Int64)::Union{}
    ◌  └───       unreachable
    ◌  21 ─       nothing::Nothing
    X  22 ┄ %81 =   builtin Base.getfield(%3, :ref)::MemoryRef{Any}
    X  │    %82 =   builtin Base.memoryrefnew(%81, 1, false)::MemoryRef{Any}
    X  │    %83 =   builtin Base.memoryrefget(%82, :not_atomic, false)::Any
    ◌  └───       goto #23
    X  23 ─ %85 =   builtin Base.getfield(%3, :size)::Tuple{Int64}
    X  │    %86 =   builtin Base.getfield(%85, 1)::Int64
    ↑′ │    %87 =   builtin Core.tuple(%83, %86)::Tuple{Any, Int64}
    ◌  └───       return %87

In order to address these difficulties, we need inference to be aware of array dimensions and propagate array dimensions in a flow-sensitive way[ArrayDimension], as well as come up with nice representation of loop-variant values.

`EscapeAnalysis` at this moment quickly switches to the more imprecise analysis that doesn't track precise index information in cases when array dimensions or indices are trivially non constant. The switch can naturally be implemented as a lattice join operation of `EscapeInfo.AliasInfo` property in the data-flow analysis framework.

### Exception Handling

It would be also worth noting how `EscapeAnalysis` handles possible escapes via exceptions. Naively it seems enough to propagate escape information imposed on `:the_exception` object to all values that may be thrown in a corresponding `try` block. But there are actually several other ways to access to the exception object in Julia, such as `Base.current_exceptions` and `rethrow`. For example, escape analysis needs to account for potential escape of `r` in the example below:

    julia> const GR = Ref{Any}();
    julia> @noinline function rethrow_escape!()
               try
                   rethrow()
               catch err
                   GR[] = err
               end
           end;
    julia> get′(x) = isassigned(x) ? x[] : throw(x);
    julia> code_escapes() do
               r = Ref{String}()
               local t
               try
                   t = get′(r)
               catch err
                   t = typeof(err)   # `err` (which `r` aliases to) doesn't escape here
                   rethrow_escape!() # but `r` escapes here
               end
               return t
           end#29() in Main at REPL[4]:2
    X  1 ─ %1  = %new(Base.RefValue{String})::Base.RefValue{String}
    ◌  2 ─ %2  = enter #8
    ◌  3 ─ %3  =   builtin Base.isdefined(%1, :x)::Bool
    ◌  └──       goto #5 if not %3
    X  4 ─ %5  =   builtin Base.getfield(%1, :x)::String
    ◌  └──       goto #6
    ◌  5 ─         builtin Main.throw(%1)::Union{}
    ◌  └──       unreachable
    ◌  6 ─       $(Expr(:leave, :(%2)))
    ◌  7 ─       goto #9
    ✓  8 ┄          invoke Main.rethrow_escape!()::Any
    ◌  └──       $(Expr(:pop_exception, :(%2)))::Core.Const(nothing)
    X  9 ┄ %13 = φ (#7 => %5, #8 => Base.RefValue{String})::Union{String, Core.TypeEgal{Base.RefValue{String}}}
    ◌  └──       return %13

It requires a global analysis in order to correctly reason about all possible escapes via existing exception interfaces. For now we always propagate the topmost escape information to all potentially thrown objects conservatively, since such an additional analysis might not be worthwhile to do given that exception handling and error path usually don't need to be very performance sensitive, and also optimizations of error paths might be very ineffective anyway since they are often even "unoptimized" intentionally for latency reasons.

`x::EscapeInfo`'s `x.ThrownEscape` property records SSA statements where `x` can be thrown as an exception. Using this information `EscapeAnalysis` can propagate possible escapes via exceptions limitedly to only those may be thrown in each `try` region:

    julia> result = code_escapes((String,String)) do s1, s2
               r1 = Ref(s1)
               r2 = Ref(s2)
               local ret
               try
                   s1 = get′(r1)
                   ret = sizeof(s1)
               catch err
                   global GV = err # will definitely escape `r1`
               end
               s2 = get′(r2)       # still `r2` doesn't escape fully
               return s2
           end#32(✓ s1::String, ✓ s2::String) in Main at REPL[1]:2
    ◌  1 ─     nothing::Nothing
    ◌  2 ─     nothing::Nothing
    ◌  3 ─     nothing::Nothing
    ◌  4 ─     nothing::Nothing
    ◌  5 ─     return _3

## Analysis Usage

`analyze_escapes` is the entry point to analyze escape information of SSA-IR elements.

Most optimizations like SROA (`sroa_pass!`) are more effective when applied to an optimized source that the inlining pass (`ssa_inlining_pass!`) has simplified by resolving inter-procedural calls and expanding callee sources. Accordingly, `analyze_escapes` is also able to analyze post-inlining IR and collect escape information that is useful for certain memory-related optimizations.

However, since certain optimization passes like inlining can change control flows and eliminate dead code, they can break the inter-procedural validity of escape information. In particular, in order to collect inter-procedurally valid escape information, we need to analyze a pre-inlining IR.

Because of this reason, `analyze_escapes` can analyze `IRCode` at any Julia-level optimization stage, and especially, it is supposed to be used at the following two stages:

- `IPO EA`: analyze pre-inlining IR to generate IPO-valid escape information cache

- `Local EA`: analyze post-inlining IR to collect locally-valid escape information

Escape information derived by `IPO EA` is transformed to the `ArgEscapeCache` data structure and cached globally. By passing an appropriate `get_escape_cache` callback to `analyze_escapes`, the escape analysis can improve analysis accuracy by utilizing cached inter-procedural information of non-inlined callees that has been derived by previous `IPO EA`. More interestingly, it is also valid to use `IPO EA` escape information for type inference, e.g., inference accuracy can be improved by forming `Const`/`PartialStruct`/`MustAlias` of mutable object.

`Base.Compiler.EscapeAnalysis.analyze_escapes` — Function

    analyze_escapes(ir::IRCode, nargs::Int, get_escape_cache) -> estate::EscapeState

Analyzes escape information in `ir`:

- `nargs`: the number of actual arguments of the analyzed call
- `get_escape_cache(::MethodInstance) -> Union{Bool,ArgEscapeCache}`: retrieves cached argument escape information

source
`Base.Compiler.EscapeAnalysis.EscapeState` — Type

    estate::EscapeState

Extended lattice that maps arguments and SSA values to escape information represented as `EscapeInfo`. Escape information imposed on SSA IR element `x` can be retrieved by `estate[x]`.

source
`Base.Compiler.EscapeAnalysis.EscapeInfo` — Type

    x::EscapeInfo

A lattice for escape information, which holds the following properties:

- `x.Analyzed::Bool`: not formally part of the lattice, only indicates whether `x` has been analyzed

- `x.ReturnEscape::Bool`: indicates `x` can escape to the caller via return

- `x.ThrownEscape::BitSet`: records SSA statement numbers where `x` can be thrown as exception:

  - `isempty(x.ThrownEscape)`: `x` will never be thrown in this call frame (the bottom)
  - `pc ∈ x.ThrownEscape`: `x` may be thrown at the SSA statement at `pc`
  - `-1 ∈ x.ThrownEscape`: `x` may be thrown at arbitrary points of this call frame (the top)

  This information will be used by `escape_exception!` to propagate potential escapes via exception.

- `x.AliasInfo::Union{Bool,IndexableFields,Unindexable}`: maintains all possible values that can be aliased to fields or array elements of `x`:

  - `x.AliasInfo === false` indicates the fields/elements of `x` aren't analyzed yet
  - `x.AliasInfo === true` indicates the fields/elements of `x` can't be analyzed, e.g. the type of `x` is not known or is not concrete and thus its fields/elements can't be known precisely
  - `x.AliasInfo::IndexableFields` records all the possible values that can be aliased to fields of object `x` with precise index information
  - `x.AliasInfo::Unindexable` records all the possible values that can be aliased to fields/elements of `x` without precise index information

- `x.Liveness::BitSet`: records SSA statement numbers where `x` should be live, e.g. to be used as a call argument, to be returned to a caller, or preserved for `:foreigncall`:

  - `isempty(x.Liveness)`: `x` is never used in this call frame (the bottom)

  - `0 ∈ x.Liveness` also has the special meaning that it's a call argument of the currently analyzed call frame (and thus it's visible from the caller immediately).

  - `pc ∈ x.Liveness`: `x` may be used at the SSA statement at `pc`

  - `-1 ∈ x.Liveness`: `x` may be used at arbitrary points of this call frame (the top)

There are utility constructors to create common `EscapeInfo`s, e.g.,

- `NoEscape()`: the bottom(-like) element of this lattice, meaning it won't escape to anywhere

- `AllEscape()`: the topmost element of this lattice, meaning it will escape to everywhere

`analyze_escapes` will transition these elements from the bottom to the top, in the same direction as Julia's native type inference routine. An abstract state will be initialized with the bottom(-like) elements:

- the call arguments are initialized as `ArgEscape()`, whose `Liveness` property includes `0` to indicate that it is passed as a call argument and visible from a caller immediately
- the other states are initialized as `NotAnalyzed()`, which is a special lattice element that is slightly lower than `NoEscape`, but at the same time doesn't represent any meaning other than it's not analyzed yet (thus it's not formally part of the lattice)

source

- LatticeDesignOur type inference implementation takes the alternative approach, where each lattice property is represented by a special lattice element type object. It turns out that it started to complicate implementations of the lattice operations mainly because it often requires conversion rules between each lattice element type object. And we are working on overhauling our type inference lattice implementation with `EscapeInfo`-like lattice design.
- MM02 A Graph-Free approach to Data-Flow Analysis. Markas Mohnen, 2002, April. https://api.semanticscholar.org/CorpusID:28519618.
- BackandForthOur type inference algorithm in contrast is implemented as a forward analysis, because type information usually flows from "definition" to "usage" and it is more natural and effective to propagate such information in a forward way.
- DynamismIn some cases, however, object fields can't be analyzed precisely. For example, object may escape to somewhere `EscapeAnalysis` can't account for possible memory effects on it, or fields of the objects simply can't be known because of the lack of type information. In such cases `AliasInfo` property is raised to the topmost element within its own lattice order, and it causes succeeding field analysis to be conservative and escape information imposed on fields of an unanalyzable object to be propagated to the object itself.
- JVM05 Escape Analysis in the Context of Dynamic Compilation and Deoptimization. Thomas Kotzmann and Hanspeter Mössenböck, 2005, June. https://dl.acm.org/doi/10.1145/1064979.1064996.
- ArrayDimensionOtherwise we will need yet another forward data-flow analysis on top of the escape analysis.

Theme

This document was generated with Documenter.jl version 1.17.0 on Saturday 29 August 2026. Using Julia version 1.14.0-DEV.3067.