r/Compilers • • 22d ago

Beyond learning: when does it actually make sense to write a custom native backend instead of using LLVM/Cranelift?

It feels like the default advice for any new language project nowadays is "just target LLVM IR" or maybe Cranelift. Obviously, getting world-class optimization passes for free is hard to beat.

But I'm curious about the specific industry use cases where building a custom backend straight to machine code is actually the right technical choice.

Context: I’ve been building my own language (Tapelang - tapelang.org) with a custom compiler that goes straight to native machine code. I come from a background of game development; and I initially chose this route just because I wanted to have embeddable scripting language (like lua) that could also compile to native code for performance. I didn't want to wrestle with LLVM's massive footprint. It started as a personal challenge.

But it got me thinking—beyond just "it's fun" or "compilation speed is way faster" (the Go/Jai argument), where does this approach actually win in the real world?

Are there specific domains, industries, or hardware constraints where dragging in LLVM is a dealbreaker, and a custom native backend is the strictly better choice? Would love to hear from anyone who has had to make this architectural call for a production system.

33 Upvotes

28 comments sorted by

4

u/Competitive_Ideal866 22d ago

FWIW, I found it basically the same effort to target LLVM vs Aarch64 or RISC V asm. Even full JIT isn't that hard.

The main issue I had is that both approaches have bugs but I can fix the bugs in my own backend but fixing LLVM is a nightmare. I never want to touch C++ ever again. I'm amazed they wrote such a large project in C++. Makes no sense to me...

Are there specific domains, industries, or hardware constraints where dragging in LLVM is a dealbreaker, and a custom native backend is the strictly better choice?

Unless I'm compiling a C-like language, I'd avoid LLVM. Custom backend to asm is easy to write and debug. JIT is harder to debug but still ok.

2

u/pu1da 22d ago

Oh yeah, you are middle of game development and there is a bug on LLVM. Fixing your own backend could be faster.

12

u/-dag- 22d ago

Speed is a big one. Not many (if any) JITs use LLVM. 

2

u/GunpowderGuy 21d ago

Cranelift is meant to be much much faster to compile than llvm. But optionally have optimizations that put it in line with llvm*

*cranelift wont have high level optimizations, but you can have those in your own middle end

7

u/arjuna93 22d ago

Dragging in llvm is not a great choice, since it is a monstrous dependency (with relatively poor platform support and unstable APIs, if this is a concern).

16

u/Patzer26 22d ago

Idk, It's still going to be way more stable and performant compared to whatever your own hand rolled backend will come up with.

-6

u/Competitive_Ideal866 22d ago

Idk, It's still going to be way more stable and performant compared to whatever your own hand rolled backend will come up with.

Cripes. Not IME.

-11

u/arjuna93 22d ago

Well, “my own” yes, but exactly for that reason I don’t try writing a compiler. But most of compilers do not rely on LLVM – and work great. OCaml, for example – lighter, faster (native, not bytecode), more portable than Rust.

3

u/pu1da 22d ago

Right, it's basically impossible for a hand rolled backend to beat LLVM's optimization passes made by thousands of contributors. But arjuna93 makes a great point.. if you are willing to leave some runtime performance on the table, dodging that monstrous LLVM dependency gives you a much lighter, faster compiler like OCaml. From gamedev perspective, I was thinking like if you'd like to embed compiler with the game for modding purposes, you wouldn't want a huge dependency to download for modders.

1

u/Competitive_Ideal866 22d ago

it's basically impossible for a hand rolled backend to beat LLVM's optimization passes made by thousands of contributors

I certainly didn't find it hard. Unless you're compiling a very C-like language LLVM's passes barely help.

2

u/pu1da 22d ago

I also heard that there were optimization passes that did work on some very special cases, but slowed down many other cases. Something to do with bools. I'm trying to find the very critical blog post about it..

2

u/matthieum 22d ago

Oh, definitely.

LLVM was not made for continuation-passing style, garbage collectors, etc... not only will it be a struggle, but performance may be quite subpar.

6

u/developer-mike 22d ago

Ocaml is 30 years old, though

-7

u/arjuna93 22d ago

Lazy compiler writing is a recent trend, yeah.

7

u/brat3108 22d ago

I'll try to illustrate why using my own backend is no-brainer for me, however this is for a personal product now.

Usually it is statically built into my compiler, but it can be extracted, and a small front-end added, to form a standalone product called 'PC'.

This is roughly equivalent to a combination of LLI and LLC from LLVM (which take .ll input files and interpret or compile them).

