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
@@ -404,7 +404,7 @@ the results are represented as a tree, where each node links to its callers.)
404
404
In contrast, the first entry is responsible for just two invalidations.
405
405
406
406
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)`.
408
408
We can see which method this is by the following:
409
409
410
410
```julia-repl
@@ -418,21 +418,23 @@ or directly as
418
418
julia> tree = trees[end]
419
419
insert (::Type{X})(x::Real) where X<:FixedPoint in FixedPointNumbers at /home/tim/.julia/packages/FixedPointNumbers/w2pxG/src/FixedPointNumbers.jl:51 invalidated:
420
420
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
424
424
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
425
427
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
428
430
429
-
julia> mi.def
431
+
julia> node.mi.def
430
432
(::Type{T})(x::Number) where T<:AbstractChar in Base at char.jl:48
431
433
```
432
434
433
435
`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.)
436
438
437
439
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.
@@ -463,33 +465,8 @@ There are good reasons to believe that the right way to fix such methods is to e
463
465
If this gets changed in Julia, then all the ones marked "ambiguous" should magically disappear.
464
466
Consequently, we can turn our attention to other cases.
465
467
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})
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`.
493
470
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.
494
471
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.
495
472
@@ -498,65 +475,42 @@ It is not clear why such methods should be invalidating, and this may be a Julia
498
475
499
476
### Partial specialization
500
477
501
-
If you try
478
+
Let's return now to
502
479
503
480
```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
509
484
485
+
julia> node = tree[:backedges, 1]
486
+
MethodInstance for reduce_empty(::Function, ::Type{T} where T) at depth 0 with 137 children
510
487
```
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.
517
488
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:
520
491
521
492
```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
525
500
```
526
501
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:
531
504
532
505
```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
543
508
```
544
509
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.
555
513
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.
560
514
561
515
### Some summary statistics
562
516
@@ -566,17 +520,17 @@ Let's go back to our table above, and count the number of invalidations in each
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.).
582
536
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.
635
589
A little prodding as done above reveals that this corresponds to the definition
636
590
637
591
```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
647
594
648
-
julia> mi.def
595
+
julia> node.mi.def
649
596
reduce_empty(op, T) in Base at reduce.jl:309
650
597
```
651
598
@@ -691,43 +638,25 @@ reduce_empty(op::F, ::Type{T}) where {F,T} = _empty_reduce_error()
691
638
692
639
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.
693
640
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:
715
642
716
643
```julia-repl
717
644
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})
731
660
⋮
732
661
```
733
662
@@ -778,6 +707,9 @@ However, it's a bit uglier than the original;
778
707
perhaps a nicer approach would be to allow one to supply `init` as a keyword argument to `maximum` itself.
779
708
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.
780
709
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
+
781
713
## Summary
782
714
783
715
Julia's remarkable flexibility and outstanding code-generation open many new horizons.
0 commit comments