Infraimplementation

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.

The question

How does GCC differ from LLVM architecturally, without either of us picking a side?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

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.

What this phase may assume or do

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

implementationGCC's internal structure has changed substantially over its lifetime: GIMPLE and the tree-SSA framework were introduced in GCC 4.0 (2005), replacing an architecture in which almost everything happened on RTL, and passes have migrated between levels repeatedly since. Statements about "how GCC works" from before that transition describe a different compiler, and the pass list differs between versions and target back ends.

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.

GCC, front to backimplementation
  1. Sourceyou write it
    C, C++, Fortran, Ada, Go, D, Objective-C or another supported language.
  2. Frontendbuild time
    Language-specific parsing and checking, then a tree.
    Everything the language guarantees; each frontend is entirely separate above this point.
  3. GENERICbuild time
    A 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.
  4. GIMPLEbuild time
    A 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.
  5. RTLbuild time
    Lisp-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.
  6. Assembly and object codebuild time
    Target 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.

Two architectures, compared at the level where they actually differimplementation
DimensionGCCLLVM
Middle representationsGENERIC, then GIMPLE (in SSA), then RTLOne IR spanning most of that range, plus a machine IR
Consumed asA compiler you extend with pluginsLibraries you link into your own program
Frontends in treeC, C++, Fortran, Ada, Go, D, Objective-C, Modula-2C-family via Clang, Fortran via Flang; other languages are separate projects
LicenceGPL, with a runtime library exceptionApache 2.0 with LLVM exceptions; BSD-style before version 9
Target breadthVery wide, including architectures with no LLVM backendWide, and the usual choice for new architectures
Typical strengths citedtypicalSome numeric and Fortran code; breadth of supported targetsTooling 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-all writes every GIMPLE pass's output to files; -fdump-tree-gimple and -fdump-tree-optimized are the two most useful individually.
  • gcc -fdump-rtl-all does the same for the RTL passes, which is where you look for an instruction-selection or register-allocation question.
  • gcc -fdump-passes lists the pass pipeline for the current options, which is how you find the name of the pass to dump.
  • gcc -Q --help=optimizers shows exactly which optimizations are enabled at the current level — the reliable answer to "does -O2 include this".
  • gcc -fopt-info-vec-missed and 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.

GCC has one intermediate representation like LLVM does.
It has three in succession, each at a different level, with transformations placed at whichever level still has the facts they need.
The two toolchains are converging into the same design.
They have borrowed from each other — diagnostics, sanitizers, link-time optimization — while keeping genuinely different internal architectures and different extension models.
Choosing between them is mostly about performance.
Platform defaults, target support, licence terms and extension model decide it far more often, and the performance difference is per-program rather than general.
A program that compiles under both is portable.
It is more likely to be conforming, which is not the same thing. Both may accept the same extension, and both may share an assumption a third implementation does not.

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.

implementationThe GENERIC/GIMPLE/RTL structure describes GCC from version 4.0 onward; before that, almost all optimization happened on RTL and the compiler was architecturally different. Pass lists, dump flag names and which level a given optimization runs at all vary by version and by target back end, so a dump-flag recipe from an old answer may name a pass that no longer exists.
typicalClaims that either toolchain generates better code are workload-, version- and target-specific and reverse in both directions between releases. Treat any published comparison as a measurement of one benchmark suite at one point in time, and measure your own program if the answer matters — the two are close enough that architecture is a poor predictor.
specWhat both must accept and reject is fixed by the C and C++ standards, so conforming code behaves identically. Divergence between them almost always indicates code relying on something unspecified, undefined or implementation-defined — which is precisely why building with both is a useful conformance check rather than a matter of preference.

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

Computer Architectureinstruction-set-architecture
Performancebenchmarking
Domains that do not exist yet
  • DevOps / Production Engineering — Running two toolchains in CI and keeping both green
    Building 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.