Does anyone know what could be done about these invalidations from defining a Base.lock method for a new IO type? https://github.com/JuliaLang/IJulia.jl/issues/1192#issuecomment-3390755056
I have a very vague understanding of invalidations but the code in IJulia/the MWE looks ok to me so I don't fully grok why it causes so many of them.
It's hard to tell precisely without digging into it, but it looks very much like an ordinary invalidation due to world splitting.
There is nothing you can do about it. It's an explicit tradeoff in the Julia compiler. The compiler does a bunch of speculative compilation, that just sometimes gets invalidation without anyone having done anything wrong.
The upside is that a lot of poorly inferred code runs fast due to this speculative compilation.
You can try to lobby the core devs to make the compiler stop doing this kind of speculative compilation, but that's kind of it.
Alas :tear: Thanks for the help.
This should be easy to fix by setting max_methods to one for Base.lock. However that needs to be done in Base. I have some relevant PRs I want to get back to some time. This one, specifically:
The other, more laborious, approach is to fix the code that is actually vulnerable to invalidation (because of type instability).
IO code in particular is often intentionally despecialized - e.g. with types having a field marked ::IO, so I doubt it can be fixed
As I said, it's just a matter of disabling world-splitting for lock.
However I feel like this should be done more comprehensively, instead of on a function-by-function basis, thus the linked PR. The PR does not currently include any changes for lock, though.
Coming back to this, if I understand correctly world splitting is this optimization where if there's some small number of methods then the compiler will speculatively compile versions of functions for all those different methods. But isn't that global max_methods set to 3 ATM? And there are many more lock() methods just in Base:
julia> length(methods(lock))
16
So how could world splitting be causing this?
And how would setting max_methods to 1 solve it?
max_methods says "if more than max_methods exist, give up on world splitting and do dynamic dispatch" so if max_methods is set to 1 this is equivalent to entirely disabling world splitting. but whether max_methods were 2, 3, 16, 100 as long as it is > 1 is when you can see invalidations
the smaller it is above 1 the more likely you are to see invalidations. if it were set to like, a billion, you would never see invalidations but you would also see shitty (generated) code
Ok, but in the case where it falls back on dynamic dispatch, doesn't that mean that there will be no invalidations?
Because it's dynamically looking up the right method at runtime, it's not compiling different versions specialized for different methods.
yeah, the invalidations happen when you cross the boundary
Right, makes sense. But then I don't understand how creating a new Base.lock method in a package could cause invalidations, because the number of methods in Base is already well above max_methods :thinking:
Could it have something to do with the type of the arguments? Looking at the methods in Base there are indeed 3 methods that take in an IO:
julia> methods(lock)
# 16 methods for generic function "lock" from Base:
[1] lock(c::Condition)
@ condition.jl:201
[2] lock(rl::ReentrantLock)
@ lock.jl:194
[3] lock(l::Base.Threads.SpinLock)
@ locks-mt.jl:41
[4] lock(l::Base.AlwaysLockedST)
@ condition.jl:49
[5] lock(s::Base.LibuvStream)
@ stream.jl:283
[6] lock(c::Base.GenericCondition)
@ condition.jl:75
[7] lock(c::Channel)
@ channels.jl:642
[8] lock(io::IOContext)
@ show.jl:424
[9] lock(::IO)
@ io.jl:26
[10] lock(l::Lockable)
@ lock.jl:453
[11] lock(wkh::WeakKeyDict)
@ weakkeydict.jl:79
[12] lock(f, wkh::WeakKeyDict)
@ weakkeydict.jl:81
[13] lock(f, c::Channel)
@ channels.jl:643
[14] lock(f, l::Lockable)
@ lock.jl:446
[15] lock(f, l::Base.AbstractLock)
@ lock.jl:332
[16] lock(f, c::Base.GenericCondition)
@ condition.jl:80
If that's part of the heuristic it makes sense adding a fourth in IJulia would cause invalidations.
oh hmmm
yeah good point. maybe it's per-arity?
Yep I think that's it, I tried replacing lock(::IJuliaStdio) with lock(::OtherType) and that fixed the invalidations :octopus:
(and also discovered an absolute slew of new invalidations from JSON.jl v1 :fear: )
there is https://github.com/JuliaLang/julia/pull/59091 a speculative proposal to just delete this feature
since the invalidations are a steep price to pay for the benefit it gives
would probably create approximately a bajillion performance regressions the first release out, but arguably leads to healthier code ecosystem in the long term
ah wait
I think it's not total methods
The price is not only paid in invalidations, steep as that already is. It's also paid as unreliable runtime performance, since package code is likely to depend on world splitting for runtime performance, but the world splitting gets disabled once the method gets invalidated.
The best time to disable world splitting was before Julia 1.0. The second best time is now.
Unfortunately we're in a fairly deep world splitting hole, so it'd going to be very hard to dig out of it by now.
One potential way for digging us out of the hole is for inlining the dynamic dispatch itself when compiling with juliac, since then you don't pay the FFI & method chasing costs anymore, but only the "which function do I jump to?" cost. Of course, that won't solve anything for interactive uses..
I'd like to add a cli flag to make it easier to try out your code with this disabled
people can't help dig out of the hole if they don't have shovels
I think that would definitely be useful :thumbs_up: I also like Cody's idea of using interfaces as a heuristic for world splitting: https://pretalx.com/juliacon-2025/talk/DCDEQV/
But I recall from previous discussions that it might not be possible to apply an interface to existing interfaces in v1.
Last updated: Nov 27 2025 at 04:44 UTC