Optimization analysis

Successful type inference and optimization are key to high-performing Julia programs. But as mentioned in the performance tips, there are cases where Julia cannot infer the types of your program well and consequently cannot optimize it well either.

There are many possible causes of such "type instabilities". The most common is the use of non-constant global variables, while probably the trickiest is the "captured variable": Julia cannot infer the type of a variable well when it is observed and modified both by an inner function and by its enclosing one. Type instabilities like these can lead to various optimization failures. One of the most common barriers to performance is known as "runtime dispatch", which happens when the compiler cannot resolve a matching method due to the lack of type information and the method must be looked up at runtime instead. Since runtime dispatch is caused by poor type information, it often indicates that the compiler also could not apply other optimizations, including inlining and scalar replacement of aggregates.

To avoid such problems, we usually inspect the output of code_typed or its family and check whether any types are not well inferred and any optimizations were unsuccessful. One problem with this workflow is that it requires enough knowledge about inference and optimization to interpret the output. Another is that these tools present only the "final" output of inference and optimization: we cannot inspect the entire call graph, so we may miss the place where a problem actually happens and how the type instability propagates from there.

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 it still requires familiarity with the Julia compiler, and it tends to be tedious.

So, why not automate it?

JET implements such an analyzer: it investigates the optimized representation of your program and automatically detects places where the compiler failed to optimize. In particular, it can find where Julia creates captured variables, where runtime dispatch happens, and where Julia gives up optimization because of an 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]:4 │ runtime dispatch detected: Main.make_vals(%1::Any)::Any └──────────────────── sumup(f::typeof(sin)) @ Main ./REPL[3]:5 │ runtime dispatch detected: Main.eltype(%2::Any)::Any └──────────────────── sumup(f::typeof(sin)) @ Main ./REPL[3]:5 │ runtime dispatch detected: Main.zero(%3::Any)::Any └──────────────────── sumup(f::typeof(sin)) @ Main ./REPL[3]:6 │ runtime dispatch detected: iterate(%2::Any)::Any └──────────────────── sumup(f::typeof(sin)) @ Main ./REPL[3]:7 │ runtime dispatch detected: f::typeof(sin)(%11::Any)::Any └──────────────────── sumup(f::typeof(sin)) @ Main ./REPL[3]:7 │ runtime dispatch detected: (%10::Any Main.:+ %13::Any)::Any └──────────────────── sumup(f::typeof(sin)) @ Main ./REPL[3]:8 │ 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]:2 │ captured variable `r` detected └──────────────────── abmult(r::Int64) @ Main ./REPL[1]:2 │ runtime dispatch detected: (%3::Any Main.:< 0)::Any └──────────────────── abmult(r::Int64) @ Main ./REPL[1]:3 │ runtime dispatch detected: Main.:-(%8::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]:4 │ 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 limit the analysis scope to a specific module context:

