Skip to content
Home
GCC Architecture: Frontends, GENERIC/GIMPLE IR, and Backends

GCC Architecture: Frontends, GENERIC/GIMPLE IR, and Backends

Compilers Compilers 7 min read 1468 words Beginner ExcellentWiki Editorial Team

The GNU Compiler Collection (GCC) is one of the most widely used compiler suites in the world. It supports over 50 frontends (including C, C++, Fortran, Ada, Go, and D) and targets dozens of architectures (x86, ARM, RISC-V, MIPS, PowerPC, and many more). GCC’s architecture follows the classic three-phase design — frontend, middle-end, and backend — but its internal representations and implementation details are distinct from LLVM. Understanding GCC’s architecture is valuable for anyone contributing to open-source compilers, porting GCC to new hardware, or working on Linux systems where GCC remains the primary compiler.

The Three Phases of GCC

Frontend

Each GCC frontend (C, C++, Fortran, etc.) parses the source language and generates GENERIC, GCC’s high-level intermediate representation. The frontend is responsible for:

  • Lexing and parsing — converting source text to an AST. The C and C++ frontends use hand-written recursive descent parsers
  • Semantic analysistype checking and name resolution
  • GENERIC generation — lowering the language-specific AST to the language-independent GENERIC representation

GENERIC is a tree representation that closely resembles the source structure but abstracts away language-specific details. All frontends lower to GENERIC, which then passes to the middle-end.

The C++ frontend is particularly complex. It handles templates, name mangling, exception handling, and the C++ standard library. GCC’s C++ frontend has decades of evolution and supports C++20 and most of C++23 features. G++ (the GCC C++ compiler) is the default compiler on most Linux distributions.

Middle-End: GENERIC to GIMPLE

The middle-end converts GENERIC to GIMPLE, GCC’s core IR. GIMPLE is a three-address, SSA-based representation that is lower-level than GENERIC and closer to what the machine actually executes. The conversion happens in several steps:

  1. Gimplification — GENERIC trees are lowered to GIMPLE statements. Complex expressions are broken into simple three-address forms. The gimplify.c file in GCC’s source tree handles this transformation.

  2. SSA conversion — the ssaexpand pass converts GIMPLE to SSA form, inserting φ functions at dominance frontiers. GCC’s SSA implementation follows the algorithm from Cytron et al. (1991).

  3. Optimization passes — GCC runs over 100 optimization passes on the GIMPLE SSA representation. These include scalar replacement of aggregates (SRA), constant propagation, dead code elimination, loop optimizations, vectorization, and inlining.

GCC’s pass manager orchestrates these passes. Each pass is a struct with execute and gate functions — gate determines whether the pass should run, execute performs the transformation. The pass ordering is defined in passes.def. Developers can add new passes by registering them in this file.

Key GIMPLE optimization passes:

  • Tree SSA uncprop — removes copy propagation artifacts
  • PRE (Partial Redundancy Elimination) — eliminates partially redundant expressions
  • SCCP (Sparse Conditional Constant Propagation) — Wegman-Zadeck style constant propagation
  • Loop header copying — optimizes loop entry for vectorization
  • Auto-vectorization — both loop vectorizer and SLP vectorizer

You can inspect GIMPLE output with GIMPLE dump flags: -fdump-tree-gimple, -fdump-tree-all, and -fdump-tree-optimized.

Backend: RTL (Register Transfer Language)

After GIMPLE optimization, the middle-end expands GIMPLE into RTL (Register Transfer Language). RTL is GCC’s low-level IR, representing instructions, registers, and memory operations in a Lisp-like S-expression format.

RTL operations include:

  • (set (reg:SI 100) (plus:SI (reg:SI 101) (reg:SI 102))) — an addition operation
  • (insn ...) — an instruction with side effects
  • (call ...) — a function call

The backend phases include:

  1. Instruction selection — expanding RTL patterns into target-specific instructions
  2. Register allocation — using the local allocator (reload) and global allocator (IRA - Integrated Register Allocator)
  3. Instruction scheduling — reordering for pipeline efficiency
  4. Peephole optimization — pattern matching on generated RTL
  5. Final pass — emitting assembly text from RTL

GCC’s reload-based register allocator historically struggled with irregular register files, but the newer LRA (Local Register Allocator) implementation improves handling of complex architectures. LRA replaced the old reload pass starting in GCC 4.3.

Machine Descriptions (MD Files)

GCC targets a new architecture by writing a machine description — a file with .md extension containing patterns that define the target’s instructions, registers, and calling conventions.

An MD file defines instruction patterns using RTL templates:

(define_insn "addsi3"
  [(set (match_operand:SI 0 "register_operand" "=r")
        (plus:SI (match_operand:SI 1 "register_operand" "%r")
                 (match_operand:SI 2 "register_operand" "r")))]
  ""
  "add\t%0, %1, %2")

This pattern defines a 3-operand addition instruction. The constraints ("=r", "%r", "r") describe which register classes are allowed. The final string is the assembly template.

