GCC
The other mature toolchain, and a genuinely different architecture: GENERIC, then GIMPLE, then RTL, then a target. Three successive intermediate representations where LLVM has one, an extension model based on plugins rather than libraries, and a different licence history.
How does GCC differ from LLVM architecturally, without either of us picking a side?
Three representations in succession, each lower than the last. GENERIC is a language-independent tree produced by each frontend — C, C++, Fortran, Ada, Go, D and others all converge on it. GIMPLE is a three-address form derived from GENERIC by "gimplification", with a restricted grammar that makes analysis tractable, and SSA is applied to it for the optimization passes that want it. RTL, Register Transfer Language, is a low-level form close to the machine, expressed as Lisp-like expressions over machine modes, on which instruction selection and register allocation operate. The question each answers is different: GENERIC unifies the frontends, GIMPLE enables the optimizer, RTL describes the machine.
Each level is entitled to assume the previous one produced a well-formed instance of its own grammar — GIMPLE has a defined restricted form and a verifier, and a pass that produces invalid GIMPLE is a bug caught immediately rather than three passes later. Transformations at each level are legal only if they preserve the semantics of the source language as expressed by the language hooks each frontend supplies: GCC keeps a per-language interface for things the middle-end must ask about, so the middle-end is language-independent in structure while remaining language-aware through those hooks. At RTL, legality is additionally constrained by the target description: a transformation is valid only if the resulting pattern is one the machine description says the target can encode.
Key points
- GCC uses three successive representations — GENERIC, GIMPLE, RTL — where LLVM uses one plus a machine IR.
- GIMPLE's restricted three-address grammar is what makes hundreds of analysis passes tractable to write.
- A transformation must run at the level where the facts it needs still exist, which is why passes have migrated between GIMPLE and RTL over time.
- GCC is extended with plugins loaded into the compiler; LLVM is consumed as libraries linked into your program.
- The licences differ — GPL versus Apache 2.0 with exceptions — and that is a genuine input to toolchain selection rather than a quality signal.
- Neither reliably produces faster code than the other; the difference is per-program, per-version and per-target.
- Building with both is a cheap way to find warnings and latent undefined-behaviour bugs that one alone would miss.
Three IRs, and why there are three
GCC's middle is layered rather than singular. Each frontend produces GENERIC, a tree form general enough to represent any of the supported languages, which is the point at which a Fortran program and an Ada program become comparable objects. Gimplification then lowers GENERIC into GIMPLE, whose grammar is deliberately restricted — at most three operands per statement, no nested expressions, explicit temporaries — because a restricted grammar is what makes writing hundreds of analysis passes tractable. Most of the interesting optimization happens here, on GIMPLE in SSA form.
RTL is the third level and the oldest. It describes computation as expressions over machine modes, close enough to hardware that instruction selection is expressed as pattern matching against a machine description file, and register allocation and scheduling operate on it. The staging means a transformation lives at whichever level still has the facts it needs — high-level loop transformations on GIMPLE, peephole rewrites on RTL — which is the same argument [[ir-levels]] makes in general.
The comparison with LLVM is instructive and is genuinely a difference of design rather than of quality. LLVM has one IR spanning the range GIMPLE covers, plus a machine IR corresponding roughly to RTL's role. GCC gets a cleaner separation between "high enough to reason about the program" and "low enough to reason about the machine", at the cost of more lowering steps and more representations to maintain, verify and debug. Both shapes work; each makes some passes natural and others awkward.
- Sourceyou write itC, C++, Fortran, Ada, Go, D, Objective-C or another supported language.
- Frontendbuild timeLanguage-specific parsing and checking, then a tree.Everything the language guarantees; each frontend is entirely separate above this point.
- GENERICbuild timeA language-independent tree form that every frontend produces.A common vocabulary. This is where the frontends converge — see
[[multiple-frontends-one-backend]].The source language, except through the language hooks the middle-end can still call. - GIMPLEbuild timeA restricted three-address form, put into SSA for most passes.A grammar simple enough to write hundreds of analyses against — see
[[three-address-code]].Nested expression structure; every intermediate now has a name. - RTLbuild timeLisp-like expressions over machine modes, close to the target.Machine reality: modes, addressing forms, and patterns the target description can match.The high-level structure loop optimizations needed. Those had to run on GIMPLE.
- Assembly and object codebuild timeTarget instructions, then an object file.Encodable instructions for one architecture.
Read it asRead the loses column: each lowering discards something the previous level was needed for, which is exactly why a pass must run at the level where its facts still exist. That constraint is why GCC has moved passes between GIMPLE and RTL over the years, and it is the same constraint that puts Rust's borrow check on MIR rather than on a tree.
Two extension models
LLVM is consumed as libraries: you link it into your program, construct modules, run passes, and the infrastructure is a dependency of your compiler. GCC is consumed as a compiler you extend: plugins load into the running compiler and register passes and callbacks, and there is a documented plugin API for doing so.
The practical differences follow from that shape. A plugin extends a build that is already happening, which suits static analysis, instrumentation and project-specific checks; the Linux kernel's GCC plugins are the canonical example. A library lets you build something that is not a compiler at all — a query JIT, a shader compiler — which is why those uses cluster on LLVM. Neither model makes the other impossible, and both projects have moved toward the middle over time, but the centre of gravity is different and it explains a lot about which project gets used for what.
This is also where the licence history belongs, stated as fact rather than as advocacy. GCC is under the GPL, LLVM is under Apache 2.0 with LLVM exceptions (having been BSD-style before version 9). The licences differ in what they require of derivative works, and organisations choose accordingly: permissive licensing is one documented reason for LLVM's adoption in proprietary and embedded toolchains, and copyleft is a deliberate, valued property for others. Both projects are enormous, mature and excellent, and the licence is a genuine engineering and legal input rather than a merit ranking.
| Dimension | GCC | LLVM |
|---|---|---|
| Middle representations | GENERIC, then GIMPLE (in SSA), then RTL | One IR spanning most of that range, plus a machine IR |
| Consumed as | A compiler you extend with plugins | Libraries you link into your own program |
| Frontends in tree | C, C++, Fortran, Ada, Go, D, Objective-C, Modula-2 | C-family via Clang, Fortran via Flang; other languages are separate projects |
| Licence | GPL, with a runtime library exception | Apache 2.0 with LLVM exceptions; BSD-style before version 9 |
| Target breadth | Very wide, including architectures with no LLVM backend | Wide, and the usual choice for new architectures |
| Typical strengths citedtypical | Some numeric and Fortran code; breadth of supported targets | Tooling ecosystem, library reuse, JIT clients |
What "both are excellent" actually means in practice
It means that on any given program, either may produce faster code, and which one does is not predictable from the architecture. Published comparisons swing by benchmark, by version, by target and by flag set, and the honest summary is that the two are close, that the gap moves in both directions between releases, and that a difference on your program is a fact about your program.
It also means the real selection criteria are usually not code quality. Which toolchain your platform ships and supports; whether your target architecture has a mature backend in both; whether you need a library to build a non-compiler tool; licence obligations; and which one your dependencies and your CI already assume. Those decide it far more often than a benchmark does.
The most useful practice is to build with both. Each finds warnings the other misses, each occasionally exposes a latent bug through a different valid interpretation of undefined behavior, and a project that compiles cleanly under both is more likely to be standard-conforming than one tuned to a single implementation. That is a testing argument rather than a partisan one, and it is the same logic as [[differential-testing]] applied to your own code.
How it works
The steps, in the order the compiler takes them.
- A language frontend parses and checks its own language and produces a GENERIC tree.
- Gimplification lowers GENERIC to GIMPLE, introducing explicit temporaries so that no statement has nested expressions.
- GIMPLE is converted into SSA form, and the bulk of the middle-end optimization passes run over it, including interprocedural passes and the loop optimizers.
- Out-of-SSA conversion runs, and GIMPLE is expanded into RTL, guided by the target's machine description.
- RTL passes perform instruction combination, scheduling, register allocation and peephole optimization, with every produced pattern required to be one the machine description can match.
- Final assembly output is emitted, and the driver invokes the assembler and linker as required.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- Code that compiles under one toolchain fails under the other, and the cause is a non-standard extension the first accepted silently.
- A program works when built by one compiler and misbehaves when built by the other, because it contained undefined behavior and the two exploited it differently — see
[[ub-and-optimization]]. - A plugin stops loading after a GCC upgrade, because the plugin interface tracks internal structures that changed.
- A benchmark comparison is quoted as a general result when it measured one program, one version and one flag set, and the next release reverses it.
- A build assumes GCC-specific flags or attributes and breaks on a platform where the system compiler is Clang, or the reverse — a portability failure with no compile error until the other platform is tried.
- A project targets an architecture with a mature backend in only one toolchain, and the choice that looked like a preference turns out to have been a constraint.
When it helps
- Choosing a toolchain deliberately, on grounds — target support, licence, extension model, platform defaults — rather than on reputation.
- Reading GCC-specific diagnostics, dumps and flags with a model of which level they refer to.
- Adding compile-time checking or instrumentation to an existing build via plugins, without turning your project into a compiler.
- Improving portability and standard conformance by building the same source with both toolchains routinely.
When it hurts
- Treating one toolchain's behaviour as the language definition. Both are implementations, and the differences between them are usually where your code was relying on something unspecified.
- Extending the compiler with plugins when the goal is really a separate tool: a plugin runs inside a build, which is the wrong shape for anything you want to run independently.
- Assuming a performance comparison generalises. The gap is per-program and moves between releases in both directions.
What it costs
Every one of these is paid by something.
- Three successive representations buy a clean separation between program-level and machine-level reasoning, and pay with more lowering steps, more code to maintain and more places for information to be lost.
- A restricted GIMPLE grammar buys tractable analysis passes, and pays with explicit temporaries everywhere and a lowering step to produce them.
- A plugin-based extension model buys the ability to extend a build in place without redistributing a compiler, and pays with an interface tied to internal structures that change between releases.
- Copyleft licensing buys the guarantee that improvements to the compiler remain available, and pays by excluding it from products whose vendors will not accept those terms — which is a real effect on where each project gets adopted.
- Supporting many frontends in one tree buys shared middle-end work across languages as different as Fortran and Ada, and pays with a language-hook interface the middle-end must maintain for all of them.
What else you could do
What a different compiler or language does instead, and when that is better.
- LLVM offers the same three-phase shape with one middle IR and a library-based consumption model — see
[[llvm]]and[[llvm-architecture]]. - MSVC is the third mature C and C++ toolchain, with its own IR, object format and mangling scheme, and is the default on Windows.
- Vendor toolchains — Intel's, ARM's, NVIDIA's — often outperform both on their own hardware and are usually LLVM-derived today.
- For a new architecture, an LLVM backend is the more common choice now, though GCC still supports targets that have no LLVM backend at all.
- For non-compiler code generation, a library-based infrastructure is the natural fit and a plugin model is not — see
[[jit-compilation]].
See it for yourself
The flag, dump or tool that shows you this directly.
gcc -fdump-tree-allwrites every GIMPLE pass's output to files;-fdump-tree-gimpleand-fdump-tree-optimizedare the two most useful individually.gcc -fdump-rtl-alldoes the same for the RTL passes, which is where you look for an instruction-selection or register-allocation question.gcc -fdump-passeslists the pass pipeline for the current options, which is how you find the name of the pass to dump.gcc -Q --help=optimizersshows exactly which optimizations are enabled at the current level — the reliable answer to "does-O2include this".gcc -fopt-info-vec-missedand its siblings report why the vectorizer declined, in prose, per loop.- Compiler Explorer runs both toolchains side by side with a version picker, which is the fastest way to check a comparison instead of repeating one.
Plausible wrong readings
Stated the way a confident engineer states them.
- "GCC is the old one and LLVM is the modern one." Both are actively developed, both have been substantially rearchitected, and GCC's current middle-end is younger than several LLVM subprojects.
- "GIMPLE is GCC's version of LLVM IR." It occupies a similar position but is a different design, and it sits between two other representations that have no direct LLVM equivalents.
- "One of them produces faster code." Which one wins depends on the program, the version, the target and the flags, and it changes in both directions between releases.
- "The licence difference does not matter technically." It shapes who adopts each project and therefore where investment goes, which is about as technical a consequence as a licence can have.
- "If my code compiles with one, it is correct." It means one implementation accepted it. Building with both is the cheap way to find out whether you were relying on something unspecified.
Misconceptions
The claim, and what is actually true.
Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
GCC is the other big compiler toolchain, and it works differently inside: your code becomes three progressively lower-level forms rather than one. It supports more languages in one tree, is extended by plugins rather than by linking libraries, and is under a different licence. On speed of generated code the two are close, and which is ahead depends on the program.
practical
Learn the dump flags — -fdump-tree-all and -fdump-rtl-all are the equivalent of dumping IR after each pass — and learn -Q --help=optimizers, which answers "is this optimization on at -O2" definitively instead of from memory. -fopt-info-vec-missed is the best vectorization-diagnosis tool in either toolchain. And build your project with both compilers in CI: it costs one job and finds warnings, extensions and undefined-behaviour bugs that a single toolchain hides.
advanced
The architectural comparison worth drawing is about where a pass can live. With one broad IR, a pass author has a single, well-documented target and considerable freedom, but every pass sees a representation that must simultaneously be high-level enough for loop transformations and low-level enough to lower cleanly to machine code — a genuine tension visible in LLVM's history of adding higher-level constructs and then constraining them. With three staged representations, each level can be exactly as expressive as its passes need, at the price of two extra lowerings and the information each discards. Neither answer is settled: LLVM added MLIR precisely to get higher-level representations back, and GCC has moved passes between GIMPLE and RTL as understanding of where facts survive improved. The underlying question — how many representations, at what levels — is the one [[ir-levels]] and [[phase-ordering]] are about, and both projects are still answering it.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
If you were asked this in an interview
- Describe GCC's internal representations and why there is more than one.
- What is the practical difference between extending a compiler with a plugin and building on it as a library?
- A program works when built by one toolchain and not the other. What are the most likely explanations?
Connections
- DevOps / Production Engineering — Running two toolchains in CI and keeping both greenBuilding with both compilers is a conformance test, and turning it into a routine practice — matrix builds, warning policies, which failures block a merge — is a build-engineering decision rather than a compiler one.