julia> function compute(x)  # problem: when ∑1/n exceeds `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
       endcompute (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]:5 println(x::String) @ Base ./coreio.jl:5 │ runtime dispatch detected: print(%1::IO, x::String, "\n")::Nothing └──────────────────── 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:211 @ Base ./timing.jl:214 sprint(::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String}) @ Base ./strings/io.jl:110 sprint(::Base.var"#932#933"{…}; context::Nothing, sizehint::Int64) @ Base ./strings/io.jl:117 (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:221 │ runtime dispatch detected: (%30::Any != 0)::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:223 │ runtime dispatch detected: (%68::Any != 0)::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:224 │ runtime dispatch detected: Base.prettyprint_getunits(%78::Any, %81::Int64, 1000)::Tuple{Any, Any} └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:225 │ runtime dispatch detected: (%86::Any == 1)::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:226 │ runtime dispatch detected: Int(%93::Any)::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:226 │ runtime dispatch detected: (Base._cnt_units)[%86::Any]::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:226 │ runtime dispatch detected: (%100::Any == 1)::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:226 │ runtime dispatch detected: print(io::IOBuffer, %94::Any, %95::Any, %105::String)::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:228 │ runtime dispatch detected: Float64(%116::Any)::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:228 │ runtime dispatch detected: writefixed(%117::Any, 2)::String └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:228 │ runtime dispatch detected: (Base._cnt_units)[%86::Any]::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:228 │ runtime dispatch detected: print(io::IOBuffer, %118::String, %119::Any, " allocations: ")::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:233 │ runtime dispatch detected: (%147::Any != 0)::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:239 │ runtime dispatch detected: (%178::Any != 0)::Any └──────────────────── (::Base.var"#932#933"{Nothing, Float64, Int64, Int64, Int64, Float64, Float64, Bool, String})(io::IOBuffer) @ Base ./timing.jl:246 │ runtime dispatch detected: (%216::Any != 0)::Any └────────────────────
julia> @report_opt target_modules=(Main,) compute(30) # focus on what we wrote, and no error should be reportedNo 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]:4
  │ runtime dispatch detected: Main.make_vals(%1::Any)::Any
  └────────────────────
  sumup(f::typeof(cos)) @ Main ./REPL[3]:5
  │ runtime dispatch detected: Main.eltype(%2::Any)::Any
  └────────────────────
  sumup(f::typeof(cos)) @ Main ./REPL[3]:5
  │ runtime dispatch detected: Main.zero(%3::Any)::Any
  └────────────────────
  sumup(f::typeof(cos)) @ Main ./REPL[3]:6
  │ runtime dispatch detected: iterate(%2::Any)::Any
  └────────────────────
  sumup(f::typeof(cos)) @ Main ./REPL[3]:7
  │ runtime dispatch detected: f::typeof(cos)(%11::Any)::Any
  └────────────────────
  sumup(f::typeof(cos)) @ Main ./REPL[3]:7
  │ runtime dispatch detected: (%10::Any Main.:+ %13::Any)::Any
  └────────────────────
  sumup(f::typeof(cos)) @ Main ./REPL[3]:8
  │ runtime dispatch detected: iterate(%2::Any, %12::Any)::Any
  └────────────────────
  
ERROR: There was an error during testing
julia> @test_opt target_modules=(Main,) 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=(Main,) compute(30) # should pass @test_opt broken=true compute(30) # should pass with the "broken" annotation endcheck 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]:4 │ runtime dispatch detected: Main.make_vals(%1::Any)::Any └──────────────────── sumup(f::typeof(cos)) @ Main ./REPL[3]:5 │ runtime dispatch detected: Main.eltype(%2::Any)::Any └──────────────────── sumup(f::typeof(cos)) @ Main ./REPL[3]:5 │ runtime dispatch detected: Main.zero(%3::Any)::Any └──────────────────── sumup(f::typeof(cos)) @ Main ./REPL[3]:6 │ runtime dispatch detected: iterate(%2::Any)::Any └──────────────────── sumup(f::typeof(cos)) @ Main ./REPL[3]:7 │ runtime dispatch detected: f::typeof(cos)(%11::Any)::Any └──────────────────── sumup(f::typeof(cos)) @ Main ./REPL[3]:7 │ runtime dispatch detected: (%10::Any Main.:+ %13::Any)::Any └──────────────────── sumup(f::typeof(cos)) @ Main ./REPL[3]:8 │ 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 RNG of the outermost testset: Random.Xoshiro(0x07c34f585007dbcd, 0x6f44191c5711ee67, 0xc10f10225de5ebff, 0x014edc0513245085, 0x2402ef7cb0b976a0) 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)
Tip

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_optFunction
report_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 and returns a JETCallResult containing detected optimization failures and unresolved method dispatches.

The general configurations and optimization-analysis-specific configurations can be supplied as keyword arguments.

See the documentation of the optimization analysis for details.

source

Test integration

As with the default error analysis, the optimization analysis also offers the integration with Test standard library:

JET.@test_optMacro
@test_opt [jetconfigs...] [broken=false] [skip=false] f(args...)

Runs @report_opt jetconfigs... f(args...) and records a test that passes when the call 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 supplied as optional leading configuration arguments:

julia> function f(n)
           r = sincos(n)
           # Ignore runtime dispatch reports from the `println` implementation.
           println(r)
           return r
       end;

julia> @test_opt ignored_modules=(Base,) f(10)
Test Passed
  Expression: #= REPL[3]:1 =# JET.@test_opt ignored_modules = (Base,) f(10)

Like @test_call, @test_opt integrates with the Test standard library. See @test_call for details.

source
JET.test_optFunction
test_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. It behaves like @test_opt, but accepts a type signature rather than a call expression.

source

Top-level entry points

JET doesn't offer top-level entry points for the optimization analysis, because the analysis is usually applied to a selective portion of a program rather than to a whole script or package. The top-level entry points listed in the error analysis documentation always use JETAnalyzer, and passing an analyzer through them is not supported.

To apply the optimization analysis to code that is only reachable from a top-level script, wrap that code in a function and analyze the function with report_opt.

Configurations

In addition to the general configurations, the optimization analysis can take the following specific configurations:

JET.OptAnalyzerType

Every entry point of optimization analysis accepts any of the general configurations, together with the following configurations specific to optimization analysis.


  • skip_noncompileable_calls::Bool = true:
    A call with an abstract inferred signature may not be compileable as-is, but this does not necessarily make its downstream code inefficient. At runtime, Julia dispatches using concrete argument types and can compile specialized code for a "kernel" function. The kernel can therefore run efficiently even when reached from a type-unstable call site. Idiomatic Julia code may intentionally use such boundaries, relying on information available only at runtime rather than requiring inference to continue through them.

    To model this programming style, OptAnalyzer does not, by default, report optimization failures or runtime dispatches found inside calls that Julia would not compile for their inferred signatures. Such calls are often non-concrete, although the more precise criterion is whether a call is compileable; see the note below. The entry call itself is always analyzed. Set skip_noncompileable_calls=false to include reports from inside those calls.

    The following example is adapted from Julia's kernel-function documentation:

    julia> function fill_twos!(a)
               for i = eachindex(a)
                   a[i] = 2
               end
           end;
    
    julia> function strange_twos(a::Vector)
               fill_twos!(a)
               return a
           end;
    
    # Analyze `strange_twos` with the abstract `Vector` entry signature.
    julia> report_opt(strange_twos, (Vector,))
    ═════ 1 possible error found ═════
    ┌ strange_twos(a::Vector) @ Main ./REPL[2]:2
    │ runtime dispatch detected: fill_twos!(a::Vector)::Any
    └────────────────────
    
    # Also include reports from inside non-compileable calls.
    julia> report_opt(strange_twos, (Vector,);
                      skip_noncompileable_calls=false)
    ═════ 5 possible errors found ═════
    ┌ strange_twos(a::Vector) @ Main ./REPL[2]:2
    │┌ fill_twos!(a::Vector) @ Main ./REPL[1]:3
    ││┌ setindex!(A::Vector, x::Int64, i::Int64) @ Base ./array.jl:986
    │││┌ _setindex!(A::Vector{T}, x::Any, i::Int64) where T @ Base ./array.jl:990
    ││││ runtime dispatch detected: Base.throw_boundserror(A::Vector, %12::Tuple{Int64})
    │││└────────────────────
    ││┌ setindex!(A::Vector, x::Int64, i::Int64) @ Base ./array.jl:985
    │││ runtime dispatch detected: convert(%5::Any, x::Int64)::Any
    ││└────────────────────
    ││┌ setindex!(A::Vector, x::Int64, i::Int64) @ Base ./array.jl:986
    │││ runtime dispatch detected: Base._setindex!(A::Vector, %9::Any, i::Int64)::Vector
    ││└────────────────────
    │┌ fill_twos!(a::Vector) @ Main ./REPL[1]:3
    ││ runtime dispatch detected: ((a::Vector)[%13::Int64] = 2::Any)
    │└────────────────────
    ┌ strange_twos(a::Vector) @ Main ./REPL[2]:2
    │ runtime dispatch detected: fill_twos!(a::Vector)::Any
    └────────────────────

    With the default setting, JET reports the runtime dispatch from the entry call to fill_twos!(::Vector) but omits reports from inside fill_twos!. With skip_noncompileable_calls=false, JET also reports runtime dispatches encountered while analyzing the body of fill_twos!(::Vector).

    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 @nospecialized 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 returns false 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 `Compiler.widenconst`, which intentionally uses runtime dispatch.
    julia> function_filter(@nospecialize f) = f !== Compiler.widenconst;
    
    julia> @test_opt function_filter=function_filter f(args...)
    ...

source