Description
We currently duplicate the C++ type hierarchy of LLVM objects (e.g. Argument, Instruction, Function, etc), even though the C API only exposes a flattened one (Value). This has several advantages:
- the ability to overload names, e.g.
unsafe_delete!
,parent
,name
, automatically dispatching to the correct C method based on the type - type checking to avoid using the wrong method, which would abort in LLVM (and thus kill your session)
... but also a disadvantage: code gets overspecialized because we have many representations of a Value. For example:
using LLVM
function main()
Context() do ctx
mod = LLVM.Module("SomeModule")
param_types = [LLVM.Int32Type(), LLVM.Int32Type()]
ret_type = LLVM.FunctionType(LLVM.Int32Type(), param_types)
sum = LLVM.Function(mod, "SomeFunctionSum", ret_type)
entry = BasicBlock(sum, "entry")
@dispose builder=IRBuilder() begin
position!(builder, entry)
tmp = add!(builder, parameters(sum)[1], parameters(sum)[2])
tmp = add!(builder, tmp, ConstantInt(LLVM.Int32Type(), 1))
tmp = add!(builder, tmp, parameters(sum)[2])
ret!(builder, tmp)
end
end
end
isinteractive() || main()
❯ jl --trace-compile=stderr --project wip.jl
precompile(Tuple{typeof(LLVM.add!), LLVM.IRBuilder, LLVM.AddInst, LLVM.ConstantInt})
precompile(Tuple{typeof(LLVM.add!), LLVM.IRBuilder, LLVM.AddInst, LLVM.Argument})
Here you can see that the add!
method was specialized multiple times, even though the generated code is basically identical: accessing the handle and passing it to C.
Is this even a problem?
I'm not sure. Although we overspecialize, we only do so for tiny methods, so I would think the added latency is minimal.
Possible solution 1: hint the compiler
We could add @nospecialize(x::Value)
annotations to functions that do not need to be specialized. That's a bit cumbersome, and also doesn't suffice. For example:
add!(builder::IRBuilder, @nospecialize(LHS::Value), @nospecialize(RHS::Value), Name::String="") =
Value(API.LLVMBuildAdd(builder, LHS, RHS, Name))
Even though this successfully avoids overspecialization of add!
, the underlying wrapper function is still specialized:
precompile(Tuple{typeof(LLVM.API.LLVMBuildAdd), LLVM.IRBuilder, LLVM.AddInst, LLVM.ConstantInt, String})
precompile(Tuple{typeof(LLVM.API.LLVMBuildAdd), LLVM.IRBuilder, LLVM.AddInst, LLVM.Argument, String})
Adding @nospecialize
over there (which isn't a trivial thing to add to our wrapper generators) pushes the specialization down another layer:
precompile(Tuple{typeof(Base.unsafe_convert), Type{Ptr{LLVM.API.LLVMOpaqueValue}}, LLVM.AddInst})
precompile(Tuple{typeof(Base.unsafe_convert), Type{Ptr{LLVM.API.LLVMOpaqueValue}}, LLVM.ConstantInt})
... but that one is probably acceptable, and would result in code that can get reused.
Still, lots of changes across the codebase. And FWIW, Base.Experimental.@max_methods 1 function add! end
doesn't help, neither does adding that to the LLVM.API submodule. Base.@nospecializeinfer
also doesn't help.
Possible solution 2: Rework the type hierarchy
We could rework the type hierarchy to be either flat (only Value) or maybe trait based. The former would mean losing a lot of functionality, the latter reimplementing what Julia does for us (e.g. checking trait as function preconditions), so I'm not inclined to implement this.
Furthermore, we currently do actually sometimes use the ability to store extra fields in Value subtypes, e.g., to store lists of roots, or parent objects. Flattening everything would remove that ability.