Skip to content

Commit

Permalink
propagate escapes via exception (#72)
Browse files Browse the repository at this point in the history
This PR revives and replaces #39.
`EscapeLattice.ThrownEscape` is tracking where the object can be thrown,
but the analysis didn't impose potential escapes via exceptions using
that information. When using the analysis result, we previously needed
to be very conservative about `ThrownEscape` since it essentially needed
to be handled same as `ReturnEscape`. Still we can often ignore
`ThrownEscape` when implementing certain optimizations like `mutating_arrayfreeze`
optimization if the analysis accounts for potential escapes via exception.

This commit does propagate escape information imposed on exception object
to all objects that can potentially be thrown in current `try` region.
After this PR, the throwness of an object will be categorized as either of:
1. potential throw will be handled by a local `catch` handler:
   `analyze_escapes` will propagated possible escapes via exception to
   the object in question
2. potential throw won't be handled within a local frame: this indicates
   the object can be thrown and escaped to somewhere in caller frames
   (this case often can be ignored for implementing some optimizations)

The new escape routine `escape_exception!` propagates possible escapes via
exceptions. Naively it seems enough to propagate escape information
imposed on `:the_exception` object, but actually there are several other
ways to access to the exception object such as `Base.current_exceptions`
and manual catch of `rethrow`n object. For example, the escape analysis
needs to account for potential escape of the allocated object via
`rethrow_escape!()` call in the example below:
```julia
const Gx = Ref{Any}()
@noinline function rethrow_escape!()
    try
        rethrow()
    catch err
        Gx[] = err
    end
end
unsafeget(x) = isassigned(x) ? x[] : throw(x)

code_escapes() do
    r = Ref{String}()
    try
        t = unsafeget(r)
    catch err
        t = typeof(err)  # `err` (which `r` may alias to) doesn't escape here
        rethrow_escape!() # `r` can escape here
    end
    return t
end
```

As indicated by the above example, it requires a global analysis in
addition to a base escape analysis to reason about all possible escapes
via existing exception interfaces correctly. For now we conservatively
always propagate `AllEscape` to all potentially thrown objects, 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 optimizations of error paths might be very
ineffective anyway since they are sometimes "unoptimized" intentionally
for latency reasons.

Specifically, `analyze_escapes` now first invokes `compute_tryregions`
that does a linear scan to find regions where potential `throw`s can be
caught (they are simply statements between `:enter` and `:leave` expressions).
`escape_exception!` is invoked after each statement iteration and will
propagate `AllEscape` to all SSA values and arguments whose `ThrownEscape`
intersects with any of `tryregions`.
In order to keep the analysis accuracy, now `ReturnEscape` and `ThrownEscape`
doesn't share the same program counter set (i.e. `EscapeSites`), and they
have each own `BitSet` program counter set.

Now possible escapes via exceptions are propagated limitedly to those may
be thrown in each `try` region:
```julia
let # sequential: escape information imposed on `err1` and `err2 should propagate separately
    result = analyze_escapes() do
        r1 = Ref{String}()
        r2 = Ref{String}()
        local ret
        try
            s1 = unsafeget(r1)
            ret = sizeof(s1)
        catch err
            global g = err # will escape `r1`
        end
        s2 = unsafeget(r2) # `r2` doesn't escape fully
        return s2, r2
    end
    is = findall(isnew, result.ir.stmts.inst)
    @test length(is) == 2
    i1, i2 = is
    r = only(findall(isreturn, result.ir.stmts.inst))
    @test has_all_escape(result.state[SSAValue(i1)])
    @test !has_all_escape(result.state[SSAValue(i2)])
    @test has_return_escape(result.state[SSAValue(i2)], r)
end
```
  • Loading branch information
aviatesk authored Jan 13, 2022
1 parent dd0ec5b commit 5daf11e
Show file tree
Hide file tree
Showing 4 changed files with 957 additions and 253 deletions.
Loading

0 comments on commit 5daf11e

Please sign in to comment.