- Larva
- Cómo funciona Larva
- El back end de LLVM
Esta página aún no está traducida al español. Leer la versión en inglés
The LLVM back end
How Larva.Llvm turns linked Larva IR into optimized machine code - types, objects, GC roots, safepoints, exceptions, bounds checks, optimization levels, processors and parallel code generation.
En esta página
- Where LLVM comes from
- The pipeline
- Types
- Functions, locals and places
- Ownership and drop flags
- Objects and allocation
- Safepoints and GC roots
- Safepoints
- Roots: two strategies
- Exceptions
- Windows: zero-cost SEH
- Linux, macOS and WebAssembly: an exception flag
- Bounds checks and their elimination
- Type-based alias analysis
- A complete example
- Optimization levels
- The processor: --cpu
- Polly
- larva native: objects to code
- Parallel code generation
- Running code in memory
- Environment variables
- Limits
Larva.Llvm is Larva's optimizing back end. It takes a linked program in Larva IR, translates every function into LLVM IR, runs LLVM's optimization pipeline and writes an object file (or assembly, or textual IR). It is the default for larva build, and the only back end that targets Linux, macOS and (through --target wasm-llvm) WebAssembly. Its sibling, the direct x86-64 back end, compiles faster and optimizes much less.
The project has five parts:
| File | What it holds |
|---|---|
LlvmBackend |
LlvmOptions, LlvmBackend (the entry points: Compile, CompileObjects, CompileConcrete), LlvmProgram (target machine, verifier, optimization pipeline, emission) and JitSession (in-memory execution). |
LlvmCodegen |
LlvmCodegen: the translation of one monomorphized IR module into one LLVM module. About 2,200 lines, most of the back end. |
StackMaps |
Precise stack roots for tracing collectors: the larva.gc.root marker and its lowering to llvm.experimental.stackmap. |
ParallelCodegen |
Large programs split into partitions, each generated, optimized and emitted on its own thread. |
JitMemory |
The memory manager of the in-memory path on Windows (one executable block, unwind tables registered). |
Where LLVM comes from
Larva does not look for an LLVM installation. The code generator is LLVM 22.1.8, called in process through LLVM's C API. Two native libraries ship next to larva (the Windows file names; the Linux and macOS packages carry their own builds of the same version):
| File | What it is |
|---|---|
LLVM-C.dll |
LLVM itself: the optimizer and the X86, AArch64 and WebAssembly code generators. |
libLLVMSharp.dll |
A thin native helper library over the C API that larva's own code calls through. |
The build of larva picks the native LLVM package of the system it is built on (Windows x64, Linux x64 or macOS arm64), and the installer copies both files beside larva.exe (see Installers). When LlvmBackend is first used it initializes three targets: X86 (target info, target, machine code, assembly printer and parser), WebAssembly and AArch64. Nothing else is registered, so those are the only processors LLVM can emit for.
Two LLVM tools are not in the installation and are looked up only when a feature needs them:
| Tool | Needed by | How it is found |
|---|---|---|
wasm-ld |
--target wasm-llvm |
LARVA_WASM_LD, then PATH, then the standard LLVM install folders. See WebAssembly. |
opt (same LLVM version, with Polly) |
larva native --polly |
--polly-opt <path>, else opt on PATH. See Polly. |
LARVA_LLVM_ARGS passes options to LLVM itself, the way llc takes them (for example -x86-asm-syntax=intel). It is parsed once, when the back end starts.
The pipeline
linked IR program (LinkResult) Larva.IR
|
| ExecutableEntry.AddWrappers larva_entry, larva_entry_f64, larva_entry_info
v
Monomorphizer.Run(..., GenericsMode.Specialize) every generic made concrete
|
| Compilable.Stub functions this back end cannot compile become stubs
v
LlvmCodegen.Generate() "llvm: build IR (N functions)"
|
v
LlvmProgram: target machine (triple, --cpu)
verify "llvm: verify"
[wasm] link wasm_heap.ll, root TBAA
default<O1..O3> pipeline, or Polly "llvm: optimize (O3)"
stack maps: lower (Windows) or keep in memory (elsewhere)
verify again
|
v
EmitObject / EmitAssembly / EmitIr / Jit() "llvm: emit object"
The names in quotes are the phases LARVA_TIMINGS=1 prints (on standard error), next to the summary line --timings prints:
LARVA_TIMINGS=1 larva build --timings -o hello.exe hello.ant
[timing] type check: 86 ms
[timing] region check: 0 ms
[timing] monomorphize (specialize): 3 ms
[timing] llvm: build IR (47 functions): 16 ms
[timing] llvm: verify: 2 ms
[timing] llvm: optimize (O3): 220 ms
[timing] llvm: emit object: 191 ms
[timing] link (runtime and linker): 1.130 ms
hello.exe (llvm back end, O3, entry hello::Hello.$Module.Main)
timings: System.Core (compiled library) 9 ms, frontend 94 ms, HIR link 9 ms, lowering 133 ms, IR checks 87 ms, IR link 53 ms, native (llvm) 1683 ms; total 2068 ms
Generating LLVM IR is cheap. Optimizing and emitting dominate, and the link (the runtime plus the system linker, see Native executables) is the rest.
Generics are always specialized for native code: LlvmBackend sets GenericsMode.Specialize whatever the compilation options say, so the code generator never sees a type parameter. A struct or function that still has type arguments when it reaches LlvmCodegen is a bug, reported as "T was not monomorphized".
Types
LlvmCodegen.ValueType maps every IR type to an LLVM type:
| IR type | LLVM type | Notes |
|---|---|---|
unit |
an empty struct | |
bool |
i1 |
|
i8 … i256, u8 … u256 |
iN |
Any width; signedness lives in the instructions (sdiv/udiv, icmp slt/ult, sext/zext), not the type. |
f16, f32, f64 |
half, float, double |
Native LLVM floating point. |
f128, f256 |
i128, i256 |
The IEEE bits. Arithmetic calls the runtime's soft-float routines (larva_float_add, …, larva_float_conv). |
&T, gc T, box T, *T, function values, T? of a handle |
ptr |
One opaque pointer type. |
| structs | a named, packed LLVM struct with explicit padding | Offsets equal Layouts.FieldOffsets. |
[T; n] (inline arrays) |
[size x i8] |
Elements are reached by byte offset. |
Two decisions stand out.
Structs are packed with explicit padding. The layout of every struct is decided once, by Layouts in Larva.IR, and shared by the VM, the garbage collector's type maps, reflection and every back end. LLVM's own struct layout rules could disagree with it. So StructOf asks Layouts for the field offsets and size, then builds a packed LLVM struct whose elements are the fields plus [n x i8] padding arrays, so that each field lands exactly where Layouts put it. The same reasoning makes inline arrays plain bytes.
Wide floats are integers. LLVM has fp128, but its library calls for it crashed the in-memory code generator on 64-bit Windows, and LLVM has no 256-bit float at all. So f128 and f256 travel as their bits in i128/i256, and each operation is a call to the same soft-float routines the direct x86-64 back end and the WebAssembly runtime use, bit for bit (round to nearest, ties to even). f16 stays a native half; LLVM turns its conversions into the compiler-rt helpers (__truncdfhf2, __extendhfsf2, …), which the C runtime provides.
Functions, locals and places
Every IR local gets an alloca in the function's entry block (so a loop never grows the stack), and mem2reg in the optimization pipeline promotes them to SSA values. A place (x.f[i].g) becomes a chain of byte-offset getelementptrs; a dereference becomes a load of the pointer first. Calls between Larva functions are direct calls to their symbols.
The symbol of a function is its linked name with a prefix: larva. followed by the name (larva.hello::Hello.$Module.Main), or larva.$ and a 64-bit FNV-1a hash in hex for mangled names, operators and overloads, whose names contain characters an export list cannot hold. Names that start with larva_ belong to the runtime and its entry wrappers and keep no prefix; [Export("name")] functions keep their C name. The prefix is what keeps a Larva function called main from clashing with the C runtime's (NativeSymbols in Larva.IR.Linking; the in-memory path uses no prefix).
Runtime services are calls to larva_* functions declared at the top of every module: allocation (larva_gc_alloc, larva_gc_array, larva_array_new, larva_raw_alloc), reference counting, regions, write barriers, pins, polls, panics, printing, files, sockets, threads, atomics, math, soft floats. A few intrinsics become LLVM instructions instead of calls: square root, floor and ceiling, and reinterpreting a float's bits. Atomic operations become LLVM's own atomic instructions, sequentially consistent.
Ownership and drop flags
Every owned resource in a frame (a box, or a gc handle when the GC counts references) has a drop flag. The flag is set when the place is initialized, cleared when it is moved out, and every release is guarded by it. Most flags are constants along every path, and LLVM folds them away; the rest cost a byte and a branch.
Under reference counting, native code passes gc handles at "+0": the caller keeps its reference for the duration of the call and the callee borrows it. A callee that stores the handle somewhere takes its own reference then. A local read once and dead afterwards hands its reference over instead of taking a new one, so the common "make it, pass it on" pattern does no counting at all. See Garbage collection.
Objects and allocation
A gc object has a 16-byte header before its payload (type descriptor pointer, reference count word, mark, flags, domain byte, age). The type descriptor is a private constant the back end emits per type:
{ i32 size, i32 stride, i32 flags, i16 handles, i16 cells, [n x i32] offsets }
It tells the collector where in an object the handles are. Here is the descriptor of an i64[] in a real module (no handles: handles and cells are zero):
@larva.type.0 = private constant { i32, i32, i32, i16, i16, [0 x i32] } { i32 8, i32 8, i32 5, i16 0, i16 0, [0 x i32] zeroinitializer }, align 8
In an executable, small young objects are allocated inline. The runtime exports larva_heap (through __imp_larva_heap), whose first fields are a "fast" flag, the nursery cursor and its limit. Code generation loads them with volatile loads, bumps the cursor if the object fits, writes the header, and calls the runtime only on the cold path. This is Main of the array-sum program below, after -O3:
ok: ; preds = %entry, %poll
%heap = load ptr, ptr @__imp_larva_heap, align 8
%heap.fast = load volatile i32, ptr %heap, align 4
%0 = getelementptr i8, ptr %heap, i64 8
%nursery.cursor = load volatile ptr, ptr %0, align 8
%1 = getelementptr i8, ptr %heap, i64 16
%nursery.limit = load volatile ptr, ptr %1, align 8
%nursery.next = getelementptr i8, ptr %nursery.cursor, i64 832
%2 = icmp ule ptr %nursery.next, %nursery.limit
%3 = icmp ne i32 %heap.fast, 0
%4 = icmp ne ptr %nursery.cursor, null
%5 = and i1 %3, %4
%alloc.fast = and i1 %5, %2
br i1 %alloc.fast, label %alloc.inline, label %alloc.call, !prof !0
alloc.inline: ; preds = %ok
store volatile ptr %nursery.next, ptr %0, align 8
store volatile ptr @larva.type.0, ptr %nursery.cursor, align 8
%6 = getelementptr i8, ptr %nursery.cursor, i64 8
store volatile i64 1, ptr %6, align 8
%7 = getelementptr i8, ptr %nursery.cursor, i64 16
br label %alloc.done
alloc.call: ; preds = %ok
%8 = tail call ptr @larva_gc_array(ptr nonnull @larva.type.0, i64 100)
br label %alloc.done
832 bytes is the 16-byte header, the 8-byte length and 100 elements of 8 bytes. !prof !0 is the branch weight {2000, 1} that marks the runtime call as rare. The nursery memory is already zero, so nothing is cleared.
Stores of a handle into an object run the generational write barrier: one load of the owner's age byte, and a call to larva_gc_wb only when the owner is old. Under reference counting, retain is a null check and an add on the header's count word (plus a check of the domain byte when the program mixes GC domains); release calls the runtime when the count reaches zero.
Safepoints and GC roots
A tracing collector needs two things from compiled code: places where a thread may stop for a collection (safepoints), and a way to find every handle the stack holds (roots).
Safepoints
Larva.IR.Analysis.Safepoints decides where code polls: at the header of every loop that allocates, and at the entry of functions that allocate, recurse, or call through a function value or a vtable. A loop that only calls needs no poll of its own, since a callee that allocates polls at its entry. In a program that starts threads every loop polls, because a collection must be able to stop every thread.
A poll is a load and a branch: the runtime's larva_poll_pending flag (through __imp_larva_poll_pending, as an import from the runtime) is read with a volatile load, and larva_poll is called only when it is set:
define void @"larva.sum::Sums.$Module.Main"() local_unnamed_addr {
entry:
%poll.flag.address = load ptr, ptr @__imp_larva_poll_pending, align 8
%poll.flag = load volatile i32, ptr %poll.flag.address, align 4
%poll.due.not = icmp eq i32 %poll.flag, 0
br i1 %poll.due.not, label %ok, label %poll, !prof !0
poll: ; preds = %entry
tail call void @larva_poll()
br label %ok
larva_poll is declared memory(inaccessiblemem: readwrite): as far as LLVM knows, it touches only memory the program cannot see. That lets a loop keep the fields it uses in registers across its safepoint. It is sound because the executable's collector scans the stack and registers conservatively, so whatever the code holds is pinned (it neither moves nor changes), and what the collector does change that code reads (the nursery cursor, an object's age) is read with volatile loads. LARVA_POLL_OPAQUE=1 turns the poll back into an ordinary call, to tell whether an optimization across it is at fault.
Roots: two strategies
LlvmOptions.PreciseStackRoots chooses between them.
Precise roots (PreciseStackRoots = true) |
Conservative roots (false) |
|
|---|---|---|
| Used by | the in-memory path (JitSession), and larva native objects |
every executable and library larva build makes |
| Handles live in | their own stack slots, marked with larva.gc.root |
registers or the stack, wherever LLVM puts them |
| The collector finds them | through the .llvm_stackmaps section |
by scanning the stack and the saved registers word by word |
The precise strategy (StackMaps) works like this. Code generation keeps every local whose type can hold a handle in its own alloca and passes that slot's address to a call of larva.gc.root. The call makes the slot escape, so no optimization (inlining included) can promote it to a register or reuse its value across a call: at any call, the frame's handles are in their slots. After optimization StackMaps.Lower deletes the marker calls and gives each function a single llvm.experimental.stackmap call listing its slots and their sizes. Slots have fixed frame offsets, so one record describes every safepoint of the function. LLVM writes the offsets into the .llvm_stackmaps section (format version 3), which Larva.Native.StackMapFormat.Parse reads back. The direct x86-64 back end writes the same section itself.
Executables gave this up. The C runtime's collector is non-moving for anything a conservative root points at, so it scans the stack and registers itself and accepts interior pointers (a pointer into an array keeps the array alive). With no markers, LLVM keeps handles in registers and does not reload them after every call, which the design notes found clearly faster. The in-memory path keeps precise roots because its in-process runtime (NativeRuntime) needs them.
On WebAssembly there are no stack maps at all: StackMaps.KeepInMemory turns the marker into an empty, never-inlined function, so the slots still escape and stay in linear memory, where the JavaScript collector scans for roots.
Exceptions
How a throw reaches its catch depends on the target. LlvmBackend.UsesSeh(triple) decides: Windows triples use structured exception handling (SEH); everything else uses an exception flag.
Windows: zero-cost SEH
A throw allocates a heap cell holding the exception object, then calls a private larva_throw(payload, typeId), which raises an SEH exception with Larva's own code through larva_raise:
; Function Attrs: noreturn
define internal fastcc void @larva_throw(ptr %0, i64 range(i64 0, 461533487) %1) unnamed_addr #1 {
entry:
%information = alloca [2 x i64], align 8
%2 = ptrtoint ptr %0 to i64
store i64 %2, ptr %information, align 8
%3 = getelementptr inbounds nuw i8, ptr %information, i64 8
store i64 %1, ptr %3, align 8
call void @larva_raise(i32 -531868095, i32 1, i32 2, ptr nonnull %information)
unreachable
}
-531868095 is 0xE04C5641: "LVA" in the customer range of exception codes. The type id is the same in every native image of the program (0 means "any Larva exception").
Functions with handlers use __C_specific_handler as their personality. A call that may throw and has somewhere to go in this frame becomes an invoke. Its unwind edge goes to a chain of catchswitch blocks, one per catch clause (SEH needs one handler per switch), each with a catchpad whose filter is a generated function: larva.filter.<typeid> returns "execute handler" when the exception is Larva's and its type id matches, and records the payload and type for the handler. After the last clause, the chain unwinds to the function's single cleanuppad, which releases what the frame still owns (by drop flags), frees the handled exception, drops pins, closes the arena regions the call opened, and lets the exception continue with cleanupret … unwind to caller. Take this program:
import System;
into namespace Fail
{
public exception Negative
{
}
Check(i32 n) : i32
{
if (n < 0)
throw new NegativeException();
return n * 2;
}
Main()
{
try
{
Console.WriteLine(Check(4));
Console.WriteLine(Check(-1));
}
catch (NegativeException e)
{
Console.WriteLine("caught");
}
}
}
LARVA_DUMP=dump larva build -O1 -o fail.exe fail.ant
fail.exe (llvm back end, O1, entry fail::Fail.$Module.Main)
./fail.exe
8
caught
In its Main (with Check inlined), the handler is reached like this:
define void @"larva.fail::Fail.$Module.Main"() local_unnamed_addr personality ptr @__C_specific_handler {
…
dispatch: ; preds = %poll.i
%5 = catchswitch within none [label %catch] unwind label %cleanup
catch: ; preds = %dispatch
%6 = catchpad within %5 [ptr @larva.filter.461533486]
catchret from %6 to label %bb2
Nothing runs on the normal path: no registration, no flag check. The cost is all on the throwing path.
Two details matter for correctness. A frame with a cleanup keeps its owning locals and their drop flags in memory with llvm.localescape, so the cleanup funclet can read them; otherwise every potentially throwing call in a large loop would first copy every flag out. Small frames skip this, because a few copies cost less than keeping everything in memory. And executables are linked with /CETCOMPAT:NO: Windows' shadow stack rejects exception continuations into code that is not registered as an EH continuation target.
Linux, macOS and WebAssembly: an exception flag
There, a throw stores the payload and its type id in two globals, larva_exception and larva_exception_type, and jumps to the handlers of the current site. After every call to a Larva function, the generated code checks whether an exception is pending: if so it goes to the first catch whose type matches, or runs the frame's cleanup and returns with the exception still pending. This costs a load and a branch after each call, but needs no unwinder, which WebAssembly does not have and which keeps the POSIX runtime simple.
Bounds checks and their elimination
Every array access is checked. An index i into array a loads the length word (the first 8 bytes of the array), compares unsigned, and calls larva_panic(1, index, length) on failure, which ends the program with exit code 101. Indexing a three-element array with 5:
./oob.exe
larva panic: index 5 out of bounds for length 3
At -O0, Sum from the example below keeps its check:
bb2: ; preds = %bb1
%8 = load i64, ptr %total, align 4
%9 = load ptr, ptr %values, align 8
%10 = load i32, ptr %i, align 4
%11 = sext i32 %10 to i64
%len1 = load i64, ptr %9, align 4, !tbaa !1, !range !4
%12 = icmp ult i64 %11, %len1
br i1 %12, label %ok, label %panic
…
panic: ; preds = %bb2
call void @larva_panic(i32 1, i64 %11, i64 %len1)
unreachable
Checks disappear in two ways.
Larva's own analysis. Larva.IR/Analysis/BoundsAnalysis is a forward must-analysis over facts about locals, run per function before code generation:
| Fact | Comes from |
|---|---|
len(*a) == n or len(*a) == k |
a = new [T; n], and n = len(*a). Array lengths never change, so the fact holds while neither local is reassigned. |
i < n or i < k |
the "then" edge of if c, where c = i < n (or n > i). |
i >= 0 |
non-negative constants, lengths, and i = i + 1 while i was bounded above (so it cannot overflow). Unsigned locals are non-negative by type. |
An access is safe when 0 <= i < n == len(*a). Assigning a local kills every fact that mentions it, and a local that is ever borrowed mutably never takes part, because it could change behind the analysis' back. Accesses into an in-place array at the end of an object count as accesses into that object's array. When the analysis proves an access safe, LlvmCodegen emits no check at all; LlvmProgram.BoundsChecks and BoundsChecksEliminated count both cases.
LLVM's own optimizations. The length load carries !range !{i64 0, i64 9223372036854775807} (lengths are in [0, 2^63)), and its own TBAA tag (see below). With that, LLVM's induction-variable and loop passes remove many of the checks the IR analysis cannot see. In the example the loop bound is values.Length, which is an i32 truncation of the 64-bit length word, so BoundsAnalysis does not match the n = len(*a) pattern; at -O3 LLVM removes the check anyway and vectorizes the loop.
The length load is deliberately not marked invariant.load: that let LLVM sink the load past the larva_free of the array.
larva native --no-bce turns Larva's analysis off (the checks are all emitted, and LLVM may still remove some). larva build always runs it.
Type-based alias analysis
Two TBAA tags, under a root named larva tbaa, tell LLVM that an array's length word and its elements are never the same memory:
!1 = !{!2, !2, i64 0}
!2 = !{!"element", !3, i64 0}
!3 = !{!"larva tbaa"}
!4 = !{!5, !5, i64 0}
!5 = !{!"length", !3, i64 0}
Without them, a store into an element forces LLVM to reload the length on the next iteration (a sort's inner loop reloaded it after every swap). Only array elements of number or pointer types and length words are tagged. Raw pointers, copies and runtime calls stay untagged, so they still alias everything.
For WebAssembly, StackMaps.SeparateRootsFromHeap adds a second pair before optimization: accesses through a root slot get larva.root, accesses through pointers loaded from memory, globals, arguments or call results get larva.data. A marked slot has escaped, so without this every store into it could change any object, and a loop that keeps a handle in one would reload everything on each pass. It holds because a slot's address only ever goes to the (empty) marker; a slot used any other way is not tagged. LARVA_ROOT_TBAA=0 disables it.
A complete example
import System;
into namespace Sums
{
Sum(i64[] values) : i64
{
i64 total = 0;
loop (i32 i = 0..values.Length)
total += values[i];
return total;
}
Main()
{
i64[] values = new i64[100];
loop (i32 i = 0..values.Length)
values[i] = i;
Console.WriteLine(Sum(values));
}
}
LARVA_DUMP=<folder> makes larva build also write the whole program's optimized LLVM IR (.ll) and assembly (.s) into that folder, named after the output:
LARVA_DUMP=dump larva build -o sum.exe sum.ant
sum.exe (llvm back end, O3, entry sum::Sums.$Module.Main)
./sum.exe
4950
The module starts with the target, the runtime's data imports and the runtime functions the program calls:
; ModuleID = 'program'
source_filename = "program"
target datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-windows-msvc"
@__imp_larva_poll_pending = external local_unnamed_addr global ptr
@larva.type.0 = private constant { i32, i32, i32, i16, i16, [0 x i32] } { i32 8, i32 8, i32 5, i16 0, i16 0, [0 x i32] zeroinitializer }, align 8
@__imp_larva_heap = external local_unnamed_addr global ptr
declare ptr @larva_gc_array(ptr, i64) local_unnamed_addr
declare void @larva_print_i64(i64) local_unnamed_addr
declare void @larva_print_line() local_unnamed_addr
; Function Attrs: memory(inaccessiblemem: readwrite)
declare void @larva_poll() local_unnamed_addr #0
Sum at -O3: no bounds check, no poll (the loop does not allocate), and a vectorized loop over two 2-lane accumulators with a scalar remainder:
; Function Attrs: nofree norecurse nosync nounwind memory(argmem: read)
define i64 @"larva.sum::Sums.$Module.Sum"(ptr readonly captures(none) %0) local_unnamed_addr #2 {
entry:
%len = load i64, ptr %0, align 4, !tbaa !4, !range !6
%1 = trunc i64 %len to i32
%2 = icmp sgt i32 %1, 0
br i1 %2, label %bb2.preheader.split, label %bb4
…
vector.body: ; preds = %vector.body, %vector.ph
%index = phi i64 [ 0, %vector.ph ], [ %index.next, %vector.body ]
%vec.phi = phi <2 x i64> [ zeroinitializer, %vector.ph ], [ %8, %vector.body ]
%vec.phi12 = phi <2 x i64> [ zeroinitializer, %vector.ph ], [ %9, %vector.body ]
%4 = shl nuw nsw i64 %index, 3
%5 = getelementptr i8, ptr %0, i64 %4
%6 = getelementptr i8, ptr %5, i64 8
%7 = getelementptr i8, ptr %5, i64 24
%wide.load = load <2 x i64>, ptr %6, align 4, !tbaa !1
%wide.load13 = load <2 x i64>, ptr %7, align 4, !tbaa !1
%8 = add <2 x i64> %wide.load, %vec.phi
%9 = add <2 x i64> %wide.load13, %vec.phi12
%index.next = add nuw i64 %index, 4
%10 = icmp eq i64 %index.next, %n.vec
br i1 %10, label %middle.block, label %vector.body, !llvm.loop !7
The + 8 in the element address skips the length word. Even Main lost its fill loop: LLVM unrolled it into stores of constant vectors (store <2 x i64> <i64 0, i64 1>, …).
Optimization levels
| Flag | Pipeline | Code generator level | Notes |
|---|---|---|---|
-O0 |
none | None |
Locals stay in allocas; fastest to compile. |
-O1 |
default<O1> |
Default |
|
-O2 |
default<O2> |
Default |
The default of larva native and of the library API. |
-O3 |
default<O3> |
Default |
The default of larva build. |
The pipeline runs through LLVM's new pass manager (LLVMRunPasses) with one change: the inliner threshold is 500 instead of LLVM's default. Every handle a function keeps used to be marked as a root with a call, which the inliner counts like any call; the higher threshold lets small methods (getters, List.Add) inline as they would without the markers.
The same hello.ant at -O0 and -O3:
larva build --timings -O0 -o hello.exe hello.ant
hello.exe (llvm back end, O0, entry hello::Hello.$Module.Main)
timings: System.Core (compiled library) 7 ms, frontend 87 ms, HIR link 8 ms, lowering 198 ms, IR checks 73 ms, IR link 54 ms, native (llvm) 541 ms; total 968 ms
larva build --timings -o hello.exe hello.ant
hello.exe (llvm back end, O3, entry hello::Hello.$Module.Main)
timings: System.Core (compiled library) 7 ms, frontend 91 ms, HIR link 12 ms, lowering 125 ms, IR checks 101 ms, IR link 51 ms, native (llvm) 901 ms; total 1288 ms
The project's BUILD chunk can set optimization: (see The .queen file); the command line wins.
The processor: --cpu
LlvmProgram creates its target machine with a processor name and a feature string:
--cpu |
Processor | Features |
|---|---|---|
(none) or generic |
generic |
none beyond the triple's baseline (SSE2 on x86-64) |
native |
LLVM.GetHostCPUName() |
LLVM.GetHostCPUFeatures(): everything this machine has |
a name (x86-64-v3, znver3, …) |
that name | the name's own |
The default is generic so that a program runs on any machine of its platform; with native it may not run on an older processor. The value comes from --cpu, else the project's BUILD cpu, else larva config cpu. WebAssembly ignores it. The design notes measured a u64 loop going from 1.6 s to 0.66 s with native (AVX2), while a u256 loop stayed at about 3.8 s, because its carry chain does not vectorize.
The Sum loop above with --cpu native on a machine with AVX2 uses 256-bit registers and four accumulators:
LARVA_DUMP=dump larva build --cpu native -o sumn.exe sum.ant
sumn.exe (llvm back end, O3, entry sumn::Sums.$Module.Main)
.LBB4_7:
vpaddq 8(%rcx,%rax,8), %ymm0, %ymm0
vpaddq 40(%rcx,%rax,8), %ymm1, %ymm1
vpaddq 72(%rcx,%rax,8), %ymm2, %ymm2
vpaddq 104(%rcx,%rax,8), %ymm3, %ymm3
addq $16, %rax
cmpq %rax, %r8
jne .LBB4_7
Relocations follow the target: Windows and WebAssembly use LLVM's default model, ELF and Mach-O targets are compiled position-independent (executables are PIE, libraries shared).
Polly
Polly (LLVM's polyhedral loop optimizer) is not part of the LLVM library larva ships, and the C API cannot load pass plugins. So when asked for it, LlvmProgram.RunPolly writes the module as textual IR to a temporary folder, runs an external opt of the same LLVM version with -passes=default<On> -polly (plus -load-pass-plugin=<LLVMPolly> when Polly is a plugin of that opt, and any extra arguments), and parses the result back into the same context.
Polly is only reachable from larva native:
larva native -o <out> [--exe] [--backend llvm|x64] [--emit obj|asm|ll] [-O0..3] [--entry name] [--library] [--no-bce]
[--polly [--polly-opt <opt>] [--polly-plugin <LLVMPolly>]] <object.lvo>...
--polly-opt and --polly-plugin imply --polly. A failing opt is reported with its exit code and error output. Polly is not a BUILD key and larva build does not offer it.
larva native: objects to code
larva native is the low-level entry to this back end. It reads Larva IR object files (.lvo), links them with Linker.Link, and then:
| Options | Result |
|---|---|
--emit obj (default) |
an object file for this machine |
--emit asm |
the assembly listing |
--emit ll |
the optimized LLVM IR as text |
--exe |
a complete executable, through the same builder larva build uses |
--backend x64 |
the direct back end instead (objects only, no Polly) |
It prints one line with the format, the triple, the level, whether Polly ran, and how many bounds checks it kept and removed (compiled <n> object(s) to <out> (<emit>, <triple>, O<n>; bounds checks: <k> kept, <e> eliminated)). larva build does not write .lvo files, so the examples on this page use LARVA_DUMP instead. Objects compiled this way keep precise stack roots.
Parallel code generation
A large program is compiled on several threads (ParallelCodegen), when it is optimized (-O1 and up) and Polly is off:
- The number of partitions is
LARVA_LLVM_THREADSif set, else the number of processors (at most 16), but never more than one per 150 defined functions. A program under 300 functions is not split. Assignshares the functions out by size (IR statements plus terminators): the largest first, each to the least loaded partition.- Each thread creates its own
LLVMContextand a fullLlvmCodegenfor the whole program, but with bodies only for its own partition (the others are declarations: another object defines them). It verifies, optimizes and emitsprogram.<p>.obj. - The system linker puts the objects together.
The pieces fit because every Larva function is already a public symbol that other objects can call. Larva globals become link-once with a COMDAT (each object defines the ones it uses; the linker keeps one), while constant data, exception filters and larva_throw stay private to each object. Each object has its own stack map table, and the runtime reads every table in the image. The cost is that inlining does not cross partitions. With LARVA_TIMINGS=1 each partition reports its own times (partition 0: generate … ms, optimize … ms, emit … ms) under the phase llvm: build, optimize and emit (N threads, M functions).
Running code in memory
LlvmProgram.Jit() compiles a clone of the module with MCJIT inside larva's own process and returns a JitSession, whose Address(name) gives a function pointer. The larva_* declarations are bound to the in-process runtime (Larva.Native.NativeRuntime) instead of the C runtime. Three things make it work on 64-bit Windows:
- Unwind tables.
JitMemoryplaces all of a module's sections in one contiguous executable block and registers its.pdatawithRtlAddFunctionTable. Without that, Windows cannot unwind through generated frames, and any SEH exception, Larva's included, would crash the process. The module's.llvm_stackmapssection is handed to the runtime at the same time. - The personality routine. Unwind information refers to
__C_specific_handlerwith a 32-bit offset from the code, so the routine must sit next to it: the module defines its own__C_specific_handleras a tail jump to the system's. - 128-bit division. LLVM lowers
i128division and remainder to calls to__udivti3,__umodti3,__divti3and__modti3, which no C runtime provides inside larva's process.DefineWideDivisiondefines them in IR: widen toi256, divide (LLVM expands that into instructions), truncate. Being IR, they get the calling convention the calls expect.
No larva command uses this path today: larva build and larva native compile ahead of time to files, and larva run runs what larva build made (or interprets a .lvm module in the VM). It is a library API of Larva.Llvm and Larva.Native, and it keeps precise stack roots.
Environment variables
| Variable | Effect |
|---|---|
LARVA_TIMINGS=1 |
Print each phase's time to standard error. |
LARVA_DUMP=<folder> |
larva build also writes <name>.ll and <name>.s of the whole optimized program. |
LARVA_LLVM_ARGS="<options>" |
Options for LLVM's own command-line parser. |
LARVA_LLVM_THREADS=<n> |
Partitions for parallel code generation. |
LARVA_POLL_OPAQUE=1 |
Safepoint polls become ordinary calls again. |
LARVA_WASM_FEATURES, LARVA_WASM_HEAP=0, LARVA_ROOT_TBAA=0 |
WebAssembly settings: see WebAssembly. |
Limits
- Targets: x86-64 Windows (
x86_64-pc-windows-msvc), x86-64 and AArch64 Linux, x86-64 and arm64 macOS, andwasm64-unknown-unknown.larva builditself only produces Windows executables and WebAssembly today; the Linux and macOS builders exist as library code (see Native executables). - Polly needs an external
optand is available only throughlarva native. --no-bceis only alarva nativeoption.
Editar esta página en GitHub Reportar un problema