Skip to content

Avoid overspecialization #400

Open
Open
@maleadt

Description

@maleadt

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions