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

add flow-insensitive, local field / alias analysis #54

Merged
merged 9 commits into from
Dec 25, 2021

Conversation

aviatesk
Copy link
Owner

@aviatesk aviatesk commented Nov 15, 2021

EDIT: rather than reading the description below, please see newly added README as well as #54 (comment).


This commit implements a simple, flow-insensitive alias analysis using an
approach inspired by the escape analysis algorithm explained in
the old JVM paper 1.

EscapeLattice is extended so that it also keeps track of possible field values.
In more detail, x::EscapeLattice has the new field called
x.FieldSet::Union{Vector{IdSet{Any}},Bool}, where:

  • x.FieldSets === false indicates the fields of x isn't analyzed yet
  • x.FieldSets === true indicates the fields of x can't be analyzed,
    e.g. the type of x is not concrete and thus the number of its fields
    can't known precisely
  • otherwise x.FieldSets::Vector{IdSet{Any}} holds all the possible
    values of each field, where x.FieldSets[i] keeps all possibilities
    that the ith field can be

And now, in addition to managing escape lattice elements, the analysis
state also maintains an "alias set" state.aliasset::IntDisjointSet{Int},
which is implemented as a disjoint set of aliased arguments and SSA statements.
When the fields of object x are known precisely (i.e. x.FieldSets isa Vector{IdSet{Any}} holds),
the alias set is updated each time z = getfield(x, y) is encountered in a way that z is
aliased to all values of x.FieldSets[y], so that escape information imposed on z will be
propagated to all the aliased values and z can be replaced with an aliased value later.
Note that in a case when the fields of object x can't known precisely (i.e. x.FieldSets is true),
when z = getfield(x, y) is analyzed, escape information of z is propagated to x rather
than any of x's fields, which is the most conservative propagation since escape information
imposed on x will end up being propagated to all of its fields anyway at definitions of x
(i.e. :new expression or setfield! call).

Now this alias analysis should allow us to implement a "stronger" SROA,
which eliminates the allocation of r within the following code:

julia> result = analyze_escapes((String,)) do s
           r = Ref(s)
           broadcast(identity, r)
       end
#3(_2::String *, _3::Base.RefValue{String} ◌) in Main at REPL[2]:2
2  1%1 = %new(Base.RefValue{String}, _2)::Base.RefValue{String}                                                                                                                                        │╻╷╷     Ref
3 ✓ │   %2 = Core.tuple(%1)::Tuple{Base.RefValue{String}}                                                                                                                                                  │╻       broadcast
  %3 = Core.getfield(%2, 1)::Base.RefValue{String}                                                                                                                                                   ││
  ◌ └──      goto #3 if not true                                                                                                                                                                           ││╻╷      materialize2nothing::Nothing* 3%6 = Base.getfield(%3, :x)::String                                                                                                                                                                 │││╻╷╷╷╷   copy
  ◌ └──      goto #4                                                                                                                                                                                       ││││┃       getindex4 ─      goto #5                                                                                                                                                                                       ││││5 ─      goto #6                                                                                                                                                                                       │││6 ─      goto #7                                                                                                                                                                                       ││7return %6                                                                                                                                                                                     │

julia> EscapeAnalysis.get_aliases(result.state.aliasset, Core.SSAValue(6), result.ir)
2-element Vector{Union{Core.Argument, Core.SSAValue}}:
 Core.Argument(2)
 :(%6)

Note that the allocation %1 isn't analyzed as ReturnEscape, still _2 is analyzed so.


