Stream: helpdesk (published)

Topic: Base.lock invalidations


view this post on Zulip James Wrigley (Oct 10 2025 at 20:07):

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.

view this post on Zulip Jakob Nybo Nissen (Oct 11 2025 at 10:11):

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.

view this post on Zulip James Wrigley (Oct 11 2025 at 11:49):

Alas :tear: Thanks for the help.

view this post on Zulip Neven Sajko (Oct 11 2025 at 14:16):

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:

view this post on Zulip Neven Sajko (Oct 11 2025 at 14:18):

The other, more laborious, approach is to fix the code that is actually vulnerable to invalidation (because of type instability).

view this post on Zulip Jakob Nybo Nissen (Oct 11 2025 at 14:28):

IO code in particular is often intentionally despecialized - e.g. with types having a field marked ::IO, so I doubt it can be fixed

view this post on Zulip Neven Sajko (Oct 11 2025 at 14:55):

As I said, it's just a matter of disabling world-splitting for lock.

view this post on Zulip Neven Sajko (Oct 11 2025 at 14:57):

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.

view this post on Zulip James Wrigley (Oct 22 2025 at 10:51):

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?

view this post on Zulip James Wrigley (Oct 22 2025 at 10:51):

And how would setting max_methods to 1 solve it?

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:02):

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

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:03):

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

view this post on Zulip James Wrigley (Oct 22 2025 at 12:14):

Ok, but in the case where it falls back on dynamic dispatch, doesn't that mean that there will be no invalidations?

view this post on Zulip James Wrigley (Oct 22 2025 at 12:15):

Because it's dynamically looking up the right method at runtime, it's not compiling different versions specialized for different methods.

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:18):

yeah, the invalidations happen when you cross the boundary

view this post on Zulip James Wrigley (Oct 22 2025 at 12:19):

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:

view this post on Zulip James Wrigley (Oct 22 2025 at 12:24):

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.

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:28):

oh hmmm

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:28):

yeah good point. maybe it's per-arity?

view this post on Zulip James Wrigley (Oct 22 2025 at 12:33):

Yep I think that's it, I tried replacing lock(::IJuliaStdio) with lock(::OtherType) and that fixed the invalidations :octopus:

view this post on Zulip James Wrigley (Oct 22 2025 at 12:34):

(and also discovered an absolute slew of new invalidations from JSON.jl v1 :fear: )

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:35):

there is https://github.com/JuliaLang/julia/pull/59091 a speculative proposal to just delete this feature

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:36):

since the invalidations are a steep price to pay for the benefit it gives

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:36):

would probably create approximately a bajillion performance regressions the first release out, but arguably leads to healthier code ecosystem in the long term

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:49):

ah wait

view this post on Zulip Andy Dienes (Oct 22 2025 at 12:49):

I think it's not total methods

view this post on Zulip Jakob Nybo Nissen (Oct 22 2025 at 19:14):

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.

view this post on Zulip Sukera (Oct 22 2025 at 20:26):

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..

view this post on Zulip Andy Dienes (Oct 22 2025 at 20:29):

I'd like to add a cli flag to make it easier to try out your code with this disabled

view this post on Zulip Andy Dienes (Oct 22 2025 at 20:30):

people can't help dig out of the hole if they don't have shovels

view this post on Zulip James Wrigley (Oct 22 2025 at 20:39):

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