Skip to content

Commit 6e17db8

Browse files
committed
Cleanups
1 parent 68c37d6 commit 6e17db8

File tree

1 file changed

+19
-18
lines changed

1 file changed

+19
-18
lines changed

blog/2020/05/invalidations.md

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ end
4242
Here I've defined two functions; `f` is a function with two very simple methods,
4343
and `arrayf` is a function with just one method that supports `Any` argument at all.
4444
When you call `applyf`, Julia will compile _specialized_ versions on demand for the
45-
particular types of `container` that you're using at that moment (even though I didn't
46-
use a single type in its definition!).
45+
particular types of `container` that you're using at that moment, even though I didn't
46+
specify a single type in its definition.
4747

4848
If you call `applyf([100, 200])`, Julia will compile and use a version of `applyf` specifically
4949
created for `Vector{Int}`. Since the element type (`Int`) is a part of the `container`'s type, it
@@ -80,12 +80,12 @@ In this case, you can see that Julia knew those two `arrayref` statements would
8080
return a `Bool`, and since it knows the value of `f(::Bool)` it just went
8181
ahead and computed the result at compile time for you.
8282

83-
At the end of these experiments, hidden away in Julia's "method cache" there will
83+
After calling `applyf` with both sets of arguments, hidden away in Julia's "method cache" there will
8484
be two `MethodInstance`s of `applyf`, one specialized for `Vector{Int}` and the other specialized for `Vector{Bool}`.
8585
You don't normally see these, but Julia manages them for you; anytime you write
8686
code that calls `applyf`, it checks to see if this previous compilation work can be reused.
8787

88-
For the purpose of this blog post, things start to get especially interesting if we try the following:
88+
For the purpose of this blog post, things start to get especially interesting if use a container that can store elements with different types (here, type `Any`):
8989

9090
```julia-repl
9191
julia> c = Any[1, false];
@@ -215,7 +215,7 @@ julia> @btime applyf($c)
215215
It's almost a tenfold difference.
216216
If `applyf` is performance-critical, you'll be very happy that Julia tries to give you the best version it can, given the available information.
217217
But this leaves the door open to invalidation, which means recompilation the next time you use `applyf`.
218-
If method invalidation happens often, this might contribute to making Julia "feel" sluggish.
218+
If method invalidation happens often, this might contribute to making Julia feel sluggish.
219219

220220
## How common is method invalidation?
221221

@@ -405,7 +405,7 @@ This list is ordered from least- to most-consequential in terms of total number
405405
The final entry, for `(::Type{X})(x::Real) where X<:FixedPoint`, triggered the invalidation of what nominally appear to be more than 350 `MethodInstance`s.
406406
(There is no guarantee that these methods are all disjoint from one another;
407407
the results are represented as a tree, where each node links to its callers.)
408-
In contrast, the first entry is responsible for just two invalidations.
408+
In contrast, the first three entries are responsible for a tiny handful of invalidations.
409409

410410
One does not have to look at this list for very long to see that the majority of the invalidated methods are due to [method ambiguity].
411411
Consider the line `...char.jl:48 with MethodInstance for (::Type{T} where T<:AbstractChar)(::Int32)`.
@@ -471,11 +471,11 @@ Consequently, we can turn our attention to other cases.
471471

472472
For now we'll skip `trees[end-1]`, and consider `tree[end-2]` which results from defining
473473
`sizeof(::Type{X}) where X<:FixedPoint`.
474-
Simply put, this looks like a method that we don't need; perhaps it dates from some confusion, or an era where perhaps it was necessary.
474+
There's a perfectly good default definition, and this looks like a method that we don't need; perhaps it dates from some confusion, or an era where perhaps it was necessary.
475475
So we've discovered an easy place where a developer could do something to productively decrease the number of invalidations, in this case by just deleting the method.
476476

477-
You'll also notice one example where the new method is *less specific*.
478-
It is not clear why such methods should be invalidating, and this may be a Julia bug.
477+
Rarely (in other packages) you'll notice cases where the new method is *less specific*.
478+
It is not clear why such methods should be invalidating, and this may be either a SnoopCompile or Julia bug.
479479

480480
### Partial specialization
481481

@@ -490,7 +490,9 @@ julia> node = tree[:backedges, 1]
490490
MethodInstance for reduce_empty(::Function, ::Type{T} where T) at depth 0 with 137 children
491491
```
492492

493-
That certainly looks like a less specific method than the one we defined.
493+
Our new method certainly looks more specific method than the one that triggered the invalidation,
494+
so at face value this looks like one of those "necessary" invalidations we'd be hard-pressed to avoid.
495+
However, appearances can be deceptive.
494496
We can look at the callers of this `reduce_empty` method:
495497

