Skip to content

Commit

Permalink
more robust test/examples for optimization analysis (#336)
Browse files Browse the repository at this point in the history
  • Loading branch information
aviatesk authored Mar 26, 2022
1 parent c487ac0 commit 2560070
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 67 deletions.
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

0 comments on commit 2560070

Please sign in to comment.