From 256007044b380fa3da1a59cb6b98700dfe557fe0 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> Date: Sat, 26 Mar 2022 21:17:41 +0900 Subject: [PATCH] more robust test/examples for optimization analysis (#336) --- examples/dispatch_analysis.jl | 79 +++++++++++++-------- test/analyzers/test_optanalyzer.jl | 106 ++++++++++++++++++----------- 2 files changed, 118 insertions(+), 67 deletions(-) diff --git a/examples/dispatch_analysis.jl b/examples/dispatch_analysis.jl index 94f1f69bf..269b2967e 100644 --- a/examples/dispatch_analysis.jl +++ b/examples/dispatch_analysis.jl @@ -156,22 +156,25 @@ 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. @@ -179,31 +182,51 @@ end # 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 diff --git a/test/analyzers/test_optanalyzer.jl b/test/analyzers/test_optanalyzer.jl index f7a16459d..d2959d8cb 100644 --- a/test/analyzers/test_optanalyzer.jl +++ b/test/analyzers/test_optanalyzer.jl @@ -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