Recently, I did an experiment by switching on only a fast and simple RA and combiner in GCC. There are no options to do this, I needed to modify GCC. (I can provide a patch if somebody is interested.) Compared to hundreds of optimizations in GCC-9.0 with -O2, these two optimizations achieve almost 80% performance on an Intel i7-9700K machine under Fedora Core 29 for real-world programs through SpecCPU, one of the most credible compiler benchmark suites - Vladimir Makarov behind MIR
This project aims to get 70% of LLVM / GCC perf from 10% of the code. That’s probably easy on average. There are a lot of things in LLVM that give a huge perf win for a tiny subset of inputs. If you care about the performance of something that hits one of these on its most critical path, you will see a huge difference. If you care about aggregate performance over a large corpus, very few of these makes a very significant overall difference. - david_chisnall
LLVM does NOT provide full C compatibility for you. In more technical terms, any language that provides good C compatibility and uses LLVM as a backend needs to reimplement large chunks of the ABI in its frontend! This well known issue in the LLVM community causes a great deal of duplication and bugs. Implementing a complete C ABI (with struct arguments and returns) is incredibly tricky, and not really a lot of fun. QBE provides you with IL operations to call in (and be called by) C with no pain. Moreover the ABI implementation in QBE has been thoroughly tested by fuzzing and manual tests. - QBE LLVM comparison page
N.b.
Choosing C as a JIT interface language significantly simplifies JIT implementation, maintenance, and debugging. Doing so also makes the JIT independent from a particular C compiler. Precompiled headers are a pretty standard feature of modern C/C++ compilers. - Vladimir Makarov talking about CRuby’s MJIT
Generating LLVM IR isn’t as easy as it should be (ABI handling is, and always has been, a total mess) but it’s still much easier than generating valid C. - david_chisnall
QBE
QBE is a compiler backend that aims to provide 70% of the performance of industrial optimizing compilers in 10% of the code … to focus on the essential and prevent embarking on a never-ending path of diminishing returns.
It has its own intermediate language:
function w $add(w %a, w %b) { # Define a function add
@start
%c =w add %a, %b # Adds the 2 arguments
ret %c # Return the result
}
export function w $main() { # Main function
@start
%r =w call $add(w 1, w 1) # Call add(1, 1)
call $printf(l $fmt, ..., w %r) # Show the result
ret 0
}
data $fmt = { b "One and one make %d!\n", b 0 }
It outputs assembly, which you must then build and link.
On my benchmarks QBE generates code whose geomean performance is only 65% of GCC with -O2 - competing MIR project
The bibliography is enlightening.
A few (compiler frontend) projects build on top of it:
Last time I looked at QBE, it conflated pointer and integer types, which made it impossible to target platforms I care about. - david_chisnall (2024)
QBE is unsuited for lisps, lacking anything for tail call optimization and lacking stack maps at safepoints like LLVM’s gc.statepoint or gc.root lacking GC support and forcing you on a shadow stack.
MIR
I set performance goals for the JIT compiler. Compared to GCC with -O2, this compiler should have 100 times faster compilation, 100 times faster start-up, and 100 times smaller code size. As for the generated code performance, I decided that it should be at least 70% of GCC -O2 performance. - Vladimir Makarov
Here is Eratosthenes’ prime sieve:
m_sieve: module
export sieve
sieve: func i32, i32:iter
local i64:flags, i64:count, i64:prime, i64:n, i64:i, i64:k, i64:temp
alloca flags, 819000
mov n, 0
loop:
bge fin, n, iter
mov count, 0; mov i, 0
loop2:
bgt fin2, i, 819000
mov ui8:(flags, i), 1; add i, i, 1
jmp loop2
fin2:
mov i, 0
loop3:
bgt fin3, i, 819000
beq cont3, ui8:(flags,i), 0
add temp, i, i; add prime, temp, 3; add k, i, prime
loop4:
bgt fin4, k, 819000
mov ui8:(flags, k), 0; add k, k, prime
jmp loop4
fin4:
add count, count, 1
cont3:
add i, i, 1
jmp loop3
fin3:
add n, n, 1; jmp loop
fin:
ret count
endfunc
endmodule
antcc
https://codeberg.org/lsof/antcc
Currently still in an alpha stage, but can successfully build some real-world C codebases such as Lua, SQLite, oksh, tin, DOOM and itself.
chibicc
https://github.com/rui314/chibicc
the reference implementation for a book I’m currently writing about the C compiler and the low-level programming. The book covers the vast topic with an incremental approach; in the first chapter, readers will implement a “compiler” that accepts just a single number as a “language”, which will then gain one feature at a time in each section of the book until the language that the compiler accepts matches what the C11 spec specifies. I took this incremental approach from Incremental Approach to Compiler Construction by Abdulaziz Ghuloum.
Each commit of this project corresponds to a section of the book. For this purpose, not only the final state of the project but each commit was carefully written with readability in mind. Readers should be able to learn how a C language feature can be implemented just by reading one or a few commits of this project. For example, this is how while, [], ?:, and thread-local variable are implemented. If you have plenty of spare time, it might be fun to read it from the first commit.
Jack Crenshaw’s Let’s Build a Compiler! has the same pedagogical approach.
Tangential…
Eclipse OMR is a set of components for high performance runtimes.