Skip to content

Commit 518c5dd

Browse files
committed
fix
1 parent 7357d78 commit 518c5dd

File tree

1 file changed

+70
-138
lines changed

1 file changed

+70
-138
lines changed

blog/2020/05/invalidations.md

Lines changed: 70 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ the results are represented as a tree, where each node links to its callers.)
404404
In contrast, the first entry is responsible for just two invalidations.
405405

406406
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].
407-
Consider the line `backedges: MethodInstance for (::Type{T} where T<:AbstractChar)(::Int32) triggered...`.
407+
Consider the line `...char.jl:48 with MethodInstance for (::Type{T} where T<:AbstractChar)(::Int32)`.
408408
We can see which method this is by the following:
409409

410410
```julia-repl
@@ -418,21 +418,23 @@ or directly as
418418
julia> tree = trees[end]
419419
insert (::Type{X})(x::Real) where X<:FixedPoint in FixedPointNumbers at /home/tim/.julia/packages/FixedPointNumbers/w2pxG/src/FixedPointNumbers.jl:51 invalidated:
420420
mt_backedges: signature Tuple{Type{T} where T<:Int64,Int64} triggered MethodInstance for convert(::Type{T}, ::Int64) where T<:Int64 (1 children) ambiguous
421-
backedges: MethodInstance for (::Type{T} where T<:AbstractChar)(::Int32) triggered MethodInstance for +(::AbstractChar, ::UInt8) (157 children) ambiguous
422-
MethodInstance for (::Type{T} where T<:AbstractChar)(::UInt32) triggered MethodInstance for (::Type{T} where T<:AbstractChar)(::UInt32) (197 children) ambiguous
423-
6 mt_cache
421+
backedges: superseding (::Type{T})(x::Number) where T<:AbstractChar in Base at char.jl:48 with MethodInstance for (::Type{T} where T<:AbstractChar)(::Int32) (187 children) ambiguous
422+
superseding (::Type{T})(x::Number) where T<:AbstractChar in Base at char.jl:48 with MethodInstance for (::Type{T} where T<:AbstractChar)(::UInt32) (198 children) ambiguous
423+
3 mt_cache
424424
425+
julia> tree.method
426+
(::Type{X})(x::Real) where X<:FixedPoint in FixedPointNumbers at /home/tim/.julia/packages/FixedPointNumbers/w2pxG/src/FixedPointNumbers.jl:51
425427
426-
julia> mi, node = tree[:backedges,1]
427-
MethodInstance for (::Type{T} where T<:AbstractChar)(::Int32) => MethodInstance for +(::AbstractChar, ::UInt8) at depth 0 with 157 children
428+
julia> node = tree[:backedges,1]
429+
MethodInstance for (::Type{T} where T<:AbstractChar)(::Int32) at depth 0 with 187 children
428430
429-
julia> mi.def
431+
julia> node.mi.def
430432
(::Type{T})(x::Number) where T<:AbstractChar in Base at char.jl:48
431433
```
432434

433435
`trees[end]` selected the last (most consequential) method and the invalidations it triggered; indexing this with `:backedges` selected the category (`:mt_backedges`, `:backedges`, or `:mt_cache`), and the integer index selected the particular entry from that category.
434-
This returns a pair `MethodInstance => InstanceTree`, where the latter is a type encoding the tree.
435-
We'll see how to work with `InstanceTree`s in a moment, for now we want to focus on the `mi` portion of that pair.
436+
This returns an `InstanceTree`, where the latter is a type encoding the tree.
437+
(`:mt_backedges` will return a `sig=>node` pair, where `sig` is the invalidated signature.)
436438

