Skip to content

Conversation

@MasonProtter
Copy link
Contributor

@MasonProtter MasonProtter commented Dec 21, 2025

(I wrote this with assistance from Gemini since I'm not very used to writing LLVM IR) No longer using that code

This is an attempt to fix #60441. After bumbling around a bit, it seems that the problem is that invoke(f, ::CodeInstance, args...) calls are not turned into Expr(:invoke statements in the IR, but remains as :calls to the invoke builtin which ends up going through the runtime.

There's probably a better way to do this, but the way I found was to just detect these builtin calls in the LLVM IR, and send them to emit_invoke. I'm now detecting InvokeCICallInfos in the inlining step of the optimizer and turning those into Expr(:invokes.

It appears to resolve the issue:

using BenchmarkTools

mysin(x::Float64) = sin(x)
@assert mysin(1.0) == sin(1.0)
const mysin_ci = Base.specialize_method(Base._which(Tuple{typeof(mysin), Float64})).cache

Before this PR:

julia> @btime invoke(mysin, mysin_ci, x) setup=(x=rand())
  24.952 ns (2 allocations: 32 bytes)
0.7024964043721993

After this PR:

julia> @btime invoke(mysin, mysin_ci, x) setup=(x=rand())
  4.748 ns (0 allocations: 0 bytes)
0.32283046823183426

@gbaraldi
Copy link
Member

Does this/should this do a world age check?
What is the difference between this and calling an opaque closure?
Could this potentially be used to make a fake codeinst, put an invoke pointer there and call that random invoke pointer?

@gbaraldi gbaraldi requested review from Keno and vtjnash January 16, 2026 14:26
@MasonProtter
Copy link
Contributor Author

What is the difference between this and calling an opaque closure?
Could this potentially be used to make a fake codeinst, put an invoke pointer there and call that random invoke pointer?

So just to be clear, I am not proposing introducing a new mechanism here. The new mechanism was already added for v1.12 in #56660. The only thing this PR does is make it so that the mechanism isn't slow to call.

I don't really know what one can or cannot do by messing around with the invoke pointer, presumably arbitrary things, but I'm not sure. Regarding a comparison to opaque closures, I think the idea is that these codeinstances are a bit more 'static', rather than combining runtime data with alternative interpretation. Another difference is that a CodeInstance is just the natural object that running the compiler pipeline on an alternative AbstractInterpreter produces, not an opaque closure. So I think encouraging people to use these instead of opaque closures allows one to operate a bit more closely to how to the compiler works.

I didn't make the feature though, that was @Keno, so maybe he can motivate it a bit more if the linked PR and my crappy explanation isn't sufficient.

Does this/should this do a world age check?

I had initially assumed that emit_invoke would do the worldage check,but I'm not actually seeing it in there, so I guess I probably should add it into here, since the version in the runtime does do a worldage check.

@MasonProtter
Copy link
Contributor Author

MasonProtter commented Jan 16, 2026

For the world-age check, how do we do that? It seems that the ctx has a min worldage and a max worldage, not a specific worldage. Do I just check that this overlaps with the min and max worldages of the CodeInstance? Or does the worldage check need to be a runtime check rather than compile-time?

@MasonProtter
Copy link
Contributor Author

MasonProtter commented Jan 17, 2026

Okay, I have

  • Added some basic checks making sure that .invoke is non-null. If it is null, we try to compile it like the interpreter does, and if it's still null after that, we just let the interpreter handle stuff like errors
  • Checked that the ctx worldage fits within the valid worldages for the codeinst, if not I simply pass it off to the interpreter to handle errors
  • Added some basic tests to CompilerDevTools that one can invoke a constant codeinstnace without allocations, check that the invoke pointer will get generated if null, and check that worldage bounds are respected.
  • Made Compiler's tests run CompilerDevTools's tests

@MasonProtter MasonProtter changed the title Add codegen support for invoke(f, ::CodeInstance, args...) Add optimizer support for invoke(f, ::CodeInstance, args...) Jan 20, 2026
@MasonProtter
Copy link
Contributor Author

MasonProtter commented Jan 23, 2026

I have moved all the testing to test/core.jl to live alongside the original tests for invoke(f, ::CodeInstance, ...). This was mostly just because the majority of lines of code in the PR were about changing the test structure, which was probably rather distracting from the content of the PR.

This is now quite minimal and hopefully easy to review.

Copy link
Member

@Keno Keno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems sensible to me.

@MasonProtter MasonProtter added the merge me PR is reviewed. Merge when all tests are passing label Jan 23, 2026
@MasonProtter
Copy link
Contributor Author

MasonProtter commented Jan 23, 2026

Thank you!

Is this backportable to 1.12/1.13?

@Keno
Copy link
Member

Keno commented Jan 23, 2026

I think it's ok to backport 1.13. It does open up a bit of a can of worms, but it's in a new and experimental feature.

@adienes adienes merged commit e1dda38 into JuliaLang:master Jan 24, 2026
10 checks passed
@adienes adienes added backport 1.13 Change should be backported to release-1.13 and removed merge me PR is reviewed. Merge when all tests are passing labels Jan 24, 2026
MasonProtter added a commit to MasonProtter/julia that referenced this pull request Jan 25, 2026
…Lang#60442)

~~(I wrote this with assistance from Gemini since I'm not very used to
writing LLVM IR)~~ No longer using that code

This is an attempt to fix
JuliaLang#60441. After bumbling around a
bit, it seems that the problem is that `invoke(f, ::CodeInstance,
args...)` calls are not turned into `Expr(:invoke` statements in the IR,
but remains as `:call`s to the `invoke` builtin which ends up going
through the runtime.

~~There's probably a better way to do this, but the way I found was to
just detect these builtin calls in the LLVM IR, and send them to
`emit_invoke`.~~ I'm now detecting `InvokeCICallInfo`s in the inlining
step of the optimizer and turning those into `Expr(:invoke`s.

It appears to resolve the issue:
```julia
using BenchmarkTools

mysin(x::Float64) = sin(x)
@Assert mysin(1.0) == sin(1.0)
const mysin_ci = Base.specialize_method(Base._which(Tuple{typeof(mysin), Float64})).cache
```
Before this PR:
```julia
julia> @Btime invoke(mysin, mysin_ci, x) setup=(x=rand())
  24.952 ns (2 allocations: 32 bytes)
0.7024964043721993
```
After this PR:
```julia
julia> @Btime invoke(mysin, mysin_ci, x) setup=(x=rand())
  4.748 ns (0 allocations: 0 bytes)
0.32283046823183426
```
KristofferC pushed a commit that referenced this pull request Jan 26, 2026
~~(I wrote this with assistance from Gemini since I'm not very used to
writing LLVM IR)~~ No longer using that code

This is an attempt to fix
#60441. After bumbling around a
bit, it seems that the problem is that `invoke(f, ::CodeInstance,
args...)` calls are not turned into `Expr(:invoke` statements in the IR,
but remains as `:call`s to the `invoke` builtin which ends up going
through the runtime.

~~There's probably a better way to do this, but the way I found was to
just detect these builtin calls in the LLVM IR, and send them to
`emit_invoke`.~~ I'm now detecting `InvokeCICallInfo`s in the inlining
step of the optimizer and turning those into `Expr(:invoke`s.

It appears to resolve the issue:
```julia
using BenchmarkTools

mysin(x::Float64) = sin(x)
@Assert mysin(1.0) == sin(1.0)
const mysin_ci = Base.specialize_method(Base._which(Tuple{typeof(mysin), Float64})).cache
```
Before this PR:
```julia
julia> @Btime invoke(mysin, mysin_ci, x) setup=(x=rand())
  24.952 ns (2 allocations: 32 bytes)
0.7024964043721993
```
After this PR:
```julia
julia> @Btime invoke(mysin, mysin_ci, x) setup=(x=rand())
  4.748 ns (0 allocations: 0 bytes)
0.32283046823183426
```

(cherry picked from commit e1dda38)
@KristofferC KristofferC mentioned this pull request Jan 26, 2026
43 tasks
@KristofferC KristofferC removed the backport 1.13 Change should be backported to release-1.13 label Feb 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allocations when invoke-ing a constant CodeInstance

8 participants