Skip to content

Convert arguments to the C API without inferring generic conversions - #703

Merged
maleadt merged 4 commits into
mainfrom
tb/api-conversions
Oct 22, 2026
Merged

maleadt merged 4 commits into
mainfrom
tb/api-conversions

Conversation

@maleadt

@maleadt maleadt commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #702. LLVM.jl's C API wrappers and IRBuilder are compiled with @nospecialize, so the strings, integers and vectors they pass to the C API are abstractly typed, and converting them was inferred for any AbstractString, Integer or AbstractVector. That makes LLVM.jl's (and its users') precompiled code depend on generic Base methods like cconvert(Cstring, ::AbstractString), UInt32(::Integer) or IteratorSize(::AbstractVector), which packages invalidate when they add string, integer or array types: InlineStrings, SentinelArrays and PooledArrays (all loaded by DataFrames), StructArrays, FillArrays, ChainRulesCore, ForwardDiff, etc.

This PR converts those values to concrete types once, with a fast path for the common type and an inference barrier for the others:

  • vectors of wrappers: Vector{Value} (and the like) are indexed directly, other vectors are converted behind a barrier;
  • strings: the generated C API wrappers convert AbstractString arguments for Cstring parameters to a String (what cconvert(Cstring, ::AbstractString) does too); pointers are passed unchanged;
  • alignments and aggregate indices are checked and converted to a Cuint by one call that specializes on their type, directly for Ints and behind a barrier otherwise; address spaces and clause counts are converted the same way;
  • metadata node elements, often an untyped literal, are converted behind a barrier.

First kernel on Julia 1.13.2, with the downstream fixes applied and only LLVM.jl varied (mean of 3 runs):

main #702 this PR
KernelAbstractions CPU, DataFrames first 3.45 s 3.24 s 1.62 s
KernelAbstractions CPU, ChainRulesCore first 1.94 s 1.00 s 0.75 s
CUDA, DataFrames first 4.71 s 4.66 s 2.92 s
CUDA, ChainRulesCore first 3.53 s 2.70 s 2.32 s
KernelAbstractions CPU / CUDA, nothing first 0.80 / 2.45 s 0.73 / 2.34 s 0.77 / 2.30 s

Per commit, the string and integer conversions account for nearly all of the gain; the vector conversions remove about 80 of the rejected method instances without a measurable effect on their own. Warm IRBuilder throughput improves slightly: building a call is about 20% faster than on main (132 vs. 170 ns, 213 vs. 244 bytes), and extract_value! with an Int index twice as fast. Converting a Vector{ConstantInt} now takes the barrier path (+32 bytes per call).

@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.92754% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/metadata.jl 63.63% 4 Missing ⚠️
src/core/instructions.jl 75.00% 2 Missing ⚠️
src/base.jl 92.85% 1 Missing ⚠️
@@                 Coverage Diff                  @@
##           tb/invalidations     #703      +/-   ##
====================================================
- Coverage             94.35%   94.31%   -0.05%     
====================================================
  Files                    59       59              
  Lines                  7853     7910      +57     
====================================================
+ Hits                   7410     7460      +50     
- Misses                  443      450       +7     
Files with missing lines Coverage Δ
src/core/function.jl 99.58% <100.00%> (-0.01%) ⬇️
src/core/module.jl 98.78% <100.00%> (ø)
src/core/type.jl 94.70% <100.00%> (+0.21%) ⬆️
src/core/value.jl 96.07% <100.00%> (+0.24%) ⬆️
src/core/value/constant.jl 97.05% <100.00%> (+0.03%) ⬆️
src/irbuilder.jl 97.24% <100.00%> (+0.09%) ⬆️
src/library.jl 60.64% <100.00%> (+0.51%) ⬆️
src/base.jl 97.17% <92.85%> (-0.38%) ⬇️
src/core/instructions.jl 97.55% <75.00%> (-0.35%) ⬇️
src/core/metadata.jl 89.00% <63.63%> (-1.11%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Callers that don't specialize, like the IRBuilder, pass abstractly-typed
vectors to `value_refs` and friends. Those were then inferred for any
`AbstractVector`, making them depend on generic iteration methods like
`IteratorSize`, which packages invalidate by extending them for their own
arrays (e.g. SparseArrays, StructArrays and FillArrays).

Index vectors with the hierarchy's abstract element type (e.g. `Vector{Value}`,
what callers usually pass) directly, and convert other vectors behind an
inference barrier. That also avoids a dynamic call for the common case, making
`call!` through the IRBuilder about 20% faster.
The IRBuilder and other methods that don't specialize take names and other
strings as `AbstractString`s, so passing them to a `ccall` made the conversion
to a `Cstring` depend on generic string conversion methods, which packages
adding string types (like InlineStrings, through DataFrames) invalidate.

Convert `AbstractString` arguments for `Cstring` parameters of the C API
wrappers to a `String` before the `ccall`, behind an inference barrier unless
they're a `String` already. That's what `cconvert(Cstring, ::AbstractString)`
does too; pointers and other values are passed unchanged. Methods that compute
a string's length for the C API, or compare it, convert it first, and so do
builders that also build a vector of references, as a dynamic call after
building it keeps Julia from eliding its allocation.
Alignments and aggregate indices are often abstractly-typed `Integer`s, as the
IRBuilder doesn't specialize, so checking them (and converting them to a `Cuint`
again when calling the C API) depended on generic integer methods, like
promotion, that packages such as ChainRulesCore invalidate.

Check them with a function that specializes on their type and returns the
converted value, and call it behind an inference barrier unless the value is an
`Int` (or, for alignments, a `Cuint`). Callers pass the converted value to the
C API. This makes `extract_value!` with an `Int` index twice as fast. Address
spaces (of globals, functions and pointer types) and clause counts are
converted the same way.
Metadata nodes are often created from an untyped literal, e.g. `MDNode([a, b])`
with elements of different metadata types, so `MDTuple` was compiled for an
abstract vector, and converting its elements to `Vector{Metadata}` depended on
generic methods for any vector, like iterating ranges, which ChainRulesCore
invalidates by extending `*`. That discarded, e.g., the precompiled Metal back-end
of GPUCompiler when ChainRulesCore was loaded first.
@maleadt
maleadt force-pushed the tb/api-conversions branch from 41a8c7a to 9833c23 Compare October 21, 2026 20:57
Base automatically changed from tb/invalidations to main October 22, 2026 05:02
@maleadt
maleadt merged commit 534a7a7 into main Oct 22, 2026
9 checks passed
@maleadt
maleadt deleted the tb/api-conversions branch October 22, 2026 05:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant