Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
propagate escapes via exception (#72)
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