496498
```julia-repl
@@ -523,7 +525,7 @@ Let's go back to our table above, and count the number of invalidations in each
523525
| Package | more specific | less specific | ambiguous |
524526
|:------- | ------------------:| --------:| -----:|
525527
| Example | 0 | 0 | 0 | 0 |
526-
| Revise | 6 | 0 | 0 |
528+
| Revise | 7 | 0 | 0 |
527529
| FixedPointNumbers | 170 | 0 | 387 |
528530
| SIMD | 3903 | 0 | 187 |
529531
| StaticArrays | 989 | 0 | 3133 |
@@ -537,8 +539,8 @@ Let's go back to our table above, and count the number of invalidations in each
537539
| DifferentialEquations | 5152 | 18 | 6218 |
538540

539541
The numbers in this table don't add up to those in the first, for a variety of reasons (here there is no attempt to remove duplicates, here we don't count "mt_cache" invalidations which were included in the first table, etc.).
540-
In general terms, the last two columns should probably be fixed by changes in how Julia does invalidations; the first column indicates invalidations that should either be fixed in packages, Julia's own code, or will need to remain unfixed.
541-
The good news is that these counts reveal that much will likely be fixed by "automated" means.
542+
In general terms, the last two columns should probably be fixed by changes in how Julia does invalidations; the first column is a mixture of ones that might be removed by changes in Julia (if they are due to partial specialization) or ones that should either be fixed in packages, Base and the standard libraries, or will need to remain unfixed.
543+
The good news is that these counts reveal that more than half of all invalidations will likely be fixed by "automated" means.
542544
However, it appears that there will need to be a second round in which package developers inspect individual invalidations to determine what, if anything, can be done to remediate them.
543545

544546
## Fixing invalidations
@@ -559,7 +561,7 @@ Here our focus is on those marked "more specific," since those are cases where i
559561
### Fixing type instabilities
560562

561563
In engineering Julia and Revise to reduce invalidations, at least two cases were fixed by resolving type-instabilities.
562-
For example, one set of invalidations happened because `CodeTracking`, a dependency of Revise's, defines new methods for `Base.PkgId`.
564+
For example, one set of invalidations happened because CodeTracking, a dependency of Revise's, defines new methods for `Base.PkgId`.
563565
It turns out that this triggered an invalidation of `_tryrequire_from_serialized`, which is used to load packages.
564566
Fortunately, it turned out to be an easy fix: one section of `_tryrequire_from_serialized` had a passage
565567

@@ -582,7 +584,7 @@ immediately after the `for` statement to fix the problem.
582584
Not only does this fix the invalidation, but it lets the compiler generate better code.
583585

584586
The other case was a call from `Pkg` of `keys` on an AbstractDict of unknown type
585-
(due to a caller's `@nospecialize` annotation).
587+
(due to partial specialization).
586588
Replacing `keys(dct)` with `Base.KeySet(dct)` (which is the default return value of `keys`) eliminated a very consequential invalidation, one that triggered seconds-long latencies in the next `Pkg` command after loading Revise.
587589
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.
588590
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.
@@ -646,7 +648,6 @@ For addressing this purely at the level of Julia code, perhaps the best approach
646648

647649
```julia-repl
648650
julia> show(node)
649-
julia> show(node)
650651
MethodInstance for reduce_empty(::Function, ::Type{T} where T)
651652
MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber})
652653
MethodInstance for reduce_empty_iter(::Base.BottomRF{typeof(max)}, ::Set{VersionNumber}, ::Base.HasEltype)
@@ -712,7 +713,7 @@ perhaps a nicer approach would be to allow one to supply `init` as a keyword arg
712713
While this is not supported on Julia versions up through 1.5, it's a feature that seems to make sense, and this analysis suggests that it might also allow developers to make code more robust against certain kinds of invalidation.
713714

714715
As this hopefully illustrates, there's often more than one way to "fix" an invalidation.
715-
Finding the best approach may require that we as a community develop experience with this novel consideration.
716+
Finding the best approach may require some experimentation.
716717

717718
## Summary
718719

@@ -721,7 +722,7 @@ These advantages come with a few costs, and here we've explored one of them, met
721722
While Julia's core developers have been aware of its cost for a long time,
722723
we're only now starting to get tools to analyze it in a manner suitable for a larger population of users and developers.
723724
Because it's not been easy to measure previously, it would not be surprising if there are numerous opportunities for improvement waiting to be discovered.
724-
One might hope that the next period of development might see significant improvement in getting packages to work together without stomping on each other's toes.
725+
One might hope that the next period of development might see significant improvement in getting packages to work together gracefully without stomping on each other's toes.
725726

726727
[Julia]: https://julialang.org/
727728
[union-splitting]: https://julialang.org/blog/2018/08/union-splitting/

0 commit comments

Comments
 (0)