Why do people prefer LLVM IR, and how exactly is it different from the GCC IR? Is target dependency a factor here?
I'm a complete newbie to compilers, and wasn't able to find anything relevant even after many hours of searching for an answer. Any insights would be helpful.
Thus GIMPLE (GCC's IR) was never considered to be more than an implementation detail, in particular it doesn't provide a full description of compiled program (e.g. it lacks program's call graph, type definitions, stack offsets and alias information).
While LLVM's Clang C/C++ compiler was traditionally known for its faster build speeds than GCC, in recent releases of GCC the build speeds have improved and in some areas LLVM/Clang has slowed down with further optimization passes and other work added to its growing code-base.
You can think of LLVM IR as a platform-independent assembly language with an infinite number of function local registers. When developing compilers there are huge benefits with compiling your source language to an intermediate representation (IR) 1 instead of compiling directly to a target architecture (e.g. x86).
LLVM is a backend compiler meant to build compilers on top of it. It deals with optimizations and production of code adapted to the target architecture. CLang is a front end which parses C, C++ and Objective C code and translates it into a representation suitable for LLVM.
Firstly, as this answer touches on complex and sensitive topics I want to make few disclaimers:
The most important fact is that LLVM IR and GCC IR (called GIMPLE) are not that different in their core - both are standard control-flow graphs of basic blocks, each block being a linear sequence of 2 inputs, 1 output instructions (so called "three-address code") which have been converted to SSA form. Most production compilers have been using this design since 1990-s.
Main advantages of LLVM IR are that it's less tightly bound to compiler implementation, more formally defined and has nicer C++ API. This allows for easier processing, transformation and analysis, which makes it IR of choice these days, both for compiler and for other related tools.
I expand on benefits of LLVM IR in subchapters below.
LLVM IR originally designed to be fully reusable across arbitrary tools besides compiler itself. The original intent was to use it for multi-stage optimization: IR would be consequently optimized by ahead-of-time compiler, link-time optimizer and JIT compiler at runtime. This didn't work out but reusability had other important implications, most noticeably it allowed easy integration of other types of tools (static analyzers, instrumenters, etc.).
GCC community never had desire to enable any tools besides compiler (Richard Stallman resisted attempts to make IR more reusable to prevent third-party commercial tools from reusing GCC's frontends). Thus GIMPLE (GCC's IR) was never considered to be more than an implementation detail, in particular it doesn't provide a full description of compiled program (e.g. it lacks program's call graph, type definitions, stack offsets and alias information).
The idea of reusability and making IR a standalone entity led to an important design consequence in LLVM: compilation passes can be run in any order which prevents complex inter-pass dependencies (all dependencies have to be made explicit via analysis passes) and enables easier experimentation with compilation pipeline e.g.
Standalone IR allows LLVM to use IR-level unit tests which allows easy testing of optimization/analysis corner-cases. This is much harder to achieve through C/C++ snippets (as in GCC testsuite) and even when you manage, the generated IR will most likely change significantly in future versions of the compiler and the corner case that your test was intended for will no longer be covered.
Standalone IR enables easy combination of IR from separate translation units with a follow-up (whole program) optimization. This is not a complete replacement for link-time optimization (as it does not deal with scalability issues which arise in production software) but is often good enough for smaller programs (e.g. in embedded development or research projects).
Although criticized by academia, LLVM IR has a much stricter semantics compared to GIMPLE. This simplifies implementation of various static analyzers e.g. IR Verifier.
LLVM IR is generated directly by the frontend (Clang, llgo, etc.) and preserved throughout the whole middle-end. This means that all tools, optimizations and internal APIs only need to operate on single IR. The same is not true for GCC - even GIMPLE has three distinct variants:
Compared to GIMPLE, LLVM IR was deliberately made simpler by reducing number of cases which IR consumers need to consider. I've added several examples below.
All basic blocks in LLVM IR program have to end with explicit control-flow opcode (branch, goto, etc.). Implicit control flow (i.e. fallthrough) is not allowed.
In LLVM IR virtual registers do not have memory. Stack allocations are represented by dedicated alloca
operations. This simplifies working with stack variables e.g. equivalent of GCC's ADDR_EXPR
is not needed.
Contrary to GIMPLE which has plethora of opcodes for memory references (INDIRECT_REF, MEM_REF, ARRAY_REF, COMPONENT_REF, etc.), LLVM IR has only plain load and store opcodes and all complex arithmetic is moved to dedicated structured indexing opcode, getelementptr.
LLVM IR provides dedicated pseudo-instructions for garbage-collected languages.
While C++ may not be the best programming language, it definitely allows to write much simpler (and in many case more functional) system code, especially with post-C++11 changes (LLVM aggressively adopts new Standards). Following LLVM, GCC has also adopted C++ but majority of the codebase is still written in C style.
There are too many instances where C++ enables a simpler code so I'll just name a few.
The hierarchy of operators in LLVM is implemented via standard inheritance and template-based custom RTTI. On the other hand GCC achieves the same via old-style inheritance-via-aggregation
// Base class which all operators aggregate struct GTY(()) tree_base { ENUM_BITFIELD(tree_code) code : 16; unsigned side_effects_flag : 1; unsigned constant_flag : 1; unsigned addressable_flag : 1; ... // Many more fields }; // Typed operators add type to base data struct GTY(()) tree_typed { struct tree_base base; tree type; }; // Constants add integer value to typed node data struct GTY(()) tree_int_cst { struct tree_typed typed; HOST_WIDE_INT val[1]; }; // Complex numbers add real and imaginary components to typed data struct GTY(()) tree_complex { struct tree_typed typed; tree real; tree imag; }; // Many more operators follow ...
and tagged union paradigms:
union GTY ((ptr_alias (union lang_tree_node), desc ("tree_node_structure (&%h)"), variable_size)) tree_node { struct tree_base GTY ((tag ("TS_BASE"))) base; struct tree_typed GTY ((tag ("TS_TYPED"))) typed; struct tree_int_cst GTY ((tag ("TS_INT_CST"))) int_cst; struct tree_complex GTY ((tag ("TS_COMPLEX"))) complex;
All GCC operator APIs use the base tree
type which is accessed via fat macro interface (DECL_NAME
, TREE_IMAGPART
, etc.). Interface is only verified at runtime (and only if GCC was configured with --enable-checking
) and does not allow static checking.
LLVM generally provides simpler APIs for pattern matching IR in optimizers. For example checking that instruction is an addition with constant in GCC looks like
if (gimple_assign_p (stmt) && gimple_assign_rhs_code (stmt) == PLUS_EXPR && TREE_CODE (gimple_assign_rhs2 (stmt)) == INTEGER_CST) { ...
and in LLVM:
if (auto BO = dyn_cast<BinaryOperator>(V)) if (BO->getOpcode() == Instruction::Add && isa<ConstantInt>(BO->getOperand(1)) {
Due to C++ support for overloading, LLVM can uses arbitrary-precision ints for all computations whereas GCC still uses physical integers (HOST_WIDE_INT
type, which is 32-bit on 32-bit hosts):
if (!tree_fits_shwi_p (arg1)) return false; *exponent = tree_to_shwi (arg1);
As shown in the example this can lead to missed optimizations.
GCC has got an equivalent of APInt
s few years ago but the majority of the codebase still uses HOST_WIDE_INT
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With