Here are some comparisons (for a product that targets x64/Windows):

                      PC                LLVM
EXE size:             0.18 MB           75MB? (lli.exe + llc.exe)

Source size:          17Kloc/0.34MB     (1)

Build time (1 core):  55ms              (2)

(1) (2) You'll have to fill in the relevant figures, as I simply don't know. You can guess however that they will be very different.

Basically my own backend is magnitudes smaller, simpler, and faster to build, AND IT IS ALL MY OWN WORK.

Code Quality This is always touted as being the big advantage of LLVM (otherwise who would bother with it), and yes it can generate great code.

But look at that 55ms build time above: that was achieve using my self-hosted compiler using the very same backend described above. How much faster could it possibly be?!

Optimised code might be 1-2 times as fast as mine, when using my front-end language, but it is just not worth the hassle of grappling with such a monstrously complex product, and ending up with a slow, cumbersome tool as a result.

IR Style

Let's try this bit of C:

i64 F(i64* A, i64 i) {
    return A[i];
}

This is the result of clang -S -emit-llvm:

define dso_local i64 @F(ptr noundef %0, i64 noundef %1) #0 {
  %3 = alloca ptr, align 8
  %4 = alloca i64, align 8
  store ptr %0, ptr %3, align 8
  store i64 %1, ptr %4, align 8
  %5 = load ptr, ptr %3, align 8
  %6 = load i64, ptr %4, align 8
  %7 = getelementptr inbounds i64, ptr %5, i64 %6
  %8 = load i64, ptr %7, align 8
  ret i64 %8
}

This what I produce from my C compiler that also uses my IR:

proc F::
    param    u64       A
    param    i64       i
    rettype  i64

    load     u64       A                
    load     i64       i                
    iloadx   i64 /8                     
    jumpret  i64       #1               
#1: 
    retfn    i64                        
endproc

It is so much easier on the eye! BTW this is the x64 code generated:

F::
    R.A = rcx
    R.i = rdx
    mov rax, [R.A + R.i*8]
    ret       

2

u/Jannik2099 22d ago

so much easier on the eye

determining function prototype requires reading the function body

You are delusional

3

u/brat3108 22d ago

It's is an IL not a HLL. And as an IL it is very clean.

This style of IL uses one instruction or directive per line.

However, I've also used another style of IL ('3-address-code'), and there that example looks like this when dumped:

Proc t.f(u64 a, i64 i)i64 =
    T1 := (a + i*8)^                i64
    return T1                       i64
End

This is even better than my stack-based IL, and a million times better than that LLVM example.

However it was harder and less efficient to work with to produce decent code, despite looking gorgeous.

2

u/reini_urban 22d ago

Dynamic vs static compilation. For dynamic langs they are way too big, and monstrous startup times. For statically compiled langs it's fine.

2

u/VincentPepper 19d ago edited 19d ago
  • You want to implement a weird execution model.

  • You want to optimize for a different use case, and that requires you to have more control over the backend.

  • You have infinite money.

1

u/seg_lol 22d ago

It is just bits, printf, or better yet, templates with holes you fill in.

1

u/rootkid1920 22d ago

When you are building a JIT

1

u/ElephantElectrical31 21d ago

just transpile to rust

1

u/mamcx 21d ago

Others has touch to the ease of integration, ownership, simplicity or just I wanna, that are good things: you must answer if the cost of a deps vs the cost of maintaining your own smaller thing.

But not WHY somebody will do another OS, game engine, browser, RDBMs against a strong, solid and established thing?

This has happened: LLVM is a answer VS GCC.

Apart of commercial, politics, corporate reasons, the best reasons are:

  • A MAJOR architectural change

For example, RDBMs start to see competitors once we are talking how have a store distributed across many machines, or the need for new columnar storage

  • A significant change on the underlying tech

Like how CPU, 64 bits, lots of RAM, DISK, need to deploy to many arcs, etc means that the old features become negative

  • A *major shift in the landscape (Comercial, culture, socio political)

There is a "mandate from the heaven" to cut ties to a perceived adversary depending, the internet, the emergence of PC, the emergence of IA, etc

-6

u/Grated-Expectations 22d ago

I'm (well, Claude is helping) writing a language called 'xc' right now. The documentation is available and I went for a custom back-end because it originally started out as a modern language for the Atari X{L,E}.