Writing MD files requires deep knowledge of both the target ISA and GCC’s RTL conventions. The GCC Internals manual devotes hundreds of pages to .md files, covering constraints, predicates, attributes, and peephole definitions.

GCC Optimization Pipeline

GCC’s optimization pipeline is organized into three major groups of passes that run in a fixed order determined by the -O level.

Early Optimization Passes

These run on GIMPLE before SSA conversion and focus on lowering constructs and canonicalizing the IR:

  • Fold-const — constant folding and expression simplification
  • Lowering passes — converting complex operations (e.g., switch to cascading branches) into simpler GIMPLE
  • Builtin expansion — expanding compiler builtins like memcpy and alloca
  • CFG cleanup — removing unreachable blocks, merging basic blocks

SSA Optimization Passes

After SSA conversion, GCC runs a sequence of passes that includes:

  • Tree SSA CCP (Conditional Constant Propagation) — Wegman-Zadeck sparse conditional constant propagation on SSA form
  • Tree SSA DCE — dead code elimination on SSA
  • Tree SSA PRE — partial redundancy elimination
  • Tree SSA DOM — dominator-optimization-based value numbering and jump threading
  • Tree SSA VRP (Value Range Propagation) — tracking value ranges to eliminate bounds checks and enable simplifications
  • Tree loop optimizations — loop invariant motion, loop unrolling, loop vectorization

Late and RTL Passes

After GIMPLE optimization, GCC expands to RTL and runs:

  • RTL CSE — common subexpression elimination on RTL
  • RTL loop optimizations — further loop transformations at the RTL level
  • GCSE (Global Common Subexpression Elimination) — eliminates redundant computations across basic blocks
  • Scheduling passes — instruction scheduling before and after register allocation
  • Register allocation (IRA + LRA) — the integrated register allocator

GCC Porting Process

Porting GCC to a new target architecture is a substantial but well-defined task. The key components:

  1. Machine description (.md file) — describes each instruction’s RTL pattern, assembly template, and constraints. This is the bulk of the port
  2. Header filesgcc/config/<arch>/<arch>.h defines target-specific macros: register classes, calling conventions, data type sizes, alignment requirements
  3. Target hooks — modern GCC uses a TARGET_* hook interface in gcc/targhooks.c. Ports override hooks for target-specific behavior like function prologue/epilogue generation
  4. Multilib configuration — support for multiple ABI variants (e.g., hard-float vs. soft-float ARM)

The GCC Internals manual (“Using and Porting the GNU Compiler Collection”) is the definitive reference for porting. The RISC-V port, contributed by SiFive in GCC 7, is a well-documented example of a modern GCC backend.

GCC Plugins

GCC supports plugins — dynamically loaded modules that can inspect and modify the compilation process. Plugins were added in GCC 4.5 and provide a stable API for:

  • Adding new passes — register passes in the pass manager
  • Modifying IR — transform GIMPLE or RTL during compilation
  • Inspecting diagnostics — intercept error and warning messages
  • Adding target attributes — extend the __attribute__ system

A simple GCC plugin might add a pass that counts function calls:

int plugin_init(struct plugin_name_args *plugin_info,
                struct plugin_gcc_version *version) {
    register_callback("my_plugin", PLUGIN_PASS_MANAGER_SETUP,
                      my_pass_setup, NULL);
    return 0;
---

GCC plugins are useful for research (experimenting with new optimizations), static analysis (checking coding standards), and instrumentation (adding profiling hooks).

Debugging GCC

GCC provides extensive debugging support:

  • Dump flags-fdump-tree-* for GIMPLE, -fdump-rtl-* for RTL, -fdump-ipa-* for interprocedural analysis
  • Graph dumps-fdump-tree-*-graph generates .dot files for visualization
  • Verbose assembly-fverbose-asm annotates assembly with source information
  • Debug build — configuring GCC with --enable-checking=yes enables runtime assertions

FAQ

What is the difference between GENERIC and GIMPLE? GENERIC is the high-level IR emitted by frontends; it preserves most source-level structure. GIMPLE is a lowered, three-address IR in SSA form, appropriate for optimization. The gimplifier converts GENERIC to GIMPLE.

How does GCC differ from LLVM architecturally? GCC uses GIMPLE + RTL as its two-level IR, while LLVM uses a single SSA-based IR throughout. GCC’s pipeline is more monolithic; LLVM’s pass infrastructure is more modular. GCC plugins require C with GCC-specific APIs; LLVM supports loadable passes written in C++.

Can I write a GCC frontend for a new language? Yes. The frontend must parse the language and emit GENERIC trees. The GCC Internals manual documents the GENERIC API. Several out-of-tree frontends exist (GNAT Ada is in-tree; others like GCC for Java — GCJ — were historically maintained).

How do I add a new optimization pass to GCC? Write a pass struct implementing the opt_pass interface, register it in the pass manager by editing passes.def, and set the appropriate gate conditions. Rebuild GCC and test with the relevant dump flags.

Internal Links

Section: Compilers 1468 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top