Skip to content

Commit d70c02a

Browse files
committed
Better explanation of fixing inference
1 parent 700a9f9 commit d70c02a

File tree

1 file changed

+63
-18
lines changed

1 file changed

+63
-18
lines changed

blog/2020/05/invalidations.md

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -560,35 +560,74 @@ Here our focus is on those marked "more specific," since those are cases where i
560560

561561
### Fixing type instabilities
562562

563-
In engineering Julia and Revise to reduce invalidations, at least two cases were fixed by resolving type-instabilities.
564-
For example, one set of invalidations happened because CodeTracking, a dependency of Revise's, defines new methods for `Base.PkgId`.
565-
It turns out that this triggered an invalidation of `_tryrequire_from_serialized`, which is used to load packages.
566-
Fortunately, it turned out to be an easy fix: one section of `_tryrequire_from_serialized` had a passage
563+
Most of the time, Julia is very good at inferring a concrete type for each object,
564+
and when successful this eliminates the risk of invalidations.
565+
However, as illustrated by our `applyf` example, certain input types can make inference impossible.
566+
Fortunately, it's possible to write your code in ways that eliminate or reduce the impact of such invalidations.
567+
568+
An excellent resource for avoiding inference problems is Julia's [performance tips] page.
569+
That these tips appear on a page about performance, rather than invalidation, is a happy state of affairs:
570+
not only are you making your (or Julia's) code more robust against invalidation, you're almost certainly making it faster.
571+
572+
Virtually all the type-related tips on that page can be used to reduce invalidations.
573+
Here, we'll present a couple of examples and then focus on some of the more subtle issues.
574+
575+
#### Add annotations for containers with abstractly-typed elements
576+
577+
As the performance page indicates, when working with containers, concrete-typing is best:
578+
`Vector{Int}` will typically be faster in usage and more robust to invalidation than `Vector{Any}`.
579+
When possible, using concrete typing is highly recommended.
580+
However, there are cases where you sometimes need elements to have an abstract type.
581+
In such cases, one common fix is to annotate elements at the point of usage.
582+
583+
For instance, Julia's [IOContext] structure is defined roughly as
567584

568585
```
569-
for M in mod::Vector{Any}
570-
if PkgId(M) == modkey && module_build_id(M) === build_id
571-
return M
572-
end
586+
struct IOContext{IO_t <: IO} <: AbstractPipe
587+
io::IO_t
588+
dict::ImmutableDict{Symbol, Any}
573589
end
574590
```
575591

576-
and since `M` had type `Any`, the compiler couldn't predict which version of `PkgId` would be called.
577-
It sufficed to add
592+
There are good reasons to use a value-type of `Any`, but that makes it impossible for the compiler to infer the type of any object looked up in an IOContext.
593+
Fortunately, you can help!
594+
For example, the documentation specifies that the `:color` setting should be a `Bool`, and since it appears in documentation it's something we can safely enforce.
595+
Changing
578596

579597
```
580-
M = M::Module
598+
iscolor = get(io, :color, false)
581599
```
582600

583-
immediately after the `for` statement to fix the problem.
584-
Not only does this fix the invalidation, but it lets the compiler generate better code.
601+
to either of
602+
603+
```
604+
iscolor = get(io, :color, false)::Bool # the rhs is Bool-valued
605+
iscolor::Bool = get(io, :color, false) # `iscolor` must be Bool throughout scope
606+
```
607+
608+
makes computations performed with `iscolor` robust against invalidation.
609+
For example, the SIMD package defines a new method for `!`, which is typically union-split on non-inferred arguments.
610+
Julia's `with_output_color` function computes `!iscolor`,
611+
and without the type annotation it becomes vulnerable to invalidation.
612+
Adding the annotation fixed hundreds of invalidations in methods that directly or indirectly call `with_output_color`.
613+
(Such annotations are not necessary for uses like `if iscolor...`, `iscolor ? a : b`, or `iscolor && return nothing` because these are built into the language and do not rely on dispatch.)
614+
615+
#### Force runtime dispatch
616+
617+
In some circumstances it may not be possible to add a type annotation: `f(x)::Bool` will throw an error if `f(x)` does not return a `Bool`.
618+
To avoid breaking generic code, sometimes it's necessary to have an alternate strategy.
619+
620+
621+
Above we looked at cases where Julia can't specialize the code due to containers with abstract elements.
622+
There are also circumstances where specialization is undesirable because it would force Julia to compile too many variants.
623+
For example, consider Julia's `methods(f::Function)`:
624+
by default, Julia specializes `myfunc(f::Function)` for the particular function `f`, but for something like `methods` which might be called hundreds or thousands of times with different `f`s and which is not performance-critical, that amount of compilation would be counterproductive.
625+
Consequently, Julia allows you to annotate an argument with `@nospecialize`, and so `methods` is defined as `function methods(@nospecialize(f), ...)`.
626+
627+
`@nospecialize` is a very important and effective tool for reducing compiler latency, but ironically it can also make you more vulnerable to invalidation.
628+
For example, consider the following definition:
585629

586-
The other case was a call from `Pkg` of `keys` on an AbstractDict of unknown type
587-
(due to inference failure).
588-
Resolving that inference problem eliminated a very consequential invalidation, one that triggered seconds-long latencies in the next `Pkg` command after loading Revise.
589630

590-
The benefits of this change in Pkg's code went far beyond helping Revise; any package depending on the OrderedCollections package (which is a dependency of Revise and what actually triggered the invalidation) got the same benefit.
591-
With these and a few other relatively simple changes, loading Revise no longer forces Julia to recompile much of Pkg's code the next time you try to update packages.
592631

593632
### Redirecting call chains
594633

@@ -716,6 +755,10 @@ While this is not supported on Julia versions up through 1.5, it's a feature tha
716755
As this hopefully illustrates, there's often more than one way to "fix" an invalidation.
717756
Finding the best approach may require some experimentation.
718757

758+
## Notes
759+
760+
MethodInstances with no backedges may be called by runtime dispatch. (Not sure how those get `::Any` type annotations, though.)
761+
719762
## Summary
720763

721764
Julia's remarkable flexibility and outstanding code-generation open many new horizons.
@@ -732,3 +775,5 @@ One might hope that the next period of development might see significant improve
732775
[PRJulia]: https://github.com/JuliaLang/julia/pull/35768
733776
[PRSC]: https://github.com/timholy/SnoopCompile.jl/pull/79
734777
[method ambiguity]: https://docs.julialang.org/en/latest/manual/methods/#man-ambiguities-1
778+
[IOContext]: https://docs.julialang.org/en/latest/base/io-network/#Base.IOContext
779+
[performance tips]: https://docs.julialang.org/en/latest/manual/performance-tips/

0 commit comments

Comments
 (0)