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