|
| 1 | +# Snooping on invalidations: `@snoopr` |
| 2 | + |
| 3 | +## Recording invalidations |
| 4 | + |
| 5 | +```@meta |
| 6 | +DocTestFilters = r"(REPL\[\d+\]|none):\d+" |
| 7 | +DocTestSetup = quote |
| 8 | + using SnoopCompile |
| 9 | +end |
| 10 | +``` |
| 11 | + |
| 12 | +Invalidations occur when there is a danger that new methods would supersede older methods in previously-compiled code. |
| 13 | +We can illustrate this process with the following example: |
| 14 | + |
| 15 | +```jldoctest invalidations |
| 16 | +julia> f(::Real) = 1; |
| 17 | +
|
| 18 | +julia> callf(container) = f(container[1]); |
| 19 | +
|
| 20 | +julia> call2f(container) = callf(container); |
| 21 | +``` |
| 22 | + |
| 23 | +Let's run this with different container types: |
| 24 | +```jldoctest invalidations |
| 25 | +julia> c64 = [1.0]; c32 = [1.0f0]; cabs = AbstractFloat[1.0]; |
| 26 | +
|
| 27 | +julia> call2f(c64) |
| 28 | +1 |
| 29 | +
|
| 30 | +julia> call2f(c32) |
| 31 | +1 |
| 32 | +
|
| 33 | +julia> call2f(cabs) |
| 34 | +1 |
| 35 | +``` |
| 36 | + |
| 37 | +It's important that you actually execute these methods: code doesn't get compiled until it gets run, and invalidations only affect compiled code. |
| 38 | + |
| 39 | +Now we'll define a new `f` method, one specialized for `Float64`. |
| 40 | +So we can see the consequences for the compiled code, we'll make this definition while snooping on the compiler with `@snoopr`: |
| 41 | + |
| 42 | +```jldoctest invalidations |
| 43 | +julia> trees = invalidation_trees(@snoopr f(::Float64) = 2) |
| 44 | +1-element Array{SnoopCompile.MethodInvalidations,1}: |
| 45 | + insert f(::Float64) in Main at REPL[9]:1 invalidated: |
| 46 | + backedges: 1: superseding f(::Real) in Main at REPL[2]:1 with MethodInstance for f(::Float64) (2 children) more specific |
| 47 | + 2: superseding f(::Real) in Main at REPL[2]:1 with MethodInstance for f(::AbstractFloat) (2 children) more specific |
| 48 | +``` |
| 49 | + |
| 50 | +The list of `MethodInvalidations` indicates that some previously-compiled code got invalidated. |
| 51 | +In this case, "`insert f(::Float64)`" means that a new method, for `f(::Float64)`, was added. |
| 52 | +There were two proximal triggers for the invalidation, both of which superseded the method `f(::Real)`. |
| 53 | +One of these had been compiled specifically for `Float64`, due to our `call2f(c64)`. |
| 54 | +The other had been compiled specifically for `AbstractFloat`, due to our `call2f(cabs)`. |
| 55 | + |
| 56 | +You can look at these invalidation trees in greater detail: |
| 57 | + |
| 58 | +```jldoctest invalidations |
| 59 | +julia> tree = trees[1]; |
| 60 | +
|
| 61 | +julia> root = tree.backedges[1] |
| 62 | +MethodInstance for f(::Float64) at depth 0 with 2 children |
| 63 | +
|
| 64 | +julia> show(root) |
| 65 | +MethodInstance for f(::Float64) (2 children) |
| 66 | + MethodInstance for callf(::Array{Float64,1}) (1 children) |
| 67 | + ⋮ |
| 68 | +
|
| 69 | +julia> show(root; minchildren=0) |
| 70 | +MethodInstance for f(::Float64) (2 children) |
| 71 | + MethodInstance for callf(::Array{Float64,1}) (1 children) |
| 72 | + MethodInstance for call2f(::Array{Float64,1}) (0 children) |
| 73 | +``` |
| 74 | + |
| 75 | +You can see that the sequence of invalidations proceeded all the way up to `call2f`. |
| 76 | +Examining `root2 = tree.backedges[2]` yields similar results, but for `Array{AbstractFloat,1}`. |
| 77 | + |
| 78 | +The structure of these trees can be considerably more complicated. For example, if `callf` |
| 79 | +also got called by some other method, and that method had also been executed (forcing it to be compiled), |
| 80 | +then `callf` would have multiple children. |
| 81 | +This is often seen with more complex, real-world tests: |
| 82 | + |
| 83 | +```julia |
| 84 | +julia> trees = invalidation_trees(@snoopr using SIMD) |
| 85 | +4-element Array{SnoopCompile.MethodInvalidations,1}: |
| 86 | + insert convert(::Type{Tuple{Vararg{R,N}}}, v::Vec{N,T}) where {N, R, T} in SIMD at /home/tim/.julia/packages/SIMD/Am38N/src/SIMD.jl:182 invalidated: |
| 87 | + mt_backedges: 1: signature Tuple{typeof(convert),Type{Tuple{DataType,DataType,DataType}},Any} triggered MethodInstance for Pair{DataType,Tuple{DataType,DataType,DataType}}(::Any, ::Any) (0 children) ambiguous |
| 88 | + 2: signature Tuple{typeof(convert),Type{NTuple{8,DataType}},Any} triggered MethodInstance for Pair{DataType,NTuple{8,DataType}}(::Any, ::Any) (0 children) ambiguous |
| 89 | + 3: signature Tuple{typeof(convert),Type{NTuple{7,DataType}},Any} triggered MethodInstance for Pair{DataType,NTuple{7,DataType}}(::Any, ::Any) (0 children) ambiguous |
| 90 | + |
| 91 | + insert convert(::Type{Tuple}, v::Vec{N,T}) where {N, T} in SIMD at /home/tim/.julia/packages/SIMD/Am38N/src/SIMD.jl:188 invalidated: |
| 92 | + mt_backedges: 1: signature Tuple{typeof(convert),Type{Tuple},Any} triggered MethodInstance for Distributed.RemoteDoMsg(::Any, ::Any, ::Any) (1 children) more specific |
| 93 | + 2: signature Tuple{typeof(convert),Type{Tuple},Any} triggered MethodInstance for Distributed.CallMsg{:call}(::Any, ::Any, ::Any) (1 children) more specific |
| 94 | + 3: signature Tuple{typeof(convert),Type{Tuple},Any} triggered MethodInstance for Distributed.CallMsg{:call_fetch}(::Any, ::Any, ::Any) (1 children) more specific |
| 95 | + 4: signature Tuple{typeof(convert),Type{Tuple},Any} triggered MethodInstance for Distributed.CallWaitMsg(::Any, ::Any, ::Any) (4 children) more specific |
| 96 | + 12 mt_cache |
| 97 | + |
| 98 | + insert <<(x1::T, v2::Vec{N,T}) where {N, T<:Union{Bool, Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}} in SIMD at /home/tim/.julia/packages/SIMD/Am38N/src/SIMD.jl:1061 invalidated: |
| 99 | + mt_backedges: 1: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for <<(::UInt64, ::Integer) (0 children) ambiguous |
| 100 | + 2: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for copy_chunks_rtol!(::Array{UInt64,1}, ::Integer, ::Integer, ::Integer) (0 children) ambiguous |
| 101 | + 3: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for copy_chunks_rtol!(::Array{UInt64,1}, ::Int64, ::Int64, ::Integer) (0 children) ambiguous |
| 102 | + 4: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for copy_chunks_rtol!(::Array{UInt64,1}, ::Integer, ::Int64, ::Integer) (0 children) ambiguous |
| 103 | + 5: signature Tuple{typeof(<<),UInt64,Any} triggered MethodInstance for <<(::UInt64, ::Unsigned) (16 children) ambiguous |
| 104 | + 20 mt_cache |
| 105 | + |
| 106 | + insert +(s1::Union{Bool, Float16, Float32, Float64, Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8, Ptr}, v2::Vec{N,T}) where {N, T<:Union{Float16, Float32, Float64}} in SIMD at /home/tim/.julia/packages/SIMD/Am38N/src/SIMD.jl:1165 invalidated: |
| 107 | + mt_backedges: 1: signature Tuple{typeof(+),Ptr{UInt8},Any} triggered MethodInstance for handle_err(::JuliaInterpreter.Compiled, ::JuliaInterpreter.Frame, ::Any) (0 children) ambiguous |
| 108 | + 2: signature Tuple{typeof(+),Ptr{UInt8},Any} triggered MethodInstance for #methoddef!#5(::Bool, ::typeof(LoweredCodeUtils.methoddef!), ::Any, ::Set{Any}, ::JuliaInterpreter.Frame) (0 children) ambiguous |
| 109 | + 3: signature Tuple{typeof(+),Ptr{UInt8},Any} triggered MethodInstance for #get_def#94(::Set{Tuple{Revise.PkgData,String}}, ::typeof(Revise.get_def), ::Method) (0 children) ambiguous |
| 110 | + 4: signature Tuple{typeof(+),Ptr{Nothing},Any} triggered MethodInstance for filter_valid_cachefiles(::String, ::Array{String,1}) (0 children) ambiguous |
| 111 | + 5: signature Tuple{typeof(+),Ptr{Union{Int64, Symbol}},Any} triggered MethodInstance for pointer(::Array{Union{Int64, Symbol},N} where N, ::Int64) (1 children) ambiguous |
| 112 | + 6: signature Tuple{typeof(+),Ptr{Char},Any} triggered MethodInstance for pointer(::Array{Char,N} where N, ::Int64) (2 children) ambiguous |
| 113 | + 7: signature Tuple{typeof(+),Ptr{_A} where _A,Any} triggered MethodInstance for pointer(::Array{T,N} where N where T, ::Int64) (4 children) ambiguous |
| 114 | + 8: signature Tuple{typeof(+),Ptr{Nothing},Any} triggered MethodInstance for _show_default(::IOContext{Base.GenericIOBuffer{Array{UInt8,1}}}, ::Any) (49 children) ambiguous |
| 115 | + 9: signature Tuple{typeof(+),Ptr{Nothing},Any} triggered MethodInstance for _show_default(::Base.GenericIOBuffer{Array{UInt8,1}}, ::Any) (336 children) ambiguous |
| 116 | + 10: signature Tuple{typeof(+),Ptr{UInt8},Any} triggered MethodInstance for pointer(::String, ::Integer) (1027 children) ambiguous |
| 117 | + 2 mt_cache |
| 118 | +``` |
| 119 | +
|
| 120 | +Your specific output will surely be different from this, depending on which packages you have loaded, |
| 121 | +which versions of those packages are installed, and which version of Julia you are using. |
| 122 | +In this example, there were four different methods that triggered invalidations, and the invalidated methods were in `Base`, |
| 123 | +`Distributed`, `JuliaInterpeter`, and `LoweredCodeUtils`. (The latter two were a consequence of loading `Revise`.) |
| 124 | +You can see that collectively more than a thousand independent compiled methods needed to be invalidated; indeed, the last |
| 125 | +entry alone invalidates 1027 method instances: |
| 126 | +
|
| 127 | +``` |
| 128 | +julia> sig, node = trees[end].mt_backedges[10] |
| 129 | +Pair{Any,SnoopCompile.InstanceTree}(Tuple{typeof(+),Ptr{UInt8},Any}, MethodInstance for pointer(::String, ::Integer) at depth 0 with 1027 children) |
| 130 | + |
| 131 | +julia> node |
| 132 | +MethodInstance for pointer(::String, ::Integer) at depth 0 with 1027 children |
| 133 | + |
| 134 | +julia> show(node) |
| 135 | +MethodInstance for pointer(::String, ::Integer) (1027 children) |
| 136 | + MethodInstance for repeat(::String, ::Integer) (1023 children) |
| 137 | + MethodInstance for ^(::String, ::Integer) (1019 children) |
| 138 | + MethodInstance for #handle_message#2(::Nothing, ::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}, ::typeof(Base.CoreLogging.handle_message), ::Logging.ConsoleLogger, ::Base.CoreLogging.LogLevel, ::String, ::Module, ::Symbol, ::Symbol, ::String, ::Int64) (906 children) |
| 139 | + MethodInstance for handle_message(::Logging.ConsoleLogger, ::Base.CoreLogging.LogLevel, ::String, ::Module, ::Symbol, ::Symbol, ::String, ::Int64) (902 children) |
| 140 | + MethodInstance for log_event_global!(::Pkg.Resolve.Graph, ::String) (35 children) |
| 141 | + ⋮ |
| 142 | + MethodInstance for #artifact_meta#20(::Pkg.BinaryPlatforms.Platform, ::typeof(Pkg.Artifacts.artifact_meta), ::String, ::Dict{String,Any}, ::String) (43 children) |
| 143 | + ⋮ |
| 144 | + MethodInstance for Dict{Base.UUID,Pkg.Types.PackageEntry}(::Dict) (79 children) |
| 145 | + ⋮ |
| 146 | + MethodInstance for read!(::Base.Process, ::LibGit2.GitCredential) (80 children) |
| 147 | + ⋮ |
| 148 | + MethodInstance for handle_err(::JuliaInterpreter.Compiled, ::JuliaInterpreter.Frame, ::Any) (454 children) |
| 149 | + ⋮ |
| 150 | + ⋮ |
| 151 | + ⋮ |
| 152 | + ⋮ |
| 153 | + ⋮ |
| 154 | +⋮ |
| 155 | +``` |
| 156 | + |
| 157 | +Many nodes in this tree have multiple "child" branches. |
| 158 | + |
| 159 | +## Avoiding or fixing invalidations |
| 160 | + |
| 161 | +Invalidations occur in situations like our `call2f(c64)` example, where we changed our mind about what value `f` should return for `Float64`. |
| 162 | +Julia could not have returned the newly-correct answer without recompiling the call chain. |
| 163 | + |
| 164 | +Aside from cases like these, most invalidations occur whenever new types are introduced, |
| 165 | +and some methods were previously compiled for abstract types. |
| 166 | +In some cases, this is inevitable, and the resulting invalidations simply need to be accepted as a consequence of a dynamic, updateable language. |
| 167 | +(You can often minimize invalidations by loading all your code at the beginning of your session, before triggering the compilation of more methods.) |
| 168 | +However, in many circumstances an invalidation indicates an opportunity to improve code. |
| 169 | +In our first example, note that the call `call2f(c32)` did not get invalidated: this is because the compiler |
| 170 | +knew all the specific types, and new methods did not affect any of those types. |
| 171 | +The main tips for writing invalidation-resistant code are: |
| 172 | + |
| 173 | +- use [concrete types](https://docs.julialang.org/en/latest/manual/performance-tips/#man-performance-abstract-container-1) wherever possible |
| 174 | +- write inferrable code |
| 175 | +- don't engage in [type-piracy](https://docs.julialang.org/en/latest/manual/style-guide/#Avoid-type-piracy-1) (our `c64` example is essentially like type-piracy, where we redefined behavior for a pre-existing type) |
| 176 | + |
| 177 | +Since these tips also improve performance and allow programs to behave more predictably, |
| 178 | +these guidelines are not intrusive. |
| 179 | +Indeed, searching for and eliminating invalidations can help you improve the quality of your code. |
| 180 | +In cases where invalidations occur, but you can't use concrete types (there are many valid uses of `Vector{Any}`), |
| 181 | +you can often prevent the invalidation using some additional knowledge. |
| 182 | +For example, suppose you're writing code that parses Julia's `Expr` type: |
| 183 | + |
| 184 | +```julia |
| 185 | +julia> ex = :(Array{Float32,3}) |
| 186 | +:(Array{Float32, 3}) |
| 187 | + |
| 188 | +julia> dump(ex) |
| 189 | +Expr |
| 190 | + head: Symbol curly |
| 191 | + args: Array{Any}((3,)) |
| 192 | + 1: Symbol Array |
| 193 | + 2: Symbol Float32 |
| 194 | + 3: Int64 3 |
| 195 | +``` |
| 196 | + |
| 197 | +`ex.args` is a `Vector{Any}`. |
| 198 | +However, for a `:curly` expression only certain types will be found among the arguments; you could write key portions of your code as |
| 199 | + |
| 200 | +``` |
| 201 | +a = ex.args[2] |
| 202 | +if a isa Symbol |
| 203 | + # inside this block, Julia knows `a` is a Symbol, and so methods called on `a` will be resistant to invalidation |
| 204 | + foo(a) |
| 205 | +elseif a isa Expr && length((a::Expr).args) > 2 |
| 206 | + a = a::Expr # sometimes you have to help inference by adding a type-assert |
| 207 | + x = bar(a) # `bar` is now resistant to invalidation |
| 208 | +elsef a isa Integer |
| 209 | + # even though you've not made this fully-inferrable, you've at least reduced the scope for invalidations |
| 210 | + # by limiting the subset of `foobar` methods that might be called |
| 211 | + y = foobar(a) |
| 212 | +end |
| 213 | +``` |
| 214 | + |
| 215 | +Adding type-assertions and fixing inference problems are the most common approaches for fixing invalidations. |
| 216 | +You can discover these manually, but the [Cthulhu](https://github.com/JuliaDebug/Cthulhu.jl) package is highly recommended. |
| 217 | +Cthulu's `ascend`, in particular, allows you to navigate an invalidation tree and focus on those branches with the most severe consequences (frequently, the most children). |
0 commit comments