Skip to content

A few changes to the analysis of abstract signatures demo #17

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

Merged
merged 3 commits into from
Aug 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 56 additions & 37 deletions demos/abstract.jl
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
using MethodAnalysis


"""
atrisktyp(tt)
is_atrisk_type(tt)

Given a Tuple-type signature (e.g., `Tuple{typeof(sum),Vector{Int}}`), determine whether this signature
is "at risk" for invalidation. Essentially it returns `true` if one or more arguments are of abstract type,
although there are prominent exceptions:

- `Function` is allowed
- any constructor call is allowed
- `convert(X, x)` where `isa(x, X)` is true
- `setindex!` and `push!` methods where the valtype is a subtype of the eltype (likewise keytype for AbstractDicts)
- Constructor calls with arbitrary argument types
- `convert(X, x)` where `isa(x, X)`
- `setindex!` and `push!` methods where the valtype is a subtype of the eltype (for AbstractDicts, likewise for the keytype)
- `getindex`, `length`, `isempty`, and `iterate` on any tuple

All of these are "allowed," meaning that they return `false`.
Moreover, some specific non-concrete argument types---like `Union`s of concrete types and `Function`---
do not trigger a return of `true`, although other at-risk argument types can lead to an overall `true` return
for the signature.
"""
function atrisktype(@nospecialize(typ))
function is_atrisk_type(@nospecialize(typ))
# signatures like `convert(Vector, a)`, `foo(::Vararg{Synbol,N}) where N` do not seem to pose a problem
isa(typ, TypeVar) && return false
# isbits parameters are not a problem
Expand All @@ -24,7 +27,7 @@ function atrisktype(@nospecialize(typ))
end
# Exclude signatures with Union{}
typ === Union{} && return false
isa(typ, Union) && return atrisktype(typ.a) | atrisktype(typ.b)
isa(typ, Union) && return is_atrisk_type(typ.a) | is_atrisk_type(typ.b)
# Type{T}: signatures like `convert(::Type{AbstractString}, ::String)` are not problematic
typ <: Type && return false
if typ <: Tuple && length(typ.parameters) >= 1
Expand All @@ -38,6 +41,9 @@ function atrisktype(@nospecialize(typ))
p2 = Base.unwrap_unionall(p2)
if isa(p2, DataType) && length(p2.parameters) === 1
T = p2.parameters[1]
if isa(T, TypeVar)
T = T.ub
end
isa(p3, Type) && isa(T, Type) && p3 <: T && return false
end
end
Expand All @@ -53,9 +59,9 @@ function atrisktype(@nospecialize(typ))
end
# show(io::IO, x) is OK as long as typeof(x) is safe
elseif p1 === typeof(Base.show) || p1 === typeof(Base.print) || p1 === typeof(Base.println)
# atrisktype(typ.parameters[2]) && return true
# is_atrisk_type(typ.parameters[2]) && return true
for i = 3:length(typ.parameters)
atrisktype(typ.parameters[i]) && return true
is_atrisk_type(typ.parameters[i]) && return true
end
return false
# setindex!(a, x, idx) and push!(a, x) are safe if typeof(x) <: eltype(a)
Expand All @@ -75,30 +81,46 @@ function atrisktype(@nospecialize(typ))
isconcretetype(typ) && return false
# ::Function args are excluded
typ === Function && return false
!isempty(typ.parameters) && (any(atrisktype, typ.parameters) || return false)
!isempty(typ.parameters) && (any(is_atrisk_type, typ.parameters) || return false)
return true
end

