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

more robust test/examples for optimization analysis #336

Merged
merged 1 commit into from
Mar 26, 2022
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
79 changes: 51 additions & 28 deletions examples/dispatch_analysis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -156,54 +156,77 @@ end
# First, let's play with simple and factitious examples and check if `DispatchAnalyzer`
# works as expected.

f(a) = a
f(a::Number) = a
getsomething(x::Any) = x
getsomething(x::Array) = x[]
getsomething(::Nothing) = throw(ArgumentError("nothing is nothing"))
getsomething(::Missing) = throw(ArgumentError("too philosophical"))

# `f(::Int)` a concrete call and just type stable and anything shouldn't be reported:
@report_dispatch f(10) # should be ok
# If callsite is type-stable (i.e. dispatched with concretely-typed arguments),
# any problem shouldn't be reported:
@report_dispatch getsomething(42) # should be ok

# But if the argument type isn't well typed, compiler can't determine which method to call,
# But if the argument isn't well-typed, compiler can't determine which method to call,
# and it will lead to runtime dispatch:
report_dispatch((Any,)) do a
f(a) # runtime dispatch !
getsomething(a) # runtime dispatch !
end

# Note that even if a call is not "well-typed", i.e. it's not a concrete call, runtime
# Note that even if a call is not "well-typed" (i.e. it's not a concrete call), runtime
# dispatch won't happen as far as a single method can be resovled statically:
report_dispatch((Integer,)) do a
f(a) # this isn't so good, but ok
report_dispatch((AbstractString,)) do a
getsomething(a) # this call isn't very concrete, but ok, Julia can optimize it
end

# Ok, working nicely so far. Let's move on to a bit more complicated examples.
# When working on inherently-untyped code base, which typically needs to deal with
# arbitrarily-typed objects at runtime (as like Julia's high-level compiler),
# the `@nospecialize` annotation can be very useful -- it helps us avoids excessive code
# specialization by _suppressing_ runtime dispatches with runtime object types.
# For example, let's assume we have a vector of arbitrary untyped objects given by user-program
# and need to check if its element is `Type`-object or not.
# The core logic for this check would be something like `isa(t, DataType) && t.name === Type.body.name`.
# In this setup we gonna see runtime dispatches if we abuse the dispatch semantics to
# implement the `isa(t, DataType)` branching. Rather, we can eliminte runtime dispatch
# and achieve a best performance by using `@nospecialize` annotation in this kind of situation.
# We can confirm the effect of `@nospecialize` with `DispatchAnalyzer` like this:

isType1(::Any) = false
isType1(t::DataType) = t.name === Type.body.name
isType2(@nospecialize t) = isa(t, DataType) && t.name === Type.body.name
# For example, let's assume we have a vector of arbitrary untyped objects used within
# an user-program and need to check if its element is `Type`-like object with the
# following logic:
function isTypelike(x)
if isa(x, DataType)
return isa(x, DataType) && x.name === Type.body.name
elseif isa(x, Union)
return isTypelike(x.a) && isTypelike(x.b)
elseif isa(x, UnionAll)
return isTypelike(x.body)
else
return false
end
end

# But without `@nospecialize`, we gonna see runtime dispatches at the recursive call sites as
# they will be specialized at runtime. In this setup, we can suppress the runtime dipsatches
# and achieve a best performance by applying `@nospecialize` annotation to the argument `x`:
function isTypelike′(@nospecialize x)
if isa(x, DataType)
return isa(x, DataType) && x.name === Type.body.name
elseif isa(x, Union)
return isTypelike′(x.a) && isTypelike′(x.b)
elseif isa(x, UnionAll)
return isTypelike′(x.body)
else
return false
end
end

# We can confirm the effect of `@nospecialize` with `DispatchAnalyzer`:
report_dispatch((Vector{Any},)) do xs
x = xs[1]
r1 = isType1(x) # this call will be runtime-dispatched
r2 = isType2(x) # this call will be statically resolved (not runtime-dispatched)
x = xs[1]
r = isTypelike(x) # this call will be runtime-dispatched
r′ = isTypelike′(x) # this call will be statically resolved (not runtime-dispatched)
return r1, r2
end

# We can assert this report by looking at the output of `code_typed`, where `isTyped1(x)`
# remains as `:call` expression (meaning it will be dispatched at runtime) while `isType2(x)`
# We can assert this report by looking at the output of `code_typed`, where `isTypelike(x)`
# remains as `:call` expression (meaning it will be dispatched at runtime) while `isTypelike′(x)`
# has been statically resolved and even inlined:
code_typed((Vector{Any},)) do xs
x = xs[1]
r1 = isType1(x) # this call will be runtime-dispatched
r2 = isType2(x) # this call will be statically resolved (not runtime-dispatched)
x = xs[1]
r = isTypelike(x) # this call will be runtime-dispatched
r′ = isTypelike′(x) # this call will be statically resolved (not runtime-dispatched)
return r1, r2
end

Expand Down
106 changes: 67 additions & 39 deletions test/analyzers/test_optanalyzer.jl
Original file line number Diff line number Diff line change
@@ -1,53 +1,81 @@
# OptAnalyzer
# ===========

@testset "runtime dispatch" begin
let M = Module()
@eval M begin
f(a) = a
f(a::Number) = a
end
getsomething(x::Any) = x
getsomething(x::Array) = x[]
getsomething(::Nothing) = throw(ArgumentError("nothing is nothing"))
getsomething(::Missing) = throw(ArgumentError("too philosophical"))

# `f(::Int)` a concrete call and just type stable and anything shouldn't be reported
@test_opt M.f(10) # should be ok
# bad: will lead to excessive specializations via runtime dispatch
function isType1(x)
if isa(x, DataType)
return isa(x, DataType) && x.name === Type.body.name
elseif isa(x, Union)
return isType1(x.a) && isType1(x.b)
elseif isa(x, UnionAll)
return isType1(x.body)
else
return false
end
end

let # if the argument type isn't well typed, compiler can't determine which method to call,
# and it will lead to runtime dispatch
result = @eval M begin
$report_opt((Vector{Any},)) do ary
f(ary[1]) # runtime dispatch !
end
end
@test length(get_reports(result)) == 1
r = first(get_reports(result))
@test isa(r, RuntimeDispatchReport)
end
# good: will be statically dispatched
function isType2(@nospecialize x)
if isa(x, DataType)
return isa(x, DataType) && x.name === Type.body.name
elseif isa(x, Union)
return isType2(x.a) && isType2(x.b)
elseif isa(x, UnionAll)
return isType2(x.body)
else
return false
end
end

let M = Module()
@eval M begin
# if we annotate `@noinline` to a function, then its call won't be inlined and will be
# dispatched runtime
@inline g1(a) = return a
@noinline g2(a) = return a
@testset "runtime dispatch" begin
test_opt((Int, Vector{Any}, String,)) do a, b, c
return (
getsomething(a),
getsomething(b),
getsomething(c),
getsomething(nothing),
getsomething(missing))
end

# NOTE the following test is line-sensitive !
# if the argument type isn't well typed, compiler can't determine which method to call,
# and it will lead to runtime dispatch
let result = report_opt((Vector{Any},)) do xs
getsomething(xs[1]) # runtime dispatch !
end
@test length(get_reports(result)) == 1
r = only(get_reports(result))
@test isa(r, RuntimeDispatchReport)
@test any(r.vst) do vf
vf.file === Symbol(@__FILE__) &&
vf.line == (@__LINE__) - 7
end
end

let
result = @eval M $report_opt((Vector{Any},)) do ary
a = ary[1]
g1(a) # this call should be statically resolved and inlined
g2(a) # this call should be statically resolved but not inlined, and will be dispatched
end
# union split might help
test_opt((Vector{Union{Int,String,Nothing}},)) do xs
getsomething(xs[1]) # runtime dispatch !
end

# NOTE the following test is line-sensitive !
@test length(get_reports(result)) == 1
r = first(get_reports(result))
@test isa(r, RuntimeDispatchReport)
@test any(r.vst) do vf
vf.file === Symbol(@__FILE__) &&
vf.line == (@__LINE__) - 9
end
# NOTE the following test is line-sensitive !
let result = report_opt((Vector{Any},)) do xs
isType1(xs[1])
end
@test length(get_reports(result)) == 1
r = only(get_reports(result))
@test isa(r, RuntimeDispatchReport)
@test any(r.vst) do vf
vf.file === Symbol(@__FILE__) &&
vf.line == (@__LINE__) - 7
end
end
test_opt((Vector{Any},)) do xs
isType2(xs[1])
end

# real-world targets
Expand Down