Announcing…
This experimental package provides the @stable
macro to enforce that functions have type stable return values.
using DispatchDoctor: @stable
@stable function relu(x)
if x > 0
return x
else
return 0.0
end
end
Calling this function will throw an error for any type instability:
julia> relu(1.0)
1.0
julia> relu(0)
ERROR: TypeInstabilityError: Instability detected in function `relu`
with arguments `(Int64,)`. Inferred to be `Union{Float64, Int64}`,
which is not a concrete type.
Code which is type stable should compile away the check:
julia> @stable f(x) = x;
with @code_llvm f(1)
:
define i64 @julia_f_12055(i64 signext %"x::Int64") #0 {
top:
ret i64 %"x::Int64"
}
Meaning there is zero overhead on this particular type stability check. (Please post an issue for any known examples of this not being the case!)
You can also use @stable
on entire blocks of code, including begin-end
blocks, module
, and anonymous functions – it will propagate through everything and “stabilize” all functions. The inverse of @stable
is @unstable
which toggles it back off.
@stable begin
f() = rand(Bool) ? 0 : 1.0
module A
# Will apply to code inside modules:
g(; a, b) = a + b
# Will recursively apply to included files:
include("myfile.jl")
module B # as well as nested submodules!
# `@unstable` inverts `@stable`:
using DispatchDoctor: @unstable
@unstable h() = rand(Bool) ? 0 : 1.0
# This can also apply to code blocks:
@unstable begin
h(x::Int) = rand(Bool) ? 0 : 1.0
# ^ And target specific methods
end
end
end
end
All methods in the block will be wrapped with the type stability check:
julia> f()
ERROR: TypeInstabilityError: Instability detected in function `f`.
Inferred to be `Union{Float64, Int64}`, which is not a concrete type.
(Tip: in the REPL, you must wrap modules with @eval
, because the REPL has special handling of the module
keyword.)
You can disable stability errors for a single scope with the allow_unstable
context:
julia> @stable f(x) = x > 0 ? x : 0.0
julia> allow_unstable() do
f(1)
end
1
although this will error if you try to use it simultaneously from two separate threads.
Note that instability errors are also automatically skipped during precompilation.
Note!
@stable
will have no effect on code if it is:
- Within an
@unstable
block - Within a macro definition
- A function inside another function (i.e., a closure) – although the wrapping function will still get checked (see this example)
- A generated function
- Within an
@eval
statement - Within a
quote
block - If the function name is an expression (such as parameterized functions like
MyType{T}(args...) = ...
)
You can safely use @stable
to wrap any of these cases; they will simply be ignored. Although, if you use @stable
internally in any of these cases, (like calling @stable
directly on a closure), then it will apply.
And of course if you find other cases of @stable
not playing well with certain Julia features, please raise an issue.
Also, @stable
is a no-op in unsupported Julia versions (before 1.10) (and also any future Julia versions I haven’t tested yet)
Credits
Many thanks to @Elrod, @thofma, @matthias314 and others for tips on Improving speed of runtime dispatch detector