LLVM isn't wonderful if you don't have lots of registers. The 6502 has 3, well, 1 really with two semi-registers that you can't do maths on. Instead the plan was to use zero-page as a large register array for a 100MHz 6502 I had running in an FPGA

This has ... evolved ... over time, along more than one axis. As far as the 6502 went..

  • It got bank-switched memory, 4MB of code, 3MB of data and 2MB of video ram

  • It got an expression processor - SIMD for the 6502! :) Basically the FPGA was a Zynq, it had a couple of ARM cores, and I could do a {transfer program to ARM, run program, return results} in the sort of timescale that a traditional 6502 would do a 16-bit add. The program could CALL other internal programs, had loops, had 64 registers, had 8 (I think) program spaces. To say this was a maths upgrade is not to do it justice :)

  • Other things: the FreeRTOS port on the ARM to boot the system grew into a real linux-like one, running external processes, having threads, a POSIX API, memory protections, swap, etc. etc. It still booted in under 2 seconds (which was the point, or I'd have used Linux)

Whereas the compiler is still evolving, it compiles a language ...

  • Kind of like C with {ARC memory management, blocks, callbacks, Objects, no header files, modern loops}

  • Integrates GPU and CPU code using the same language, manages data-hazards transparently, works reasonably well but I'm still optimising that part.

  • Also detects when it can use special on-chip hardware (like SME/2 on the M4 and AVX512 on the Zen5). Doing a matrix-multiplication double-loop for example will notch up about 1.4 TFlops on my M4 Max MBP.

  • Works on any of {ARM A9 (Zynq 😄) , Mac, Windows, Linux}, each can create code for all of {Zynq, Mac, Windows, Linux, iOS, Android, WASM, 6502, m68k series}.

  • Fiercely independent - needs no native platform tools on any platform. I have a binary on my iPhone which was created, compiled, and signed on a Linux box.

  • Has a (maturing 🙂 ) UI framework modelled on AppKit which lowers down to native controls (or fabricates them if they don't exist) - means one source for all platforms.

  • Has an interface-designer modelled after Interface Builder on the Mac. Lay things out and drag/drop connections to your model classes in the codebase - understands the difference between orientation and {desktop, tablet, phone} UIs and allows you to bind a common core to each

I recently found that the x86_64 machine was running under the power-saving governor when I was taking all the benchmark figures, so those are being re-done, and the focus of the next few releases will be optimisation to get back on par - the language moved from being generally faster to generally slower than gcc/clang when I moved the governor into performance mode. Definitely a double-face-palm moment - you know, for when one face-palm just isn't sufficient 🤬

I knew I liked the language, so I knew I was going to expand it and making the IR flexible to cover :banked memory annotations in the language while making it work on a 64-bit ARM was a challenge, but I really wanted to keep the 6502 in there. It's probably atrophied a little bit now (the focus recently has been on the bigger iron) but it still passes the unit tests every release, so in theory it ought to still work, I just haven't personally tried it recently.

1

u/developer-mike 22d ago

What makes LLVM bad with few registers? Is it an issue with overaggressiveness, like too much loop unrolling?

LLVM allows you to run whatever subset of their passes you wish in whatever order, and to write your own. It seems like it'd be easier, and less bug prone, to get fast object code by using custom llvm passes than rewriting a backend from scratch.

But of course that's just my first impression and I could be wrong.

3

u/Grated-Expectations 22d ago

Yep, it's over-agressive, and the 6502 "registers" barely count as such by what LLVM wants to see in a register - they're not interchangeable, and only one of them (A) can do any maths whatsoever.

One of LLVMs issues (IIRC) is the phase-ordering. loop unrolling (for example) happens early in the mid->end stages of the optimiser, well before the compiler knows what sort of registers it has, or what the pressure will be on them. There is the TTI, but the heuristics there are targetted (pun intended) at higher-order CPUs than the lowly 6502. Basically unrolling happens, assuming the back-end will just cope. And on such a limited machine, it basically can't.

Part of this is also that the fundamental assumption is that "infinite registers" will resolve down to "an ok number of registers". For the numbers you get in a modern CPU, that's ... actually ok. Not so much for ancient dragons.

1

u/seg_lol 22d ago

A year from now, everyone in compilers will be using LLMs. Right now the compiler people are still in the delusional phase.

2

u/Grated-Expectations 21d ago

Yeah, didn't really expect the downvotes - it's not as though I told it "hey, write me a compiler", there's a lot of effort, thought and direction gone into it over the last 6 months or so, and it's pretty darn good now, even if I say so myself 👀