Skip to content

Commit f0cb3a2

Browse files
committed
Discuss the effect of lack of specialization in the reduce_empty example
1 parent 72bc02c commit f0cb3a2

File tree

1 file changed

+35
-16
lines changed

1 file changed

+35
-16
lines changed

blog/2020/05/invalidations.md

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ insert (::Type{X})(x::Real) where X<:FixedPoint in FixedPointNumbers at /home/ti
415415
6 mt_cache
416416
417417
418-
julia> mi, invtree = tree[:backedges,1]
418+
julia> mi, node = tree[:backedges,1]
419419
MethodInstance for (::Type{T} where T<:AbstractChar)(::Int32) => MethodInstance for +(::AbstractChar, ::UInt8) at depth 0 with 157 children
420420
421421
julia> mi.def
@@ -465,7 +465,21 @@ insert reduce_empty(::typeof(Base.mul_prod), ::Type{F}) where F<:FixedPoint in F
465465

466466
`reduce_empty(::typeof(Base.mul_prod), ::Type{F}) where F<:FixedPoint` is strictly more specific than `reduce_empty(::Function, ::Type{T} where T)`.
467467
This might look like one of those "necessary" invalidations.
468-
Below we'll analyze this in far greater detail and discover some possible fixes.
468+
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})`:
469+
470+
```julia-repl
471+
julia> mi, node = tree[:backedges, 1]
472+
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
473+
474+
julia> node.mi # this is the MethodInstance that called `mi`
475+
MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber})
476+
477+
julia> typeintersect(tree.method.sig, node.mi.specTypes)
478+
Union{}
479+
```
480+
481+
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.
482+
Thus, while it looks like a case of greater specificity, in fact this is more analogous to the "partial specialization" described below.
469483

470484
Moving backward another step, we get to `sizeof(::Type{X}) where X<:FixedPoint`.
471485
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.
@@ -613,7 +627,7 @@ Let's return to our FixedPointNumbers `reduce_empty` example above.
613627
A little prodding as done above reveals that this corresponds to the definition
614628

615629
```julia-repl
616-
julia> mi, invtree = tree[:backedges, 1]
630+
julia> mi, node = tree[:backedges, 1]
617631
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
618632
619633
julia> mi
@@ -635,35 +649,40 @@ which indicates that it is the fallback method for reducing over an empty collec
635649
julia> op = Base.BottomRF(Base.max)
636650
Base.BottomRF{typeof(max)}(max)
637651
638-
julia> Base.reduce_empty(op, VERSION)
652+
julia> Base.reduce_empty(op, VersionNumber)
639653
ERROR: ArgumentError: reducing over an empty collection is not allowed
640654
Stacktrace:
641655
[1] _empty_reduce_error() at ./reduce.jl:299
642-
[2] reduce_empty(::Function, ::VersionNumber) at ./reduce.jl:309
643-
[3] reduce_empty(::Base.BottomRF{typeof(max)}, ::VersionNumber) at ./reduce.jl:324
644-
[4] top-level scope at REPL[36]:1
656+
[2] reduce_empty(::Function, ::Type{T} where T) at ./reduce.jl:309
657+
[3] reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{T} where T) at ./reduce.jl:324
658+
[4] top-level scope at REPL[2]:1
645659
```
646660

647661
This essentially means that no "neutral element" has been defined for this operation and type.
648662

649-
Can we avoid this fallback?
663+
For the purposes of illustration, let's ignore the fact that this might be a case where the fix might in principle be made in the compiler.
664+
Using ordinary Julia code, can we avoid this fallback?
650665
One approach is to define the method directly: modify Julia to add
651666

652667
```
653668
reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber}) = _empty_reduce_error()
654669
```
655670

656671
so that we get the same result but don't rely on the fallback.
657-
But perhaps a better approach is to see who's calling it:
658672

659-
```julia-repl
660-
julia> invtree.mi
661-
MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{VersionNumber})
673+
Given our observation above that this *apparent* invalidating method is not actually reachable by this call chain,
674+
another approach is to force the compiler to specialize the method by adding type parameters:
662675

663-
julia> invtree.mi.def
664-
reduce_empty(op::Base.BottomRF, T) in Base at reduce.jl:324
676+
```
677+
reduce_empty(op::F, ::Type{T}) where {F,T} = _empty_reduce_error()
678+
```
665679

666-
julia> invtree.children
680+
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.
681+
682+
For addressing this purely at the level of Julia code, perhaps the best approach is to see who's calling it:
683+
684+
```julia-repl
685+
julia> node.children
667686
5-element Array{SnoopCompile.InstanceTree,1}:
668687
MethodInstance for reduce_empty_iter(::Base.BottomRF{typeof(max)}, ::Set{VersionNumber}, ::Base.HasEltype) at depth 1 with 38 children
669688
MethodInstance for reduce_empty(::Base.BottomRF{typeof(max)}, ::Type{Int64}) at depth 1 with 39 children
@@ -676,7 +695,7 @@ This illustrates how to work with an `InstanceTree`: you access the MethodInstan
676695
Let's start with the first one:
677696

678697
```julia-repl
679-
julia> node = invtree.children[1]
698+
julia> node = node.children[1]
680699
MethodInstance for reduce_empty_iter(::Base.BottomRF{typeof(max)}, ::Set{VersionNumber}, ::Base.HasEltype) at depth 1 with 38 children
681700
```
682701

0 commit comments

Comments
 (0)