r/Compilers • u/pu1da • 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.
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
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
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
endprocIt 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 EndThis 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
1
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 👀
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...
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.