GCC Toolchain Internals

What the gcc driver actually runs, what each optimization and debug level enables, and the flags that silently change correctness or portability — march=native, -Ofast, -std defaults, and the libstdc++ versus libc++ split.

Overview

gcc is a driver, not a compiler

Most engineers use gcc for years without knowing it is a driver. It reads input suffixes, consults a table of spec strings and launches subprocesses. Almost every "why is this flag not doing what I expect" question is answered in thirty seconds by -###, because the driver injects arguments you never typed.

  • gcc -v — Prints and runs each subprocess command
  • gcc -### — Prints them without running anything

Optimization levels

  • -O2 — Nearly all supported optimizations with no space-speed trade-off — The correct production default
  • -O3 — Adds code-growing passes: loop interchange, unroll-and-jam, peeling, IPA constant propagation with cloning — A measurement, not an upgrade; icache pressure can go either way
  • -Os — Most of -O2 minus the code-growing alignment passes — Size-sensitive builds
  • -Og — -O1 minus passes that interfere with debugging — The underused right choice for debug builds, instead of -O0

-Ofast changes correctness

GCC documents -Ofast as enabling optimizations not valid for all standard-compliant programs. It turns on -ffast-math and -fallow-store-data-races.

  • Changes NaN and infinity handling and the sign of zero — Edge-case checks silently break
  • Allows reassociation — Results change
  • On Linux, links a startup object setting flush-to-zero and denormals-are-zero — Changes the arithmetic of every library in the process

> [!WARNING] Numerical code that "works" under -Ofast has usually just not been tested near the edges yet.

Target selection: -march vs -mtune

  • -march — Which instructions may be emitted, so where the binary can run at all — Too new means SIGILL on older hosts
  • -mtune — Scheduling and cost modeling only; never emits outside the base ISA — None for portability
  • -march=native — Everything the build machine supports — Perfect for a local benchmark, catastrophic for an artifact

GCC vs Clang differences that break builds

  • Default -std varies by compiler version — gnu++17 under one, gnu++14 under another
  • Clang enforces two-phase name lookup — Dependent names need this-> or typename where GCC historically tolerated them
  • Different default warning sets — -Werror clean on one fails on the other
  • libstdc++ vs libc++ — Distinct ABIs: no std::string across a boundary where each side used a different one
  • _GLIBCXX_USE_CXX11_ABI inside libstdc++ — Changes std::string layout; shows up as __cxx11 in mangled names

> [!TIP] Interviewers probe whether you reason about flags or copy them: which flags change semantics rather than speed, what you set for a shipped artifact vs a local benchmark, and how you find out what the toolchain is actually doing.

Key concepts

The driver and its subprocesses
gcc and g++ are drivers, not compilers. They infer the language from file suffixes and run cc1 or cc1plus for compilation proper, as for assembly, and collect2 wrapping ld for the link. -v prints and executes each subprocess command; -### prints them quoted without executing; -save-temps keeps the .i and .s intermediates.
Spec strings
The mapping from your command-line flags to the arguments passed to each subprocess is table-driven data, not hardcoded logic. gcc -dumpspecs prints the built-in table; -specs=file overrides it. This is why the same flag can behave differently on two distributions that patch their specs, and it is the mechanism embedded toolchains use to inject board-specific startup files and linker scripts.
-O2 versus -O3
-O2 performs nearly all supported optimizations that do not involve a space-speed tradeoff. -O3 adds passes that trade size for speed — -fgcse-after-reload, -fipa-cp-clone, -floop-interchange, -floop-unroll-and-jam, -fpeel-loops among others. -O3 is not strictly better; the extra code size can cost more in instruction cache misses than the transforms gain.
-Og and -Os and -Oz
-Og is -O1 minus the passes known to greatly interfere with debugging, and it is the intended flag for a debug build. -Os is -O2 minus the code-growing alignment passes (-falign-functions, -falign-jumps, -falign-labels, -falign-loops). -Oz optimizes for size more aggressively than -Os.
-Ofast and -ffast-math
-Ofast is -O3 plus -ffast-math, -fallow-store-data-races, -fno-protect-parens, with -fsemantic-interposition turned off; GCC documents it as disregarding strict standards compliance. -ffast-math permits reassociation, assumes no NaN or infinity, ignores signed zero, and on Linux links a startup object that sets flush-to-zero for the whole process, affecting code you did not compile.
-g levels and DWARF
-g1 gives enough for backtraces (functions and line numbers, no locals); -g2 (plain -g) adds locals and typedefs; -g3 adds macro definitions so the debugger can expand macros. DWARF 5 is the default on most targets, selectable with -gdwarf-N. -gsplit-dwarf writes .dwo files the linker does not have to merge. -grecord-gcc-switches embeds the compile flags in DW_AT_producer.
-march versus -mtune
-march=cpu selects the instruction set and may generate code that will not run at all on other processors. -mtune=cpu only affects scheduling and cost heuristics and never emits instructions outside the default machine type. -march implies -mtune. -march=native picks the build machine's CPU and enables every subset it supports, which makes the result non-portable by construction.
libstdc++ versus libc++ and the CXX11 ABI
libstdc++ (GNU) and libc++ (LLVM) are separate implementations with incompatible ABIs; standard library types cannot cross a boundary where the two sides used different ones. Within libstdc++, _GLIBCXX_USE_CXX11_ABI selects between the old copy-on-write std::string and the C++11-conforming one, changing the layout and the mangled names — the tell is __cxx11 appearing in an undefined reference.

When to use it

  • Investigating why a build behaves differently on two machines or two distributions with nominally identical flags
  • Choosing the optimization and debug flag set for a shipped release artifact that must also be debuggable from a production core dump
  • Deciding how to get vectorization benefit without producing a binary that only runs on the build machine
  • Introducing -Wall -Wextra -Werror to a codebase that has never had warning discipline, without stopping all other work
  • Setting up or debugging a cross-compilation or sysroot-based build where the driver is picking up the wrong headers or libraries
  • Reconciling a codebase that must build cleanly under both GCC and Clang, or that mixes libstdc++ and libc++ consumers

Interview questions

  • Explain what actually happens when you type g++ -O2 -g main.cpp -o app, and how you would inspect it.
  • What flags would you set for a shipped release build, and which flags would you refuse to set?
  • You want AVX-512 performance but the fleet is heterogeneous. How do you get it without shipping a binary that crashes?
  • A team wants to turn on -Wall -Wextra -Werror on a ten-year-old codebase. How do you do it without stopping all other work?
  • What GCC versus Clang differences have actually bitten you, and how do you keep a codebase building on both?

Sign up free to read the full topic

More DevOps topics

Start free on Cariara · Coding problems · System design studio