437439
You may find it surprising that this method signature is ambiguous with `(::Type{X})(x::Real) where X<:FixedPoint`: after all, an `AbstractChar` is quite different from a `FixedPoint` number.
438440
We can discover why with
@@ -441,10 +443,10 @@ We can discover why with
441443
julia> tree.method.sig
442444
Tuple{Type{X},Real} where X<:FixedPoint
443445
444-
julia> mi.specTypes
446+
julia> node.mi.specTypes
445447
Tuple{Type{T} where T<:AbstractChar,Int32}
446448
447-
julia> typeintersect(tree.method.sig, mi.specTypes)
449+
julia> typeintersect(tree.method.sig, node.mi.specTypes)
448450
Tuple{Type{Union{}},Int32}
449451
```
450452

@@ -463,33 +465,8 @@ There are good reasons to believe that the right way to fix such methods is to e
463465
If this gets changed in Julia, then all the ones marked "ambiguous" should magically disappear.
464466
Consequently, we can turn our attention to other cases.
465467

466-
Let's look at the next item up the list:
467-
468-
```julia-repl
469-
julia> tree = trees[end-1]
470-
insert reduce_empty(::typeof(Base.mul_prod), ::Type{F}) where F<:FixedPoint in FixedPointNumbers at /home/tim/.julia/packages/FixedPointNumbers/w2pxG/src/FixedPointNumbers.jl:225 invalidated:
471-
backedges: MethodInstance for reduce_empty(::Function, ::Type{T} where T) triggered MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber}) (136 children) more specific
472-
```
473-
474-
`reduce_empty(::typeof(Base.mul_prod), ::Type{F}) where F<:FixedPoint` is strictly more specific than `reduce_empty(::Function, ::Type{T} where T)`.
475-
This might look like one of those "necessary" invalidations.
476-
However, even though it's marked "more specific," the new method `reduce_empty(::typeof(Base.add_sum), ::Type{F}) where F<:FixedPoint` can't be reached from a call `reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber})`:
477-
478-
```julia-repl
479-
julia> mi, node = tree[:backedges, 1]
480-
MethodInstance for reduce_empty(::Function, ::Type{T} where T) => MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber}) at depth 0 with 143 children
481-
482-
julia> node.mi # this is the MethodInstance that called `mi`
483-
MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber})
484-
485-
julia> typeintersect(tree.method.sig, node.mi.specTypes)
486-
Union{}
487-
```
488-
489-
This is is a consequence of the fact that Julia's compiler has chosen not to specialize the argument types, and `reduce_empty(::Function, ::Type{T} where T)` is broader than what could be determined from the caller.
490-
Thus, while it looks like a case of greater specificity, in fact this is more analogous to the "partial specialization" described below.
491-
492-
Moving backward another step, we get to `sizeof(::Type{X}) where X<:FixedPoint`.
468+
For now we'll skip `trees[end-1]`, and consider `tree[end-2]` which results from defining
469+
`sizeof(::Type{X}) where X<:FixedPoint`.
493470
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.
494471
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.
495472

@@ -498,65 +475,42 @@ It is not clear why such methods should be invalidating, and this may be a Julia
498475

499476
### Partial specialization
500477

501-
If you try
478+
Let's return now to
502479

503480
```julia-repl
504-
julia> trees = invalidation_trees(@snoopr using StaticArrays)
505-
```
506-
507-
you'll see a much longer output.
508-
A large number of invalidations derive from the fact that StaticArrays defines a method for `!=`, which invalidates the fallback definition
481+
julia> tree = trees[end-1]
482+
insert reduce_empty(::typeof(Base.add_sum), ::Type{F}) where F<:FixedPoint in FixedPointNumbers at /home/tim/.julia/packages/FixedPointNumbers/w2pxG/src/FixedPointNumbers.jl:222 invalidated:
483+
backedges: superseding reduce_empty(op, T) in Base at reduce.jl:309 with MethodInstance for reduce_empty(::Function, ::Type{T} where T) (137 children) more specific
509484
485+
julia> node = tree[:backedges, 1]
486+
MethodInstance for reduce_empty(::Function, ::Type{T} where T) at depth 0 with 137 children
510487
```
511-
!=(x, y) = !(x == y)
512-
```
513-
514-
Since such definitions account for hundreds of nominal invalidations, it would be well worth considering whether it is possible to delete the custom `!=` methods.
515-
For example, if they are purely for internal use you could modify each caller to
516-
use the default method.
517488

518-
The vast majority of the rest appear to derive from ambiguities.
519-
However, one more interesting case we've not seen before is
489+
That certainly looks like a less specific method than the one we defined.
490+
We can look at the callers of this `reduce_empty` method:
520491

521492
```julia-repl
522-
julia> tree = trees[end-7]
523-
insert unsafe_convert(::Type{Ptr{T}}, m::Base.RefValue{FA}) where {N, T, D, FA<:FieldArray{N,T,D}} in StaticArrays at /home/tim/.julia/packages/StaticArrays/mlIi1/src/FieldArray.jl:124 invalidated:
524-
mt_backedges: signature Tuple{typeof(Base.unsafe_convert),Type{Ptr{_A}} where _A,Base.RefValue{_A} where _A} triggered MethodInstance for unsafe_convert(::Type{Ptr{Nothing}}, ::Base.RefValue{_A} where _A) (159 children) more specific
493+
julia> node.children
494+
5-element Array{SnoopCompile.InstanceTree,1}:
495+
MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber}) at depth 1 with 39 children
496+
MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{Int64}) at depth 1 with 39 children
497+
MethodInstance for mapreduce_empty(::typeof(identity), ::typeof(max), ::Type{Pkg.Resolve.FieldValue}) at depth 1 with 21 children
498+
MethodInstance for mapreduce_empty(::typeof(identity), ::Pkg.Resolve.var"#132#134"{Pkg.Resolve.var"#smx#133"{Pkg.Resolve.Graph,Pkg.Resolve.Messages}}, ::Type{Int64}) at depth 1 with 10 children
499+
MethodInstance for mapreduce_empty(::typeof(identity), ::typeof(max), ::Type{Int64}) at depth 1 with 23 children
525500
```
526501

527-
In this case, the signature that triggered the invalidation, `Base.unsafe_convert(::Type{Ptr{_A}} where _A, ::Base.RefValue{_A} where _A)`,
528-
has been only partially specified: it depends on a type parameter `_A`.
529-
Where does such a signature come from?
530-
You can extract this line with
502+
If we look at the source for these definitions, we can figure out that they'd call `reduce_empty` with one of two functions, `max` and `identity`.
503+
Neither of these is consistent with the method for `add_sum` we've defined:
531504

532505
```julia-repl
533-
julia> trigger = tree[:mt_backedges, 1]
534-
MethodInstance for unsafe_convert(::Type{Ptr{Nothing}}, ::Base.RefValue{_A} where _A) at depth 0 with 159 children
535-
536-
julia> trigger.children
537-
5-element Array{SnoopCompile.InstanceTree,1}:
538-
MethodInstance for unsafe_convert(::Type{Ptr{Nothing}}, ::Base.RefValue{_A} where _A) at depth 1 with 0 children
539-
MethodInstance for unsafe_convert(::Type{Ptr{T}}, ::Base.RefValue{Tuple{Vararg{T,N}}}) where {N, T} at depth 1 with 2 children
540-
MethodInstance for _show_default(::Base.GenericIOBuffer{Array{UInt8,1}}, ::Any) at depth 1 with 113 children
541-
MethodInstance for _show_default(::IOContext{Base.GenericIOBuffer{Array{UInt8,1}}}, ::Any) at depth 1 with 37 children
542-
MethodInstance for _show_default(::IOContext{REPL.Terminals.TTYTerminal}, ::Any) at depth 1 with 2 children
506+
julia> tree.method
507+
reduce_empty(::typeof(Base.add_sum), ::Type{F}) where F<:FixedPoint in FixedPointNumbers at /home/tim/.julia/packages/FixedPointNumbers/w2pxG/src/FixedPointNumbers.jl:222
543508
```
544509

545-
and see all the `MethodInstance`s that called this one.
546-
You'll notice three `_show_default` `MethodInstance`s with the bulk of the children here;
547-
a little digging reveals that this is defined as
548-
549-
```
550-
function _show_default(io::IO, @nospecialize(x))
551-
t = typeof(x)
552-
...
553-
end
554-
```
510+
What's happening here is that we're running up against the compiler's heuristics for specialization:
511+
it's not actually possible that any of these callers would end up calling our new method,
512+
but because the compiler decides to create a "generic" version of the method, the signature gets flagged by the invalidation machinery as matching.
555513

556-
So the `@nospecialize` annotation, designed to reduce the number of cases when `_show_default` needs to be recompiled, causes the methods *it* uses to become triggers for invalidation.
557-
So here we see that a technique that very successfully reduces latencies also has a side effect of increasing the number of invalidations.
558-
Fortunately, these cases of partial specialization also seem to count as ambiguities, and so if ambiguous matches are eliminated it should also solve partial specialization.
559-
In the statistics below, we'll lump partial specialization in with ambiguity.
560514

561515
### Some summary statistics
562516

@@ -566,17 +520,17 @@ Let's go back to our table above, and count the number of invalidations in each
566520
|:------- | ------------------:| --------:| -----:|
567521
| Example | 0 | 0 | 0 | 0 |
568522
| Revise | 6 | 0 | 0 |
569-
| FixedPointNumbers | 139 | 0 | 381 |
570-
| SIMD | 3040 | 0 | 1017 |
571-
| StaticArrays | 1382 | 13 | 2540 |
572-
| Optim | 1385 | 13 | 2941 |
573-
| Images | 1513 | 113 | 3102 |
574-
| Flux | 1177 | 49 | 4107 |
575-
| Plots | 1104 | 48 | 4604 |
576-
| DataFrames | 2725 | 0 | 2680 |
577-
| JuMP | 1549 | 14 | 5164 |
578-
| Makie | 5147 | 92 | 4145 |
579-
| DifferentialEquations | 3776 | 53 | 7419 |
523+
| FixedPointNumbers | 170 | 0 | 387 |
524+
| SIMD | 3903 | 0 | 187 |
525+
| StaticArrays | 989 | 0 | 3133 |
526+
| Optim | 1643 | 0 | 2921 |
527+
| Images | 1749 | 14 | 3671 |
528+
| Flux | 1991 | 26 | 3460 |
529+
| Plots | 1542 | 11 | 4302 |
530+
| DataFrames | 4919 | 0 | 783 |
531+
| JuMP | 2145 | 0 | 4670 |
532+
| Makie | 6233 | 46 | 5526 |
533+
| DifferentialEquations | 5152 | 18 | 6218 |
580534

581535
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.).
582536
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.
@@ -635,17 +589,10 @@ Let's return to our FixedPointNumbers `reduce_empty` example above.
635589
A little prodding as done above reveals that this corresponds to the definition
636590

637591
```julia-repl
638-
julia> tree = trees[end-1]
639-
insert reduce_empty(::typeof(Base.mul_prod), ::Type{F}) where F<:FixedPoint in FixedPointNumbers at /home/tim/.julia/packages/FixedPointNumbers/w2pxG/src/FixedPointNumbers.jl:225 invalidated:
640-
backedges: MethodInstance for reduce_empty(::Function, ::Type{T} where T) triggered MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber}) (136 children) more specific
641-
642-
julia> mi, node = tree[:backedges, 1]
643-
MethodInstance for reduce_empty(::Function, ::Type{T} where T) => MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber}) at depth 0 with 136 children
644-
645-
julia> mi
646-
MethodInstance for reduce_empty(::Function, ::Type{T} where T)
592+
julia> node = tree[:backedges, 1]
593+
MethodInstance for reduce_empty(::Function, ::Type{T} where T) at depth 0 with 137 children
647594
648-
julia> mi.def
595+
julia> node.mi.def
649596
reduce_empty(op, T) in Base at reduce.jl:309
650597
```
651598

@@ -691,43 +638,25 @@ reduce_empty(op::F, ::Type{T}) where {F,T} = _empty_reduce_error()
691638

692639
While there's little actual reason to force specialization on a method that just issues an error, in this case it does have the effect of allowing the compiler to realize that our new method is not reachable from this call path.
693640

694-
For addressing this purely at the level of Julia code, perhaps the best approach is to see who's calling it:
695-
696-
```julia-repl
697-
julia> node.children
698-
5-element Array{SnoopCompile.InstanceTree,1}:
699-
MethodInstance for reduce_empty_iter(::Base.BottomRF{typeof(max)}, ::Set{VersionNumber}, ::Base.HasEltype) at depth 1 with 38 children
700-
MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{Int64}) at depth 1 with 39 children
701-
MethodInstance for mapreduce_empty(::typeof(identity), ::typeof(max), ::Type{Pkg.Resolve.FieldValue}) at depth 1 with 21 children
702-
MethodInstance for mapreduce_empty(::typeof(identity), ::Pkg.Resolve.var"#132#134"{Pkg.Resolve.var"#smx#133"{Pkg.Resolve.Graph,Pkg.Resolve.Messages}}, ::Type{Int64}) at depth 1 with 10 children
703-
MethodInstance for mapreduce_empty(::typeof(identity), ::typeof(max), ::Type{Int64}) at depth 1 with 23 children
704-
```
705-
706-
This illustrates how to work with an `InstanceTree`: you access the MethodInstance through `.mi` and its callers through `.children`.
707-
Let's start with the first one:
708-
709-
```julia-repl
710-
julia> node = node.children[1]
711-
MethodInstance for reduce_empty_iter(::Base.BottomRF{typeof(max)}, ::Set{VersionNumber}, ::Base.HasEltype) at depth 1 with 38 children
712-
```
713-
714-
We can display this whole branch of the tree using `show(node)`:
641+
For addressing this purely at the level of Julia code, perhaps the best approach is to see who's calling it. We looked at `node.children` above, but now let's get a more expansive view:
715642

716643
```julia-repl
717644
julia> show(node)
718-
MethodInstance for reduce_empty_iter(::Base.BottomRF{typeof(max)}, ::Set{VersionNumber}, ::Base.HasEltype)
719-
MethodInstance for reduce_empty_iter(::Base.BottomRF{typeof(max)}, ::Set{VersionNumber})
720-
MethodInstance for foldl_impl(::Base.BottomRF{typeof(max)}, ::NamedTuple{(),Tuple{}}, ::Set{VersionNumber})
721-
MethodInstance for mapfoldl_impl(::typeof(identity), ::typeof(max), ::NamedTuple{(),Tuple{}}, ::Set{VersionNumber})
722-
MethodInstance for #mapfoldl#201(::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(mapfoldl), ::typeof(identity), ::typeof(max), ::Set{VersionNumber})
723-
MethodInstance for mapfoldl(::typeof(identity), ::typeof(max), ::Set{VersionNumber})
724-
MethodInstance for #mapreduce#205(::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(mapreduce), ::typeof(identity), ::typeof(max), ::Set{VersionNumber})
725-
MethodInstance for mapreduce(::typeof(identity), ::typeof(max), ::Set{VersionNumber})
726-
MethodInstance for maximum(::Set{VersionNumber})
727-
MethodInstance for set_maximum_version_registry!(::Pkg.Types.Context, ::Pkg.Types.PackageSpec)
728-
MethodInstance for collect_project!(::Pkg.Types.Context, ::Pkg.Types.PackageSpec, ::String, ::Dict{Base.UUID,Array{Pkg.Types.PackageSpec,1}})
729-
MethodInstance for collect_fixed!(::Pkg.Types.Context, ::Array{Pkg.Types.PackageSpec,1}, ::Dict{Base.UUID,String})
730-
MethodInstance for resolve_versions!(::Pkg.Types.Context, ::Array{Pkg.Types.PackageSpec,1})
645+
julia> show(node)
646+
MethodInstance for reduce_empty(::Function, ::Type{T} where T)
647+
MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber})
648+
MethodInstance for reduce_empty_iter(::Base.BottomRF{typeof(max)}, ::Set{VersionNumber}, ::Base.HasEltype)
649+
MethodInstance for reduce_empty_iter(::Base.BottomRF{typeof(max)}, ::Set{VersionNumber})
650+
MethodInstance for foldl_impl(::Base.BottomRF{typeof(max)}, ::NamedTuple{(),Tuple{}}, ::Set{VersionNumber})
651+
MethodInstance for mapfoldl_impl(::typeof(identity), ::typeof(max), ::NamedTuple{(),Tuple{}}, ::Set{VersionNumber})
652+
MethodInstance for #mapfoldl#201(::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(mapfoldl), ::typeof(identity), ::typeof(max), ::Set{VersionNumber})
653+
MethodInstance for mapfoldl(::typeof(identity), ::typeof(max), ::Set{VersionNumber})
654+
MethodInstance for #mapreduce#205(::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(mapreduce), ::typeof(identity), ::typeof(max), ::Set{VersionNumber})
655+
MethodInstance for mapreduce(::typeof(identity), ::typeof(max), ::Set{VersionNumber})
656+
MethodInstance for maximum(::Set{VersionNumber})
657+
MethodInstance for set_maximum_version_registry!(::Pkg.Types.Context, ::Pkg.Types.PackageSpec)
658+
MethodInstance for collect_project!(::Pkg.Types.Context, ::Pkg.Types.PackageSpec, ::String, ::Dict{Base.UUID,Array{Pkg.Types.PackageSpec,1}})
659+
MethodInstance for collect_fixed!(::Pkg.Types.Context, ::Array{Pkg.Types.PackageSpec,1}, ::Dict{Base.UUID,String})
731660
732661
```
733662

@@ -778,6 +707,9 @@ However, it's a bit uglier than the original;
778707
perhaps a nicer approach would be to allow one to supply `init` as a keyword argument to `maximum` itself.
779708
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.
780709

710+
As this hopefully illustrates, there's often more than one way to "fix" an invalidation.
711+
Finding the best approach may require that we as a community develop experience with this novel consideration.
712+
781713
## Summary
782714

783715
Julia's remarkable flexibility and outstanding code-generation open many new horizons.

0 commit comments

Comments
 (0)