@assert atrisktype(Tuple{typeof(==),Any,Any})
@assert atrisktype(Tuple{typeof(==),Symbol,Any})
@assert atrisktype(Tuple{typeof(==),Any,Symbol})
@assert !atrisktype(Tuple{typeof(==),Symbol,Symbol})
@assert !atrisktype(Tuple{typeof(convert),Type{Any},Any})
@assert !atrisktype(Tuple{typeof(convert),Type{AbstractString},AbstractString})
@assert !atrisktype(Tuple{typeof(convert),Type{AbstractString},String})
@assert atrisktype(Tuple{typeof(convert),Type{String},AbstractString})
@assert !atrisktype(Tuple{typeof(map),Function,Vector{Any}})
@assert !atrisktype(Tuple{typeof(getindex),Dict{Union{String,Int},Any},Union{String,Int}})
@assert atrisktype(Tuple{typeof(getindex),Dict{Union{String,Int},Any},Any})
@assert !atrisktype(Tuple{Type{BoundsError},Any,Any})
@assert atrisktype(Tuple{typeof(sin),Any})
@assert !atrisktype(Tuple{typeof(length),Tuple{Any,Any}})
@assert atrisktype(Tuple{typeof(setindex!),Vector{Int},Any,Int})
@assert !atrisktype(Tuple{typeof(setindex!),Vector{Any},Any,Int})
@assert atrisktype(Tuple{typeof(push!),Vector{Int},Any})
@assert !atrisktype(Tuple{typeof(push!),Vector{Any},Any})
@assert is_atrisk_type(Tuple{typeof(==),Any,Any})
@assert is_atrisk_type(Tuple{typeof(==),Symbol,Any})
@assert is_atrisk_type(Tuple{typeof(==),Any,Symbol})
@assert !is_atrisk_type(Tuple{typeof(==),Symbol,Symbol})
@assert !is_atrisk_type(Tuple{typeof(convert),Type{Any},Any})
@assert !is_atrisk_type(Tuple{typeof(convert),Type{AbstractString},AbstractString})
@assert !is_atrisk_type(Tuple{typeof(convert),Type{AbstractString},String})
@assert is_atrisk_type(Tuple{typeof(convert),Type{String},AbstractString})
@assert !is_atrisk_type(Tuple{typeof(convert),Type{Union{Int,Float32}},Int})
@assert !is_atrisk_type(Tuple{typeof(convert),Type{Union{Int,Float32}},Int32})
@assert is_atrisk_type(Tuple{typeof(convert),Type{Union{Int,Float32}},Integer})
@assert !is_atrisk_type(Tuple{typeof(convert),Type{T} where T<:Union{Int,Float32},Int})
@assert !is_atrisk_type(Tuple{typeof(map),Function,Vector{Any}})
@assert !is_atrisk_type(Tuple{typeof(getindex),Dict{Union{String,Int},Any},Union{String,Int}})
@assert is_atrisk_type(Tuple{typeof(getindex),Dict{Union{String,Int},Any},Any})
@assert !is_atrisk_type(Tuple{Type{BoundsError},Any,Any})
@assert is_atrisk_type(Tuple{typeof(sin),Any})
@assert !is_atrisk_type(Tuple{typeof(length),Tuple{Any,Any}})
@assert is_atrisk_type(Tuple{typeof(setindex!),Vector{Int},Any,Int})
@assert !is_atrisk_type(Tuple{typeof(setindex!),Vector{Any},Any,Int})
@assert is_atrisk_type(Tuple{typeof(push!),Vector{Int},Any})
@assert !is_atrisk_type(Tuple{typeof(push!),Vector{Any},Any})

# Get the name of a method as written in the code. This strips keyword-method mangling.
function codename(sym::Symbol)
symstr = String(sym)
# Body methods
m = match(r"^#(.*?)#\d+$", symstr)
m !== nothing && return Symbol(only(m.captures))
# kw methods
m = match(r"^(.*?)##kw$", symstr)
m !== nothing && return Symbol(only(m.captures))
return sym
end

isexported(mi::Core.MethodInstance) = isdefined(Main, mi.def.name)
isexported(mi::Core.MethodInstance) = isdefined(Main, codename(mi.def.name))
getfunc(mi::Core.MethodInstance) = getfunc(mi.def)
getfunc(m::Method) = getfield(m.module, m.name)
nmethods(mi::Core.MethodInstance) = length(methods(getfunc(mi)))
Expand All @@ -124,7 +146,7 @@ end
const becounter = Dict{Core.MethodInstance,Int}()
visit() do item
if item isa Core.MethodInstance && !fromcc(item.def.module)
if atrisktype(item.specTypes)
if is_atrisk_type(item.specTypes)
becounter[item] = length(all_backedges(item))
end
return false
Expand All @@ -141,15 +163,12 @@ open("/tmp/methdata_$VERSION.log", "w") do io
end

# Split into exported & private functions
mtup = (nmethods = 0, nbackedges = 0)
miexp = Pair{Core.MethodInstance,typeof(mtup)}[]
miexp = Pair{Core.MethodInstance,Int}[]
mipriv = similar(miexp)
for (mi, c) in prs
n = nmethods(mi)
pr = mi=>(nmethods=n, nbackedges=c)
if isexported(mi)
push!(miexp, pr)
push!(miexp, mi=>c)
else
push!(mipriv, pr)
push!(mipriv, mi=>c)
end
end
165 changes: 165 additions & 0 deletions demos/abstract_tests.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
atrisk_backedges = with_all_backedges(keys(becounter))

