Skip to content

Commit a95151a

Browse files
committed
Enhance the API
This is a significant expansion of the API, particularly for analyzing backedges.
1 parent 65340b6 commit a95151a

File tree

6 files changed

+361
-37
lines changed

6 files changed

+361
-37
lines changed

demos/blog.jl

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
## Counting invalidations
2+
3+
using SnoopCompile
4+
umis(list) = unique(filter(x->isa(x, Core.MethodInstance), list))
5+
# length(umis(@snoopr using <SomePkg>))
6+
7+
## Counting MethodInstances
8+
9+
using MethodAnalysis
10+
using MethodAnalysis: equal
11+
function getmis(parent...) # use either 0 or 1 argument
12+
mis = Set{Core.MethodInstance}()
13+
visit(parent...) do mi
14+
isa(mi, Core.MethodInstance) && push!(mis, mi)
15+
end
16+
return collect(mis)
17+
end
18+
mis = getmis()
19+
20+
# counting by module
21+
22+
const modcounts = Dict{Module,Int}()
23+
for mi in mis
24+
modcounts[mi.def.module] = get(modcounts, mi.def.module, 0) + 1
25+
end
26+
27+
## When invalidations do and do not happen: the applyf example
28+
29+
f(::Int) = 1
30+
f(::Bool) = 2
31+
function applyf(container)
32+
x1 = f(container[1])
33+
x2 = f(container[2])
34+
return x1 + x2
35+
end
36+
c = Any[1, true]
37+
applyf(c)
38+
w2 = worlds(only(getmis(applyf)))
39+
code2 = only(code_typed(applyf, (Vector{Any},)))
40+
f(::String) = 3
41+
applyf(c)
42+
w3 = worlds(only(getmis(applyf)))
43+
code3 = only(code_typed(applyf, (Vector{Any},)))
44+
f(::AbstractVector) = 4 # if we replaced this with Vector{Int} we'd not have the unnecessary(?) invalidation
45+
applyf(c)
46+
w4 = worlds(only(getmis(applyf)))
47+
code4 = only(code_typed(applyf, (Vector{Any},)))
48+
f(::Missing) = 5
49+
applyf(c)
50+
w5 = worlds(only(getmis(applyf)))
51+
code5 = only(code_typed(applyf, (Vector{Any},)))
52+
f(::Nothing) = 6
53+
applyf(c)
54+
w6 = worlds(only(getmis(applyf)))
55+
code6 = only(code_typed(applyf, (Vector{Any},)))
56+
57+
# One apparently-unnecessary invalidation?
58+
@show equal(code3, code4)
59+
@show w3 == w4
60+
61+
## Determining the source of invalidations
62+
63+
# Mechanism 1: MethodInstances with TypeVar parameters
64+
function hastv(typ)
65+
isa(typ, UnionAll) && return true
66+
if isa(typ, DataType)
67+
for p in typ.parameters
68+
hastv(p) && return true
69+
end
70+
end
71+
return false
72+
end
73+
mitv = filter(mi->hastv(mi.specTypes), mis)
74+
mitvi = Set(with_all_backedges(mitv))

demos/specialization.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ end
1515
categorizemi(x) = nothing
1616

1717
visit(categorizemi)
18+
println("Found ", length(fully_specialized), " MethodInstances and ", length(nonspecialized), " other MethodInstances")
19+
1820
# We're especially interested in the nonspecialized ones.
19-
# Start with the ones in Base
20-
ubase = collect(filter(x->x.def.module === Base, nonspecialized))
2121
# Find specializations that have a TypeVar parameter
2222
function hastv(typ)
2323
isa(typ, UnionAll) && return true
@@ -28,18 +28,18 @@ function hastv(typ)
2828
end
2929
return false
3030
end
31-
ubasetv = filter(mi->hastv(mi.specTypes), ubase)
32-
println("There are ", length(ubasetv), " MethodInstances that have a TypeVar in specTypes")
31+
mitv = filter(mi->hastv(mi.specTypes), nonspecialized)
32+
println("There are ", length(mitv), " MethodInstances that have a TypeVar in specTypes")
3333

3434
# Let's analyze a specific case
3535
m = which(similar, (Vector,))
36-
mitv = Ref{Any}(nothing)
36+
_mi = Ref{Any}(nothing)
3737
visit(m.specializations) do mi
38-
mitv[] isa Core.MethodInstance && return nothing
39-
hastv(mi.specTypes) && (mitv[] = mi)
38+
_mi[] isa Core.MethodInstance && return nothing
39+
hastv(mi.specTypes) && (_mi[] = mi)
4040
return nothing
4141
end
42-
mi = mitv[]
42+
mi = _mi[]
4343
@assert(mi isa Core.MethodInstance)
4444
println("\n## Performing an analysis of ", mi, '\n')
4545
# Find the last callers with TypeVar specializations

src/MethodAnalysis.jl

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
module MethodAnalysis
22

3-
using Core: MethodInstance, SimpleVector
3+
using Core: MethodInstance, SimpleVector, MethodTable
44

5-
export visit, visit_backedges, all_backedges, terminal_backedges, call_type
5+
export visit, call_type, instance, worlds
6+
export visit_backedges, all_backedges, with_all_backedges, terminal_backedges, direct_backedges
67

78
include("visit.jl")
89
include("backedges.jl")
910

11+
## Move to Base?
12+
Base.:(==)(stmt1::Core.PhiNode, stmt2::Core.PhiNode) = stmt1.edges == stmt2.edges && stmt1.values == stmt2.values
13+
1014
"""
1115
call_type(tt)
1216
@@ -15,8 +19,62 @@ Split a signature type like `Tuple{typeof(f),ArgTypes...}` back out to `(f, Tupl
1519
function call_type(tt)
1620
ft = tt.parameters[1]
1721
argt = Tuple{tt.parameters[2:end]...}
18-
name = ft.name
19-
return (getfield(name.module, Symbol(String(name.name)[2:end])), argt)
22+
name = Symbol(String(ft.name.name)[2:end]) # strip off leading '#'
23+
return (getfield(ft.name.module, name), argt)
24+
end
25+
26+
"""
27+
minmaxs = worlds(mi::MethodInstance)
28+
29+
Collect the (min,max) world-age pairs for all CodeInstances associated with `mi`.
30+
"""
31+
function worlds(mi::Core.MethodInstance)
32+
w = Tuple{UInt,UInt}[]
33+
if isdefined(mi, :cache)
34+
ci = mi.cache
35+
push!(w, (ci.min_world, ci.max_world))
36+
while isdefined(ci, :next)
37+
ci = ci.next
38+
push!(w, (ci.min_world, ci.max_world))
39+
end
40+
end
41+
return w
42+
end
43+
44+
# Not sure we want to change the meaning of == here, so let's define our own name
45+
# A few fields are deliberately unchecked
46+
function equal(ci1::Core.CodeInfo, ci2::Core.CodeInfo)
47+
return ci1.code == ci2.code &&
48+
ci1.codelocs == ci2.codelocs &&
49+
ci1.ssavaluetypes == ci2.ssavaluetypes &&
50+
ci1.ssaflags == ci2.ssaflags &&
51+
ci1.method_for_inference_limit_heuristics == ci2.method_for_inference_limit_heuristics &&
52+
ci1.linetable == ci2.linetable &&
53+
ci1.slotnames == ci2.slotnames &&
54+
ci1.slotflags == ci2.slotflags &&
55+
ci1.slottypes == ci2.slottypes &&
56+
ci1.rettype == ci2.rettype
57+
end
58+
equal(p1::Pair, p2::Pair) = p1.second == p2.second && equal(p1.first, p2.first)
59+
60+
"""
61+
mi = instance(f, types)
62+
63+
Return the `MethodInstance` `mi` for function `f` and the given `types`.
64+
If no version compiled for these types exists, returns `nothing`.
65+
"""
66+
function instance(f, types)
67+
m = which(f, types)
68+
inst = nothing
69+
tt = Tuple{typeof(f), types...}
70+
visit(m) do mi
71+
if isa(mi, MethodInstance)
72+
if mi.specTypes === tt
73+
inst = mi
74+
end
75+
end
76+
end
77+
return inst
2078
end
2179

2280
end # module

src/backedges.jl

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,44 @@
1-
function all_backedges(mi)
1+
"""
2+
all_backedges(mi::MethodInstance)
3+
4+
Return a list of all backedges (direct and indirect) of `mi`.
5+
"""
6+
function all_backedges(mi::MethodInstance)
27
backedges = Set{MethodInstance}()
38
visit_backedges(x->(push!(backedges, x); true), mi)
49
delete!(backedges, mi)
510
return collect(backedges)
611
end
712

8-
function terminal_backedges(mi)
13+
"""
14+
with_all_backedges(itr)
15+
16+
Return all MethodInstances detected when iterating through items in `itr` and any
17+
their backedges. The result includes both MethodTable and MethodInstance backedges.
18+
"""
19+
function with_all_backedges(iter)
20+
backedges = Set{MethodInstance}()
21+
visited = Set{Union{MethodInstance,MethodTable}}()
22+
for item in iter
23+
visit_backedges(item, visited) do edge
24+
if isa(edge, MethodInstance)
25+
push!(backedges, edge)
26+
else
27+
sig, mi = edge
28+
push!(backedges, mi)
29+
end
30+
true
31+
end
32+
end
33+
return collect(backedges)
34+
end
35+
36+
"""
37+
terminal_backedges(mi::MethodInstance)
38+
39+
Obtain the "ultimate callers" of `mi`, i.e., the reason(s) `mi` was compiled.
40+
"""
41+
function terminal_backedges(mi::MethodInstance)
942
backedges = Set{MethodInstance}()
1043
visit_backedges(mi) do x
1144
if !isdefined(x, :backedges) || isempty(x.backedges)
@@ -16,3 +49,34 @@ function terminal_backedges(mi)
1649
delete!(backedges, mi)
1750
return collect(backedges)
1851
end
52+
53+
"""
54+
direct_backedges(f::Function; skip=true)
55+
56+
Collect all backedges for a function `f` as pairs `instance=>caller` or `sig=>caller` pairs.
57+
The latter occur for MethodTable backedges.
58+
If `skip` is `true`, any `caller` listed in a MethodTable backedge is omitted from the instance backedges.
59+
"""
60+
function direct_backedges(f::Function; skip::Bool=true)
61+
bes = []
62+
_skip = Set{MethodInstance}()
63+
mths = methods(f).ms
64+
callee = nothing
65+
visit_backedges(f) do item
66+
if isa(item, Pair)
67+
push!(bes, item)
68+
push!(_skip, item.second)
69+
return false
70+
else
71+
mi = item::MethodInstance
72+
if mi.def mths
73+
callee = mi
74+
return true
75+
else
76+
(!skip || mi _skip) && push!(bes, callee=>mi)
77+
return false
78+
end
79+
end
80+
end
81+
return bes
82+
end

src/visit.jl

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,24 @@
44
Scan all loaded modules with `operation`. `operation(x)` should handle `x::Module`, `x::Function`,
55
`x::Method`, `x::MethodInstance`. Any return value from `operation` will be discarded.
66
"""
7-
function visit(operation)
7+
function visit(operation; print=true)
88
visiting=Set{Module}()
99
for mod in Base.loaded_modules_array()
10-
operation(mod)
11-
visit(operation, mod, visiting)
10+
visit(operation, mod, visiting; print=print)
1211
end
1312
return nothing
1413
end
1514

16-
function visit(operation, mod::Module, visiting=Set{Module}())
15+
function visit(operation, mod::Module, visiting=Set{Module}(); print=true)
16+
mod visiting && return nothing
1717
push!(visiting, mod)
18-
println("Module ", mod)
18+
operation(mod)
19+
print && println("Module ", mod)
1920
for nm in names(mod; all=true)
2021
if isdefined(mod, nm)
2122
obj = getfield(mod, nm)
2223
if isa(obj, Module)
23-
obj in visiting && continue
24-
visit(operation, obj, visiting)
24+
visit(operation, obj, visiting; print=print)
2525
else
2626
visit(operation, obj)
2727
end
@@ -80,23 +80,69 @@ end
8080
visit(operation, x) = nothing
8181

8282
"""
83-
visit_backedges(operation, mi::MethodInstance)
83+
visit_backedges(operation, obj)
8484
85-
Visit the backedges of `mi` and apply `operation`.
86-
`operation(edge::MethodInstance)` should return `true` if the backedges of `edge` should in turn be visited,
85+
Visit the backedges of `obj` and apply `operation` to each.
86+
`operation` may need to be able to handle two call forms, `operation(mi)` and
87+
`operation(sig=>mi)`, where `mi` is a `MethodInstance` and `sig` is a `Tuple`-type.
88+
The latter arises from `MethodTable` backedges and can be ignored if `obj` does not
89+
contain `MethodTable`s.
90+
91+
`operation(edge)` should return `true` if the backedges of `edge` should in turn be visited,
8792
`false` otherwise.
93+
94+
The set of visited objects includes `obj` itself.
95+
For example, `visit_backedges(operation, f::Function)` will visit all methods of `f`,
96+
and this in turn will visit all MethodInstances of these methods.
8897
"""
89-
visit_backedges(operation, mi::MethodInstance) =
90-
visit_backedges(operation, mi, Set{MethodInstance}())
98+
visit_backedges(operation, obj) =
99+
visit_backedges(operation, obj, Set{Union{MethodInstance,MethodTable}}())
91100

