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.

Última actualización: 28 min de lectura Editar en GitHub

En esta página

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:

Terminal
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:

AntrilRun
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");
		}
	}
}
Terminal
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:

Terminal
./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

AntrilRun
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:

Terminal
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:

Terminal
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:

Terminal
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:

  1. The number of partitions is LARVA_LLVM_THREADS if 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.
  2. Assign shares the functions out by size (IR statements plus terminators): the largest first, each to the least loaded partition.
  3. Each thread creates its own LLVMContext and a full LlvmCodegen for the whole program, but with bodies only for its own partition (the others are declarations: another object defines them). It verifies, optimizes and emits program.<p>.obj.
  4. 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. JitMemory places all of a module's sections in one contiguous executable block and registers its .pdata with RtlAddFunctionTable. Without that, Windows cannot unwind through generated frames, and any SEH exception, Larva's included, would crash the process. The module's .llvm_stackmaps section is handed to the runtime at the same time.
  • The personality routine. Unwind information refers to __C_specific_handler with a 32-bit offset from the code, so the routine must sit next to it: the module defines its own __C_specific_handler as a tail jump to the system's.
  • 128-bit division. LLVM lowers i128 division and remainder to calls to __udivti3, __umodti3, __divti3 and __modti3, which no C runtime provides inside larva's process. DefineWideDivision defines them in IR: widen to i256, 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, and wasm64-unknown-unknown. larva build itself only produces Windows executables and WebAssembly today; the Linux and macOS builders exist as library code (see Native executables).
  • Polly needs an external opt and is available only through larva native.
  • --no-bce is only a larva native option.

¿Te resultó útil esta página?

No se guarda ninguna dirección; tu nombre de usuario sí, si iniciaste sesión.

Esc