function atrisk_method(m::Method, atrisk_backedges)
for mi in methodinstances(m)
mi ∈ atrisk_backedges && return true
end
return false
end

function atrisk_triggers(m::Method, atrisk_instances)
triggers = Set{Core.MethodInstance}()
for mi in atrisk_instances
if atrisk_method(m, all_backedges(mi))
push!(triggers, mi)
end
end
return triggers
end

# This removes MethodInstances that no one in their right mind should ever invalidate by specialization.
function remove_unlikely_methodinstances(list)
out = Core.MethodInstance[]
for mi in list
mi = mi::Core.MethodInstance # must have MethodInstance elements
# All `continue` statements below omit the MethodInstance
name = codename(mi.def.name)
name ∈ (:invokelatest, :unwrap_unionall, :rewrap_unionall) && continue
name ∈ (:print, :sprint) && length(mi.specTypes.parameters) - mi.def.nkw > 3 && continue
# No one should ever specialize on notify or schedule's `val` argument
name === :notify && !is_atrisk_type(mi.specTypes.parameters[2]) &&
!any(is_atrisk_type, mi.specTypes.parameters[4:end]) && continue
name === :schedule && !any(is_atrisk_type, mi.specTypes.parameters[2:end-1]) && continue
# Add more removal-filters here

# We've decided to keep it
push!(out, mi)
end
return out
end

using Test

# Invalidating the code that loads packages leads to major slowdowns, especially if it happens repeatedly
# in a dependent chain of package loads. Ideally, we'd make this code-path "bulletproof".
for m in methods(Base.require)
@test_broken isempty(remove_unlikely_methodinstances(atrisk_triggers(m, first.(miexp))))
@test_broken isempty(remove_unlikely_methodinstances(atrisk_triggers(m, first.(mipriv))))
end

# Test overall number of atrisk MethodInstances and their average number of backedges
badexp = Set(remove_unlikely_methodinstances(first.(miexp)))
badcounts = filter(pr->pr.first ∈ badexp, miexp)
@test length(badcounts) < 1000
if length(badcounts) < 800
@info "There are now only $(length(badcounts)) at-risk specializations of exported methods, consider dropping the threshold"
end
meancounts = sum(last.(badcounts))/length(badcounts)
@test meancounts < 32
if meancounts < 24
@info "The mean number of at-risk backedges is now only $meancounts, consider dropping the threshold"
end

# Check for inference quality in specific functions.
# This is valid only for functions that should always return a particular type for any valid call of their methods.
function function_returns(@nospecialize(f), @nospecialize(typ); allow_missing_for_missing=true, minargs=0)
for m in methods(f)
sig = Base.unwrap_unionall(m.sig)
for rt in Base.return_types(call_type(Base.unwrap_unionall(m.sig))...)
rt <: typ && continue
if allow_missing_for_missing && any(T->T===Missing, sig.parameters[2:end]) && rt === Missing
continue
end
length(sig.parameters) - 1 < minargs && continue
return false
end
end
return true
end

