You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: blog/2020/05/invalidations.md
+63-18Lines changed: 63 additions & 18 deletions
Original file line number
Diff line number
Diff line change
@@ -560,35 +560,74 @@ Here our focus is on those marked "more specific," since those are cases where i
560
560
561
561
### Fixing type instabilities
562
562
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
567
584
568
585
```
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}
573
589
end
574
590
```
575
591
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
578
596
579
597
```
580
-
M = M::Module
598
+
iscolor = get(io, :color, false)
581
599
```
582
600
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:
585
629
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.
589
630
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.
592
631
593
632
### Redirecting call chains
594
633
@@ -716,6 +755,10 @@ While this is not supported on Julia versions up through 1.5, it's a feature tha
716
755
As this hopefully illustrates, there's often more than one way to "fix" an invalidation.
717
756
Finding the best approach may require some experimentation.
718
757
758
+
## Notes
759
+
760
+
MethodInstances with no backedges may be called by runtime dispatch. (Not sure how those get `::Any` type annotations, though.)
761
+
719
762
## Summary
720
763
721
764
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
0 commit comments