LLVM.jl

by: Tim Besard (GitHub: maleadt)

Julia wrapper of the LLVM C API:

  • Low-level API through Clang.jl
  • High-level wrappers

Supports LLVM 3.9+, Julia 0.5+

Motivation

CUDAnative.jl needs LLVM to:

  • generate IR: replace Julia codegen
  • optimize: GPU-specific
  • compile: to PTX

Move PTX back-end out of codegen → birth of LLVM.jl

Why not target the C++ API directly, using Cxx.jl?

High-level, "Julian" API

Usage

Build your own compiler

Generate IR for Julia

Optimize or compile code generated by Julia

...

Installation

Pkg.add("LLVM")

Linux, macOS (unsupported)

Julia source build: llvm-config, headers, same host compiler ...

LLVM_ASSERTIONS=1

First steps

In [1]:
using LLVM
In [2]:
mod = LLVM.Module("demo")
Out[2]:
; ModuleID = 'demo'
source_filename = "demo"
In [3]:
context(mod) == GlobalContext()
Out[3]:
true
In [4]:
Context() do ctx
    mod = LLVM.Module("temp_module", ctx)
    # ...
    dispose(mod)
end

No automatic collection, use do blocks or call dispose!

When emitting code for Julia, use the appropriate context:

In [5]:
jlctx = LLVM.Context(cglobal(:jl_LLVMContext, Void));

Let's create a function: sum(::Int32, ::Int32)::Int32

In [6]:
param_types = [LLVM.Int32Type(), LLVM.Int32Type()]
ret_type = LLVM.Int32Type();

LLVM types are created by functions, bound to a context.

In [7]:
fun_type = LLVM.FunctionType(ret_type, param_types)
sum = LLVM.Function(mod, "sum", fun_type)
Out[7]:
declare i32 @sum(i32, i32)
In [8]:
mod
Out[8]:
; ModuleID = 'demo'
source_filename = "demo"

declare i32 @sum(i32, i32)

Adding IR

In [9]:
builder = Builder();
In [10]:
Builder() do builder
    # ...
end
In [11]:
bb = BasicBlock(sum, "entry")
position!(builder, bb)
In [12]:
tmp = add!(builder, parameters(sum)[1], parameters(sum)[2], "tmp")
Out[12]:
  %tmp = add i32 %0, %1
In [13]:
ret!(builder, tmp)
Out[13]:
  ret i32 %tmp
In [14]:
dispose(builder)
In [15]:
mod
Out[15]:
; ModuleID = 'demo'
source_filename = "demo"

define i32 @sum(i32, i32) {
entry:
  %tmp = add i32 %0, %1
  ret i32 %tmp
}
In [16]:
verify(mod)
In [17]:
ir = convert(String, mod);

Execution

In [18]:
engine = Interpreter(mod);
In [19]:
args = [GenericValue(LLVM.Int32Type(), 1),
        GenericValue(LLVM.Int32Type(), 2)];
In [20]:
res = LLVM.run(engine, sum, args);
In [21]:
convert(Int, res)
Out[21]:
3
In [22]:
dispose.(args)
dispose(res)
dispose(engine)

mod has been consumed by the ExecutionEngine:

In [23]:
mod
Out[23]:
; ModuleID = 'demo'
source_filename = "demo"

Integration

In [24]:
println(ir)
; ModuleID = 'demo'
source_filename = "demo"

define i32 @sum(i32, i32) {
entry:
  %tmp = add i32 %0, %1
  ret i32 %tmp
}

In [25]:
mod = parse(LLVM.Module, ir, jlctx)
Out[25]:
source_filename = "demo"

define i32 @sum(i32, i32) {
entry:
  %tmp = add i32 %0, %1
  ret i32 %tmp
}
In [26]:
context(mod) == jlctx
Out[26]:
true
In [27]:
sum = get(functions(mod), "sum")
Out[27]:
define i32 @sum(i32, i32) {
entry:
  %tmp = add i32 %0, %1
  ret i32 %tmp
}
In [28]:
call_sum(x, y) = Base.llvmcall(LLVM.ref(sum), Int32,
                               Tuple{Int32, Int32},
                               convert(Int32,x), convert(Int32,y))
Out[28]:
call_sum (generic function with 1 method)
In [29]:
call_sum(Int32(1),Int32(2))
Out[29]:
3
In [30]:
@code_llvm call_sum(Int32(1),Int32(2))
define i32 @julia_call_sum_61382(i32, i32) #0 !dbg !5 {
top:
  %2 = call i32 @jl_llvmcall1(i32 %0, i32 %1)
  ret i32 %2
}
In [31]:
push!(function_attributes(sum), EnumAttribute("alwaysinline"))
In [32]:
@code_llvm call_sum(Int32(1),Int32(2))
define i32 @julia_call_sum_61382(i32, i32) #0 !dbg !5 {
top:
  %tmp.i = add i32 %1, %0
  ret i32 %tmp.i
}

LLVM passes

In [33]:
ctx = context(mod)
Builder() do builder
    bb = first(blocks(sum))
    inst = first(instructions(bb))
    position!(builder, inst)
    alloca!(builder, LLVM.Int32Type(ctx), "unused")
end
sum
Out[33]:
; Function Attrs: alwaysinline
define i32 @sum(i32, i32) #0 {
entry:
  %unused = alloca i32
  %tmp = add i32 %0, %1
  ret i32 %tmp
}

Always start with a pass manager:

In [34]:
mpm = ModulePassManager();
In [35]:
ModulePassManager() do mpm
    # ...
end
In [36]:
aggressive_dce!(mpm)
In [37]:
run!(mpm, mod)
Out[37]:
true
In [38]:
mod
Out[38]:
source_filename = "demo"

; Function Attrs: alwaysinline
define i32 @sum(i32, i32) #0 {
entry:
  %tmp = add i32 %0, %1
  ret i32 %tmp
}

attributes #0 = { alwaysinline }

You can populate a pass manager using a PassManagerBuilder:

In [39]:
PassManagerBuilder() do pmb
    optlevel!(pmb, 0)
    sizelevel!(pmb, 0)
    
    populate!(mpm, pmb)
    run!(mpm, mod)
end
Out[39]:
false

LLVM passes... in Julia?!

In [40]:
function runOnFunction(fn::LLVM.Function)
    println("Processing $(name(fn))")
    return false
end
pass = FunctionPass("DummyFunctionPass", runOnFunction);
In [41]:
FunctionPassManager(mod) do fpm
    add!(fpm, pass)
    run!(fpm, sum)
end
Processing sum
Out[41]:
false

That's it!