# All the is* functions
# Not all of the broken cases have been checked carefully; it's possible some of these should return `Union{Bool,Missing}`
# or something.
@test_broken function_returns(isabspath, Bool)
@test function_returns(isabstracttype, Bool)
@test_broken function_returns(isapprox, Bool)
@test_broken function_returns(isascii, Bool)
# @test function_returns(isassigned, Bool)
@test function_returns(isbits, Bool)
@test function_returns(isbitstype, Bool)
@test function_returns(isblockdev, Bool)
@test function_returns(ischardev, Bool)
@test function_returns(iscntrl, Bool)
@test function_returns(isconcretetype, Bool)
@test function_returns(isconst, Bool)
@test function_returns(isdefined, Bool)
@test function_returns(isdigit, Bool)
@test function_returns(isdir, Bool)
@test function_returns(isdirpath, Bool)
@test_broken function_returns(isdisjoint, Bool)
@test function_returns(isdispatchtuple, Bool)
@test_broken function_returns(isempty, Bool)
@test_broken function_returns(isequal, Bool; minargs=2)
@test_broken function_returns(iseven, Bool)
@test function_returns(isexported, Bool)
@test function_returns(isfifo, Bool)
@test function_returns(isfile, Bool)
@test_broken function_returns(isfinite, Bool)
@test_broken function_returns(isinf, Bool)
@test_broken function_returns(isinteger, Bool)
@test function_returns(isinteractive, Bool)
@test_broken function_returns(isless, Bool)
@test function_returns(isletter, Bool)
@test function_returns(islink, Bool)
@test function_returns(islocked, Bool)
@test function_returns(islowercase, Bool)
@test_broken function_returns(ismarked, Bool)
@test function_returns(ismissing, Bool)
@test function_returns(ismount, Bool)
@test function_returns(ismutable, Bool)
@test_broken function_returns(isnan, Bool)
@test function_returns(isnothing, Bool)
@test function_returns(isnumeric, Bool)
@test_broken function_returns(isodd, Bool)
@test_broken function_returns(isone, Bool)
@test_broken function_returns(isopen, Bool)
@test function_returns(ispath, Bool)
@test_broken function_returns(isperm, Bool)
@test function_returns(ispow2, Bool)
@test function_returns(isprimitivetype, Bool)
@test function_returns(isprint, Bool)
@test function_returns(ispunct, Bool)
@test_broken function_returns(isreadable, Bool)
@test_broken function_returns(isreadonly, Bool)
@test_broken function_returns(isready, Bool)
@test_broken function_returns(isreal, Bool)
@test function_returns(issetequal, Bool)
@test function_returns(issetgid, Bool)
@test function_returns(issetuid, Bool)
@test function_returns(issocket, Bool)
@test_broken function_returns(issorted, Bool)
@test function_returns(isspace, Bool)
@test function_returns(issticky, Bool)
@test function_returns(isstructtype, Bool)
@test_broken function_returns(issubnormal, Bool)
@test_broken function_returns(issubset, Bool)
@test function_returns(istaskdone, Bool)
@test function_returns(istaskfailed, Bool)
@test function_returns(istaskstarted, Bool)
@test_broken function_returns(istextmime, Bool)
@test function_returns(isuppercase, Bool)
@test_broken function_returns(isvalid, Bool)
@test_broken function_returns(iswritable, Bool)
@test function_returns(isxdigit, Bool)
@test_broken function_returns(iszero, Bool)

@test_broken function_returns(eof, Bool) # just the Pkg.TOML one is broken

# Check that we never infer certain methodinstances
for f in (==, isequal, <, <=)
for mi in methodinstances(==)
if any(T->T<:Real, mi.specTypes.parameters)
@test !is_atrisk_type(mi.specTypes)
end
end
end
11 changes: 7 additions & 4 deletions src/findcallers.jl
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
export findcallers

function get_typed_instances!(srcs, mi::MethodInstance, world, interp)
# This is essentially take from code_typed_by_type
tt = mi.specTypes
function get_typed_instances!(srcs, @nospecialize(tt), method::Method, world, interp)
# This is essentially taken from code_typed_by_type
matches = Base._methods_by_ftype(tt, -1, world)
if matches === false
error("signature $(item.specTypes) does not correspond to a generic function")
end
for match in matches
match.method == mi.def || continue
match.method == method || continue
meth = Base.func_for_method_checked(match.method, tt, match.sparams)
(src, ty) = isdefined(Core.Compiler, :NativeInterpreter) ?
Core.Compiler.typeinf_code(interp, meth, match.spec_types, match.sparams, false) :
Expand All @@ -17,6 +16,7 @@ function get_typed_instances!(srcs, mi::MethodInstance, world, interp)
end
return srcs
end
get_typed_instances!(srcs, mi::MethodInstance, world, interp) = get_typed_instances!(srcs, mi.specTypes, mi.def, world, interp)

defaultinterp(world) = isdefined(Core.Compiler, :NativeInterpreter) ?
Core.Compiler.NativeInterpreter(world) :
Expand All @@ -25,6 +25,9 @@ defaultinterp(world) = isdefined(Core.Compiler, :NativeInterpreter) ?
function get_typed_instances(mi::MethodInstance; world=typemax(UInt), interp=defaultinterp(world))
return get_typed_instances!(Tuple{CodeInfo,Core.SimpleVector}[], mi, world, interp)
end
function get_typed_instances(@nospecialize(tt), method::Method; world=typemax(UInt), interp=defaultinterp(world))
return get_typed_instances!(Tuple{CodeInfo,Core.SimpleVector}[], tt, method, world, interp)
end

struct CallMatch
mi::MethodInstance
Expand Down