92-
function visit_backedges(operation, mi, visited)
93-
mi visited && return nothing
94-
push!(visited, mi)
95-
status = operation(mi)
96-
if status && isdefined(mi, :backedges)
101+
function _visit_backedges(operation, mi::MethodInstance, visited)
102+
if isdefined(mi, :backedges)
97103
for edge in mi.backedges
98104
visit_backedges(operation, edge, visited)
99105
end
100106
end
101107
return nothing
102108
end
109+
110+
function visit_backedges(operation, mi::MethodInstance, visited)
111+
mi visited && return nothing
112+
push!(visited, mi)
113+
operation(mi) && _visit_backedges(operation, mi, visited)
114+
return nothing
115+
end
116+
117+
function visit_backedges(operation, mt::MethodTable, visited)
118+
mt visited && return nothing
119+
push!(visited, mt)
120+
if isdefined(mt, :backedges)
121+
for i = 1:2:length(mt.backedges)
122+
sig, mi = mt.backedges[i], mt.backedges[i+1]
123+
if operation(sig=>mi) && mi visited
124+
push!(visited, mi)
125+
_visit_backedges(operation, mi, visited)
126+
end
127+
end
128+
end
129+
return nothing
130+
end
131+
132+
function visit_backedges(operation, m::Method, visited)
133+
visit(m) do mi
134+
if isa(mi, MethodInstance)
135+
visit_backedges(operation, mi, visited)
136+
end
137+
end
138+
return nothing
139+
end
140+
141+
function visit_backedges(operation, f::Function, visited)
142+
ml = methods(f)
143+
visit_backedges(operation, ml.mt, visited)
144+
for m in ml
145+
visit_backedges(operation, m, visited)
146+
end
147+
return nothing
148+
end

0 commit comments

Comments
 (0)