Remaining TODOs:

  • inter-procedural conversion (done with the most conservative option, i.e. disables field/alias analysis immediately once it's passed to a callee)
  • think of how to make it flow-sensitive and allow partial SROA (with allocation movement)

Footnotes

  1. 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.

@aviatesk aviatesk mentioned this pull request Nov 15, 2021
5 tasks
@aviatesk aviatesk force-pushed the avi/aliasanalysis branch 4 times, most recently from 4212ad7 to 1b207be Compare November 15, 2021 14:19
@aviatesk
Copy link
Owner Author

The JET test failures are all attributed to Base definitions, and they should be fixed by JuliaLang/julia#43087.

@aviatesk aviatesk force-pushed the avi/aliasanalysis branch 3 times, most recently from 1645d64 to 85aa8be Compare November 15, 2021 16:16
@aviatesk aviatesk closed this Nov 16, 2021
@aviatesk aviatesk reopened this Nov 16, 2021
@codecov-commenter
Copy link

codecov-commenter commented Nov 16, 2021

Codecov Report

Merging #54 (41d1e02) into master (813163f) will increase coverage by 4.80%.
The diff coverage is 81.68%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #54      +/-   ##
==========================================
+ Coverage   71.19%   75.99%   +4.80%     
==========================================
  Files           2        3       +1     
  Lines         361      604     +243     
==========================================
+ Hits          257      459     +202     
- Misses        104      145      +41     
Impacted Files Coverage Δ
src/utils.jl 29.68% <0.00%> (-0.07%) ⬇️
src/disjoint_set.jl 68.18% <68.18%> (ø)
src/EscapeAnalysis.jl 90.50% <88.97%> (-1.58%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 813163f...41d1e02. Read the comment docs.

@aviatesk aviatesk force-pushed the avi/aliasanalysis branch 2 times, most recently from b3b0e20 to e0017a3 Compare November 16, 2021 08:23
@aviatesk aviatesk force-pushed the avi/aliasanalysis branch 3 times, most recently from e27cf0b to d6d6d85 Compare November 16, 2021 13:18
@aviatesk

This comment has been minimized.

@aviatesk aviatesk force-pushed the avi/aliasanalysis branch 8 times, most recently from d1763f6 to e2046b9 Compare November 19, 2021 09:40
@@ -227,6 +274,14 @@ can_elide_finalizer(x::EscapeLattice, pc::Int) =
# we need to make sure this `==` operator corresponds to lattice equality rather than object equality,
# otherwise `propagate_changes` can't detect the convergence
x::EscapeLattice == y::EscapeLattice = begin
xf, yf = x.FieldSets, y.FieldSets
if isa(xf, Bool)
isa(yf, Bool) || return false
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
isa(yf, Bool) || return false

@aviatesk aviatesk force-pushed the avi/aliasanalysis branch 2 times, most recently from 71e2373 to abac396 Compare November 24, 2021 15:33
@ianatol ianatol self-requested a review December 21, 2021 00:14
src/EscapeAnalysis.jl Outdated Show resolved Hide resolved
"""
struct EscapeState
arguments::Vector{EscapeLattice}
ssavalues::Vector{EscapeLattice}
aliasset::IntDisjointSet{Int}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't quite understand aliasset, or rather, the difference between the information stored in aliasset and that stored in FieldSets. Do we use aliasset for performance reasons? IUIC we could (albeit with probably slow performance) write get_aliases(state::EscapeState, x, ir) by searching for x's value within the FieldSets of state.arguments and/or state.ssavalues?

Copy link
Owner Author

@aviatesk aviatesk Dec 23, 2021

Choose a reason for hiding this comment

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

I think I introduced aliasset so that we can track a case like below:

analyze_escapes((String,)) do a # => ReturnEscape
    o1  = Ref(a)                # => NoEscape
    o2  = Ref(o1)               # => NoEscape
    o1′ = o2[]                  # (aliased to `o1`)
    a′  = o1′[]                 # (needs to propagate ReturnEscape to `a`)
    return a′
end

That is, FieldSets tracks values of a field that are known from Expr(:new, ...) or setfield! call, but in order to handle a case where escape information propagates through values that are aliased by getfield chain, we need some external set that holds all the possible values that can be aliased to the field.

Having said that, now I think the design of FieldSets is wrong.
I made FieldSets tracks actual values (i.e. SSAValue/Argument) and escape information is propagated via those values (and also via aliased values by looking at aliasset), just because I had SROA in my mind as a downstream optimization so that it can directly use that information of field values.
But the important observation here is that field information is something like type information, and it is something better propagated in a forward direction. This PR tries to analyze field information within a backward analysis, but it leads a complex and inefficient (and likely incomplete) data flow.
It may be better not to include a design that is specific downstream optimization in mind (like SROA), and I think we can have a better implementation by making FieldSets somewhat like aliasset, and just trying to propagate escape information without holding field values.

@aviatesk aviatesk force-pushed the avi/aliasanalysis branch 3 times, most recently from 7661c3a to c008837 Compare December 24, 2021 19:13
@aviatesk
Copy link
Owner Author

aviatesk commented Dec 24, 2021

Okay, I think I've finished this PR.

I hope the updated README well describes the general algorithm of EA, and key ideas of newly introduced field and alias analyses.

As for aliasset, I finally conclude that we want to have something like that in order to incorporate a sort of forward information propagation into our backward analysis. I described the insight on this in the README, so I'd like to show some experiment here.

As @ianatol pointed, it is possible to propagate escape information to aliased values when encountering new aliasing, but it turns out less efficient than maintaining external aliasset-like data structure.
If we propagate escape information on aliasing (5ea6139), we get this excessive iteration counts by running our test suite (N.B. the minimum # of iteration is 2 in our implementation)

comment in these lines to get these numbers

[EA]: excessive iteration count found 3 (Base._str_sizehint)
[EA]: excessive iteration count found 4 (Base.var"#open_flags#362")
[EA]: excessive iteration count found 4 (Base.var"#open_flags#362")
[EA]: excessive iteration count found 4 (Base.var"#open_flags#362")
[EA]: excessive iteration count found 4 (Base.print)
[EA]: excessive iteration count found 3 (Base.print_to_string)
[EA]: excessive iteration count found 4 (Base.convert)
[EA]: excessive iteration count found 5 (Main.var"#21")
[EA]: excessive iteration count found 3 (Main.var"#26")
[EA]: excessive iteration count found 3 (Base.print_to_string)
[EA]: excessive iteration count found 3 (Base.getproperty)
[EA]: excessive iteration count found 3 (Base.getproperty)
[EA]: excessive iteration count found 3 (Base._trylock)
[EA]: excessive iteration count found 4 (Base.list_deletefirst!)
[EA]: excessive iteration count found 4 (Base.popfirst!)
[EA]: excessive iteration count found 7 (Base.popfirst!)
[EA]: excessive iteration count found 5 (Base.poptask)
[EA]: excessive iteration count found 3 (Base.pushfirst!)
[EA]: excessive iteration count found 3 (Base.pushfirst!)
[EA]: excessive iteration count found 7 (Base.list_deletefirst!)
[EA]: excessive iteration count found 3 (Base.ensure_rescheduled)
[EA]: excessive iteration count found 3 (Base.list_deletefirst!)
[EA]: excessive iteration count found 3 (Base.list_deletefirst!)
[EA]: excessive iteration count found 3 (Base.list_deletefirst!)
[EA]: excessive iteration count found 3 (Base.list_deletefirst!)
[EA]: excessive iteration count found 4 (Base.wait)
[EA]: excessive iteration count found 4 (Base.slowlock)
[EA]: excessive iteration count found 3 (Base.getproperty)
[EA]: excessive iteration count found 3 (Base.push!)
[EA]: excessive iteration count found 6 (Base.notify)
[EA]: excessive iteration count found 3 (Base.notifywaiters)
[EA]: excessive iteration count found 3 (Base.print)
[EA]: excessive iteration count found 3 (Main.var"#35")
[EA]: excessive iteration count found 3 (Main.var"#49")
[EA]: excessive iteration count found 3 (Main.var"#50")
[EA]: excessive iteration count found 3 (anonymous.var"#1")
[EA]: excessive iteration count found 3 (Main.var"#60")
[EA]: excessive iteration count found 4 (Main.var"#63")
[EA]: excessive iteration count found 4 (Main.var"#154")
[EA]: excessive iteration count found 3 (Main.var"#64")
[EA]: excessive iteration count found 3 (Main.var"#65")
[EA]: excessive iteration count found 4 (Main.var"#66")
[EA]: excessive iteration count found 3 (Main.var"#76")

On the other hand, if we equalize escape information between aliased values (c008837) with an external aliasset, the number of excessive iterations is reduced as below:

comment in these lines

[EA]: excessive iteration count 3 (Base.print_to_string)
[EA]: excessive iteration count 3 (Base._trylock)
[EA]: excessive iteration count 5 (Base.list_deletefirst!)
[EA]: excessive iteration count 5 (Base.popfirst!)
[EA]: excessive iteration count 4 (Base.popfirst!)
[EA]: excessive iteration count 3 (Base.pushfirst!)
[EA]: excessive iteration count 3 (Base.pushfirst!)
[EA]: excessive iteration count 4 (Base.list_deletefirst!)
[EA]: excessive iteration count 3 (Base.list_deletefirst!)
[EA]: excessive iteration count 3 (Base.list_deletefirst!)
[EA]: excessive iteration count 4 (Base.notify)
[EA]: excessive iteration count 3 (Base.print)
[EA]: excessive iteration count 3 (Main.var"#49")
[EA]: excessive iteration count 3 (Main.var"#50")
[EA]: excessive iteration count 3 (anonymous.var"#1")
[EA]: excessive iteration count 3 (Main.var"#76")

The key observation is that aliasing happens at "definition" site, and thus is somewhat better to be propagated in forwardly.
It would be less efficient to naively incorporate such forward-ish information in our backward analysis, but it can be done more effectively if we use aliasset-like idea, which allows escape information between newly aliased values to be equalized efficiently.

@aviatesk aviatesk requested review from vtjnash and ianatol December 24, 2021 19:31
@aviatesk aviatesk force-pushed the avi/aliasanalysis branch 2 times, most recently from 8f1a6aa to ef58547 Compare December 25, 2021 05:14
This commit implements a simple, flow-insensitive alias analysis using
an
approach inspired by the escape analysis algorithm explained in the old
JVM paper [^JVM05].

`EscapeLattice` is extended so that it also keeps track of possible
field values.
In more detail, `x::EscapeLattice` has the new field called
`x.FieldSet::Union{Vector{IdSet{Any}},Bool}`, where:
- `x.FieldSets === false` indicates the fields of `x` isn't analyzed yet
- `x.FieldSets === true` indicates the fields of `x` can't be analyzed,
  e.g. the type of `x` is not concrete and thus the number of its fields
  can't known precisely
- otherwise `x.FieldSets::Vector{IdSet{Any}}` holds all the possible
  values of each field, where `x.FieldSets[i]` keeps all possibilities
  that the `i`th field can be

And now, in addition to managing escape lattice elements, the analysis
state also maintains an "alias set"
`state.aliasset::IntDisjointSet{Int}`,
which is implemented as a disjoint set of aliased arguments and SSA
statements.
When the fields of object `x` are known precisely (i.e. `x.FieldSets isa
Vector{IdSet{Any}}` holds),
the alias set is updated each time `z = getfield(x, y)` is encountered
in a way that `z` is
aliased to all values of `x.FieldSets[y]`, so that escape information
imposed on `z` will be
propagated to all the aliased values and `z` can be replaced with an
aliased value later.
Note that in a case when the fields of object `x` can't known precisely
(i.e. `x.FieldSets` is `true`),
when `z = getfield(x, y)` is analyzed, escape information of `z` is
propagated to `x` rather
than any of `x`'s fields, which is the most conservative propagation
since escape information
imposed on `x` will end up being propagated to all of its fields anyway
at definitions of `x`
(i.e. `:new` expression or `setfield!` call).

[^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>.

Now this alias analysis should allow us to implement a "stronger" SROA,
which eliminates the allocation of `r` within the following code:
```julia
julia> result = analyze_escapes((String,)) do s
           r = Ref(s)
           broadcast(identity, r)
       end
\#3(_2::String *, _3::Base.RefValue{String} ◌) in Main at REPL[2]:2
2 ↓ 1 ─ %1 = %new(Base.RefValue{String}, _2)::Base.RefValue{String}

                                                         │╻╷╷     Ref
3 ✓ │   %2 = Core.tuple(%1)::Tuple{Base.RefValue{String}}

                                                         │╻
broadcast
  ↓ │   %3 = Core.getfield(%2, 1)::Base.RefValue{String}

                                                         ││
  ◌ └──      goto #3 if not true

                                                         ││╻╷
materialize
  ◌ 2 ─      nothing::Nothing

                                                         │
  * 3 ┄ %6 = Base.getfield(%3, :x)::String

                                                         │││╻╷╷╷╷   copy
  ◌ └──      goto #4

                                                         ││││┃
getindex
  ◌ 4 ─      goto #5

                                                         ││││
  ◌ 5 ─      goto #6

                                                         │││
  ◌ 6 ─      goto #7

                                                         ││
  ◌ 7 ─      return %6

                                                         │

julia> EscapeAnalysis.get_aliases(result.state.aliasset,
Core.SSAValue(6), result.ir)
2-element Vector{Union{Core.Argument, Core.SSAValue}}:
 Core.Argument(2)
 :(%6)
```
Note that the allocation `%1` isn't analyzed as `ReturnEscape`, still
`_2` is analyzed so.
Comment on lines +976 to 1018
# demonstrate the power of our field / alias analysis with a realistic end to end example
abstract type AbstractPoint{T} end
mutable struct MPoint{T} <: AbstractPoint{T}
x::T
y::T
end
add(a::P, b::P) where P<:AbstractPoint = P(a.x + b.x, a.y + b.y)
function compute(T, ax, ay, bx, by)
a = T(ax, ay)
b = T(bx, by)
for i in 0:(100000000-1)
a = add(add(a, b), b)
end
a.x, a.y
end
function compute(a, b)
for i in 0:(100000000-1)
a = add(add(a, b), b)
end
a.x, a.y
end
function compute!(a, b)
for i in 0:(100000000-1)
a′ = add(add(a, b), b)
a.x = a′.x
a.y = a′.y
end
end
let result = @analyze_escapes compute(MPoint, 1+.5im, 2+.5im, 2+.25im, 4+.75im)
for i in findall(isnew, result.ir.stmts.inst)
@test is_sroa_eligible(result.state.ssavalues[i])
end
end
let result = @analyze_escapes compute(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im))
for i in findall(isnew, result.ir.stmts.inst)
@test is_sroa_eligible(result.state.ssavalues[i])
end
end
let result = @analyze_escapes compute!(MPoint(1+.5im, 2+.5im), MPoint(2+.25im, 4+.75im))
for i in findall(isnew, result.ir.stmts.inst)
@test is_sroa_eligible(result.state.ssavalues[i])
end
end
Copy link
Owner Author

Choose a reason for hiding this comment

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

So this PR will allow us to fully eliminate allocations involved within these examples (taken from LuaJIT's allocation sinking).

@aviatesk aviatesk merged commit 443f84b into master Dec 25, 2021
@aviatesk aviatesk changed the title a simple and flow-insensitive alias analysis add flow-insensitive, local field / alias analysis Dec 25, 2021
@vtjnash vtjnash deleted the avi/aliasanalysis branch January 5, 2022 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants