Optimization Analysis
Successful type inference and optimization are key to high-performing Julia programs. But as mentioned in the performance tips, there are some chances where Julia can not infer the types of your program very well and can not optimize it well accordingly.
While there are many possibilities of "type-instabilities", like usage of non-constant global variable most notably, probably the most tricky one would be "captured variable" – Julia can not really well infer the type of variable that is observed and modified by both inner function and enclosing one. And such type instabilities can lead to various optimization failures. One of the most common barriers to performance is known as "runtime dispatch", which happens when a matching method can't be resolved by the compiler due to the lack of type information and it is looked up at runtime instead. Since runtime dispatch is caused by poor type information, it often indicates the compiler could not do other optimizations including inlining and scalar replacements of aggregates.
In order to avoid such problems, we usually inspect the output of code_typed
or its family, and check if there is anywhere type is not well inferred and optimization was not successful. But the problem is that one needs to have enough knowledge about inference and optimization in order to interpret the output. Another problem is that they can only present the "final" output of the inference and optimization, and we can not inspect the entire call graph and may miss finding where a problem actually happened and how the type-instability has been propagated. There is a nice package called Cthulhu.jl, which allows us to look at the outputs of code_typed
by descending into a call tree, recursively and interactively. The workflow with Cthulhu is much more efficient and powerful, but still, it requires much familiarity with the Julia compiler and it tends to be tedious.
So, why not automate it? JET implements such an analyzer that investigates the optimized representation of your program and automatically detects anywhere the compiler failed in optimization. Especially, it can find where Julia creates captured variables, where runtime dispatch will happen, and where Julia gives up the optimization work due to unresolvable recursive function call.
SnoopCompile also detects inference failures, but JET and SnoopCompile use different mechanisms: JET performs static analysis of a particular call, while SnoopCompile performs dynamic analysis of new inference. As a consequence, JET's detection of inference failures is reproducible (you can run the same analysis repeatedly and get the same result) but terminates at any non-inferable node of the call graph: you will miss runtime dispatch in any non-inferable callees. Conversely, SnoopCompile's detection of inference failures can explore the entire callgraph, but only for those portions that have not been previously inferred, and the analysis cannot be repeated in the same session.
Quick Start
julia> using JET
JET exports @report_opt
, which analyzes the entire call graph of a given generic function call, and then reports detected performance pitfalls.
As a first example, let's see how we can find and fix runtime dispatches using JET:
julia> n = rand(Int); # non-constant global variable
julia> make_vals(n) = n ≥ 0 ? (zero(n):n) : (n:zero(n));
julia> function sumup(f) # this function uses the non-constant global variable `n` here # and it makes every succeeding operations type-unstable vals = make_vals(n) s = zero(eltype(vals)) for v in vals s += f(v) end return s end;
julia> @report_opt sumup(sin) # runtime dispatches will be reported
═════ 7 possible errors found ═════ ┌ sumup(f::typeof(sin)) @ Main ./REPL[3]:5 │ runtime dispatch detected: Main.make_vals(%1::Any)::Any └──────────────────── ┌ sumup(f::typeof(sin)) @ Main ./REPL[3]:6 │ runtime dispatch detected: Main.eltype(%2::Any)::Any └──────────────────── ┌ sumup(f::typeof(sin)) @ Main ./REPL[3]:6 │ runtime dispatch detected: Main.zero(%3::Any)::Any └──────────────────── ┌ sumup(f::typeof(sin)) @ Main ./REPL[3]:7 │ runtime dispatch detected: iterate(%2::Any)::Any └──────────────────── ┌ sumup(f::typeof(sin)) @ Main ./REPL[3]:8 │ runtime dispatch detected: f::typeof(sin)(%11::Any)::Any └──────────────────── ┌ sumup(f::typeof(sin)) @ Main ./REPL[3]:8 │ runtime dispatch detected: (%10::Any Main.:+ %13::Any)::Any └──────────────────── ┌ sumup(f::typeof(sin)) @ Main ./REPL[3]:9 │ runtime dispatch detected: iterate(%2::Any, %12::Any)::Any └────────────────────
JET's analysis result will be dynamically updated when we (re-)define functions[1], and we can "hot-fix" the runtime dispatches within the same running Julia session like this:
julia> # we can pass parameters as a function argument instead, and then everything will be type-stable function sumup(f, n) vals = make_vals(n) s = zero(eltype(vals)) for v in vals # NOTE here we may get union type like `s::Union{Int,Float64}`, # but Julia can optimize away such small unions (thus no runtime dispatch) s += f(v) end return s end;
julia> @report_opt sumup(sin, rand(Int)) # now runtime dispatch free !
No errors detected
@report_opt
can also report the existence of captured variables, which are really better to be eliminated within performance-sensitive context:
julia> # the examples below are all adapted from https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured function abmult(r::Int) if r < 0 r = -r end # the closure assigned to `f` make the variable `r` captured f = x -> x * r return f end;
julia> @report_opt abmult(42)
═════ 3 possible errors found ═════ ┌ abmult(r::Int64) @ Main ./REPL[1]:3 │ captured variable `r` detected └──────────────────── ┌ abmult(r::Int64) @ Main ./REPL[1]:3 │ runtime dispatch detected: (%5::Any Main.:< 0)::Any └──────────────────── ┌ abmult(r::Int64) @ Main ./REPL[1]:4 │ runtime dispatch detected: Main.:-(%11::Any)::Any └────────────────────
julia> function abmult(r0::Int) # we can improve the type stability of the variable `r` like this, # but it is still captured r::Int = r0 if r < 0 r = -r end f = x -> x * r return f end;
julia> @report_opt abmult(42)
═════ 1 possible error found ═════ ┌ abmult(r0::Int64) @ Main ./REPL[3]:6 │ captured variable `r` detected └────────────────────
julia> function abmult(r::Int) if r < 0 r = -r end # we can try to eliminate the capturing # and now this function would be the most performing f = let r = r x -> x * r end return f end;
julia> @report_opt abmult(42)
No errors detected
With the target_modules
configuration, we can easily limit the analysis scope to a specific module context:
julia> # problem: when ∑1/n exceeds `x` ? function compute(x) r = 1 s = 0.0 n = 1 @time while r < x s += 1/n if s ≥ r # `println` call is full of runtime dispatches for good reasons # and we're not interested in type-instabilities within this call # since we know it's only called a few times println("round $r/$x has been finished") r += 1 end n += 1 end return n, s end
compute (generic function with 1 method)
julia> @report_opt compute(30) # bunch of reports will be reported from the `println` call
═════ 16 possible errors found ═════ ┌ compute(x::Int64) @ Main ./REPL[1]:11 │┌ println(xs::String) @ Base ./coreio.jl:4 ││ runtime dispatch detected: println(%1::IO, %2::String)::Nothing │└──────────────────── ┌ compute(x::Int64) @ Main ./timing.jl:322 │┌ kwcall(::@NamedTuple{…}, ::typeof(Base.time_print), io::IO, elapsedtime::Float64, bytes::Int64, gctime::Int64, allocs::Int64, lock_conflicts::Int64, compile_time::Float64, recompile_time::Float64, newline::Bool) @ Base ./timing.jl:173 ││┌ @ Base ./timing.jl:176 │││┌ sprint(::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String}) @ Base ./strings/io.jl:107 ││││┌ sprint(::Base.var"#1138#1139"{…}; context::Nothing, sizehint::Int64) @ Base ./strings/io.jl:114 │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:183 ││││││ runtime dispatch detected: (%30::Any != 0)::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:185 ││││││ runtime dispatch detected: (%68::Any != 0)::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:186 ││││││ runtime dispatch detected: Base.prettyprint_getunits(%78::Any, %82::Int64, 1000)::Tuple{Any, Any} │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:187 ││││││ runtime dispatch detected: (%87::Any == 1)::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:188 ││││││ runtime dispatch detected: Base.Int(%94::Any)::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:188 ││││││ runtime dispatch detected: (Base._cnt_units)[%87::Any]::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:188 ││││││ runtime dispatch detected: (%101::Any == 1)::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:188 ││││││ runtime dispatch detected: print(io::IOBuffer, %95::Any, %96::Any, %106::String)::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:190 ││││││ runtime dispatch detected: Base.Float64(%117::Any)::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:190 ││││││ runtime dispatch detected: Base.Ryu.writefixed(%118::Any, 2)::String │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:190 ││││││ runtime dispatch detected: (Base._cnt_units)[%87::Any]::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:190 ││││││ runtime dispatch detected: print(io::IOBuffer, %119::String, %120::Any, " allocations: ")::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:195 ││││││ runtime dispatch detected: (%149::Any != 0)::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:201 ││││││ runtime dispatch detected: (%181::Any != 0)::Any │││││└──────────────────── │││││┌ (::Base.var"#1138#1139"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:208 ││││││ runtime dispatch detected: (%219::Any != 0)::Any │││││└────────────────────
julia> @report_opt target_modules=(@__MODULE__,) compute(30) # focus on what we wrote, and no error should be reported
No errors detected
There is also function_filter
, which can ignore specific function calls.
@test_opt
can be used to assert that a given function call is free from performance pitfalls. It is fully integrated with Test
standard library's unit-testing infrastructure, and we can use it like other Test
macros e.g. @test
:
julia> @test_opt sumup(cos)
JET-test failed at REPL[1]:1 Expression: #= REPL[1]:1 =# JET.@test_opt sumup(cos) ═════ 7 possible errors found ═════ ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:5 │ runtime dispatch detected: Main.make_vals(%1::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:6 │ runtime dispatch detected: Main.eltype(%2::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:6 │ runtime dispatch detected: Main.zero(%3::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:7 │ runtime dispatch detected: iterate(%2::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:8 │ runtime dispatch detected: f::typeof(cos)(%11::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:8 │ runtime dispatch detected: (%10::Any Main.:+ %13::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:9 │ runtime dispatch detected: iterate(%2::Any, %12::Any)::Any └──────────────────── ERROR: There was an error during testing
julia> @test_opt target_modules=(@__MODULE__,) compute(30)
Test Passed
julia> using Test
julia> @testset "check type-stabilities" begin @test_opt sumup(cos) # should fail n = rand(Int) @test_opt sumup(cos, n) # should pass @test_opt target_modules=(@__MODULE__,) compute(30) # should pass @test_opt broken=true compute(30) # should pass with the "broken" annotation end
check type-stabilities: JET-test failed at REPL[4]:2 Expression: #= REPL[4]:2 =# JET.@test_opt sumup(cos) ═════ 7 possible errors found ═════ ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:5 │ runtime dispatch detected: Main.make_vals(%1::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:6 │ runtime dispatch detected: Main.eltype(%2::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:6 │ runtime dispatch detected: Main.zero(%3::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:7 │ runtime dispatch detected: iterate(%2::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:8 │ runtime dispatch detected: f::typeof(cos)(%11::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:8 │ runtime dispatch detected: (%10::Any Main.:+ %13::Any)::Any └──────────────────── ┌ sumup(f::typeof(cos)) @ Main ./REPL[3]:9 │ runtime dispatch detected: iterate(%2::Any, %12::Any)::Any └──────────────────── Test Summary: | Pass Fail Broken Total Time check type-stabilities | 2 1 1 4 0.1s ERROR: Some tests did not pass: 2 passed, 1 failed, 0 errored, 1 broken.
Integration with Cthulhu
If you identify inference problems, you may want to fix them. Cthulhu can be a useful tool for gaining more insight, and JET integrates nicely with Cthulhu.
To exploit Cthulhu, you first need to split the overall report into individual inference failures:
julia> report = @report_opt sumup(sin);
julia> rpts = JET.get_reports(report)
7-element Vector{JET.InferenceErrorReport}: RuntimeDispatchReport(runtime dispatch detected: Main.make_vals(%1::Any)::Any) RuntimeDispatchReport(runtime dispatch detected: Main.eltype(%2::Any)::Any) RuntimeDispatchReport(runtime dispatch detected: Main.zero(%3::Any)::Any) RuntimeDispatchReport(runtime dispatch detected: iterate(%2::Any)::Any) RuntimeDispatchReport(runtime dispatch detected: f::typeof(sin)(%11::Any)::Any) RuntimeDispatchReport(runtime dispatch detected: (%10::Any Main.:+ %13::Any)::Any) RuntimeDispatchReport(runtime dispatch detected: iterate(%2::Any, %12::Any)::Any)
If rpts
is a long list, consider using urpts = unique(reportkey, rpts)
to trim it. See reportkey
.
Now you can ascend
individual reports:
julia> using Cthulhu
julia> ascend(rpts[1])
Choose a call for analysis (q to quit):
runtime dispatch to make_vals(%1::Any)::Any
> sumup(::typeof(sin))
Open an editor at a possible caller of
Tuple{typeof(make_vals), Any}
or browse typed code:
> "REPL[7]", sumup: lines [4]
Browse typed code
ascend
will show the full call-chain to reach a particular runtime dispatch; in this case, it was our entry point, but in other cases it may be deeper in the call graph. In this case, we've interactively moved the selector >
down to the sumup
call (you cannot descend into the "runtime dispatch to..."
as there is no known code associated with it) and hit <Enter>
, at which point Cthulhu showed us that the call to make_vals(::Any)
occured only on line 4 of the definition of sumup
(which we entered at the REPL). Cthulhu is now prompting us to either open the code in an editor (which will fail in this case, since there is no associated file!) or view the type-annoted code. If we select the "Browse typed code" option we see
sumup(f) @ Main REPL[7]:1
1 function sumup(f::Core.Const(sin))::Any
2 # this function uses the non-constant global variable `n` here
3 # and it makes every succeeding operations type-unstable
4 vals::Any = make_vals(n::Any)::Any
5 s::Any = zero(eltype(vals::Any)::Any)::Any
6 for v::Any in vals::Any::Any
7 (s::Any += f::Core.Const(sin)(v::Any)::Any)::Any
8 end
9 return s::Any
10 end
Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark.
⋮
with red highlighting to indicate the non-inferable arguments.
For more information, you're encouraged to read Cthulhu's documentation, which includes a video tutorial better-suited to this interactive tool.
Entry Points
Interactive Entry Points
The optimization analysis offers interactive entry points that can be used in the same way as @report_call
and report_call
:
JET.@report_opt
— Macro@report_opt [jetconfigs...] f(args...)
Evaluates the arguments to a function call, determines their types, and then calls report_opt
on the resulting expression.
The general configurations and the optimization analysis specific configurations can be specified as an optional argument.
JET.report_opt
— Functionreport_opt(f, [types]; jetconfigs...) -> JETCallResult
report_opt(tt::Type{<:Tuple}; jetconfigs...) -> JETCallResult
report_opt(mi::Core.MethodInstance; jetconfigs...) -> JETCallResult
Analyzes a function call with the given type signature to detect optimization failures and unresolved method dispatches.
The general configurations and the optimization analysis specific configurations can be specified as a keyword argument.
See the documentation of the optimization analysis for more details.
Test
Integration
As with the default error analysis, the optimization analysis also offers the integration with Test
standard library:
JET.@test_opt
— Macro@test_opt [jetconfigs...] [broken=false] [skip=false] f(args...)
Runs @report_opt jetconfigs... f(args...)
and tests that the function call f(args...)
is free from optimization failures and unresolved method dispatches that @report_opt
can detect.
As with @report_opt
, the general configurations and optimization analysis specific configurations can be specified as an optional argument:
julia> function f(n)
r = sincos(n)
# `println` is full of runtime dispatches,
# but we can ignore the corresponding reports from `Base`
# with the `target_modules` configuration
println(r)
return r
end;
julia> @test_opt target_modules=(@__MODULE__,) f(10)
Test Passed
Expression: #= REPL[3]:1 =# JET.@test_call analyzer = JET.OptAnalyzer target_modules = (#= REPL[3]:1 =# @__MODULE__(),) f(10)
Like @test_call
, @test_opt
is fully integrated with the Test
standard library. See @test_call
for the details.
JET.test_opt
— Functiontest_opt(f, [types]; broken::Bool = false, skip::Bool = false, jetconfigs...)
test_opt(tt::Type{<:Tuple}; broken::Bool = false, skip::Bool = false, jetconfigs...)
Runs report_opt
on a function call with the given type signature and tests that it is free from optimization failures and unresolved method dispatches that report_opt
can detect. Except that it takes a type signature rather than a call expression, this function works in the same way as @test_opt
.
Top-level Entry Points
By default, JET doesn't offer top-level entry points for the optimization analysis, because it's usually used for only a selective portion of your program. But if you want you can just use report_file
or similar top-level entry points with specifying analyzer = OptAnalyzer
configuration in order to apply the optimization analysis on a top-level script, e.g. report_file("path/to/file.jl"; analyzer = OptAnalyzer)
.
Configurations
In addition to the general configurations, the optimization analysis can take the following specific configurations:
JET.OptAnalyzer
— TypeEvery entry point of optimization analysis can accept any of the general configurations as well as the following additional configurations that are specific to the optimization analysis.
skip_noncompileable_calls::Bool = true
:
Julia's runtime dispatch is "powerful" because it can always compile code with concrete runtime arguments so that a "kernel" function runs very effectively even if it's called from a type-instable call site. This means, we (really) often accept that some parts of our code are not inferred statically, and rather we want to just rely on information that is only available at runtime. To model this programming style, the optimization analyzer by default does NOT report any optimization failures or runtime dispatches detected within non-concrete calls (more correctly, "non-compileable" calls are ignored: see also the note below). We can turn off thisskip_noncompileable_calls
configuration to get type-instabilities within those calls.# the following examples are adapted from https://docs.julialang.org/en/v1/manual/performance-tips/#kernel-functions julia> function fill_twos!(a) for i = eachindex(a) a[i] = 2 end end; julia> function strange_twos(n) a = Vector{rand(Bool) ? Int64 : Float64}(undef, n) fill_twos!(a) return a end; # by default, only type-instabilities within concrete call (i.e. `strange_twos(3)`) are reported # and those within non-concrete calls (`fill_twos!(a)`) are not reported julia> @report_opt strange_twos(3) ═════ 2 possible errors found ═════ ┌ strange_twos(n::Int64) @ Main ./REPL[23]:2 │ runtime dispatch detected: %33::Type{Vector{_A}} where _A(undef, n::Int64)::Vector └──────────────────── ┌ strange_twos(n::Int64) @ Main ./REPL[23]:3 │ runtime dispatch detected: fill_twos!(%34::Vector)::Any └──────────────────── # we can get reports from non-concrete calls with `skip_noncompileable_calls=false` julia> @report_opt skip_noncompileable_calls=false strange_twos(3) ┌ strange_twos(n::Int64) @ Main ./REPL[23]:3 │┌ fill_twos!(a::Vector) @ Main ./REPL[22]:3 ││┌ setindex!(A::Vector, x::Int64, i1::Int64) @ Base ./array.jl:1014 │││ runtime dispatch detected: convert(%5::Any, x::Int64)::Any ││└──────────────────── │┌ fill_twos!(a::Vector) @ Main ./REPL[22]:3 ││ runtime dispatch detected: ((a::Vector)[%13::Int64] = 2::Any) │└──────────────────── ┌ strange_twos(n::Int64) @ Main ./REPL[23]:2 │ runtime dispatch detected: %33::Type{Vector{_A}} where _A(undef, n::Int64)::Vector └──────────────────── ┌ strange_twos(n::Int64) @ Main ./REPL[23]:3 │ runtime dispatch detected: fill_twos!(%34::Vector)::Any └────────────────────
Non-compileable calls Julia runtime system sometimes generate and execute native code of an abstract call. More technically, when some of call arguments are annotated as
@nospecialize
, Julia compiles the call even if those@nospecialize
d arguments aren't fully concrete.skip_noncompileable_calls = true
also respects this behavior, i.e. doesn't skip compileable abstract calls:julia> function maybesin(x) if isa(x, Number) return sin(x) else return 0 end end; julia> report_opt((Vector{Any},)) do xs for x in xs # This `maybesin` call is dynamically dispatched since `maybesin(::Any)` # is not compileable. Therefore, JET by default will only report the # runtime dispatch of `maybesin` while it will not report the runtime # dispatch within `maybesin(::Any)`. s = maybesin(x) s !== 0 && return s end end ═════ 1 possible error found ═════ ┌ (::var"#3#4")(xs::Vector{Any}) @ Main ./REPL[3]:7 │ runtime dispatch detected: maybesin(%19::Any)::Any └──────────────────── julia> function maybesin(@nospecialize x) # mark `x` with `@nospecialize` if isa(x, Number) return sin(x) else return 0 end end; julia> report_opt((Vector{Any},)) do xs for x in xs # Now `maybesin` is marked with `@nospecialize` allowing `maybesin(::Any)` # to be resolved statically and compiled. Thus JET will not report the # runtime dispatch of `maybesin(::Any)`, although it now reports the # runtime dispatch _within_ `maybesin(::Any)`. s = maybesin(x) s !== 0 && return s end end ═════ 1 possible error found ═════ ┌ (::var"#5#6")(xs::Vector{Any}) @ Main ./REPL[5]:7 │┌ maybesin(x::Any) @ Main ./REPL[4]:3 ││ runtime dispatch detected: sin(%3::Number)::Any │└────────────────────
function_filter = @nospecialize(f)->true
:
A predicate which takes a function object and returnsfalse
to skip runtime dispatch analysis on calls of the function. This configuration is particularly useful when your program uses a function that is intentionally designed to use runtime dispatch.# ignore `Core.Compiler.widenconst` calls (since it's designed to be runtime-dispatched): julia> function_filter(@nospecialize f) = f !== Core.Compiler.widenconst; julia> @test_opt function_filter=function_filter f(args...) ...
skip_unoptimized_throw_blocks::Bool = true
:
By default, Julia's native compilation pipeline intentionally disables inference (and so succeeding optimizations too) on "throw blocks", which are code blocks that will eventually lead tothrow
calls, in order to ease the compilation latency problem, a.k.a. "first-time-to-plot". Accordingly, the optimization analyzer also ignores any performance pitfalls detected within those blocks since we usually don't mind if code involved with error handling isn't optimized. Ifskip_unoptimized_throw_blocks
is set tofalse
, it doesn't ignore them and will report type instabilities detected within "throw blocks".See also https://github.com/JuliaLang/julia/pull/35982.
# by default, unoptimized "throw blocks" are not analyzed julia> @test_opt sin(10) Test Passed Expression: #= none:1 =# JET.@test_opt sin(10) # we can turn on the analysis on unoptimized "throw blocks" with `skip_unoptimized_throw_blocks=false` julia> @test_opt skip_unoptimized_throw_blocks=false sin(10) JET-test failed at none:1 Expression: #= REPL[6]:1 =# JET.@test_call analyzer = JET.OptAnalyzer skip_unoptimized_throw_blocks = false sin(10) ═════ 1 possible error found ═════ ┌ @ math.jl:1221 Base.Math.sin(xf) │┌ @ special/trig.jl:39 Base.Math.sin_domain_error(x) ││┌ @ special/trig.jl:28 Base.Math.DomainError(x, "sin(x) is only defined for finite x.") │││ runtime dispatch detected: Base.Math.DomainError(x::Float64, "sin(x) is only defined for finite x.")::Any ││└────────────────────── ERROR: There was an error during testing # we can also turns off the heuristic itself julia> @test_opt unoptimize_throw_blocks=false skip_unoptimized_throw_blocks=false sin(10) Test Passed Expression: #= REPL[7]:1 =# JET.@test_call analyzer = JET.OptAnalyzer unoptimize_throw_blocks = false skip_unoptimized_throw_blocks = false sin(10)
- 1Technically, it's fully integrated with Julia's method invalidation system.