Curriculum / Appendix

42Compiler Flag Reference

The flags that change your latency, your correctness, or your ability to debug, with the GCC and Clang spellings and the three command lines you actually need.

Appendixintermediate10 min
flagsgccclangltopgosanitizers

Flags are the cheapest performance work available and the easiest place to quietly break a production binary. This page is the lookup table: what each flag does, where GCC and Clang disagree, and which tempting-looking options are traps. Everything here assumes x86-64 Linux and -std=c++23; the concepts appear first in lesson 1 and are used throughout lessons 19 to 30.

Optimisation level and target architecture#

Levels#

FlagWhat you getWhen
-O0Nothing. Every variable lives on the stack.Only when stepping in a debugger. Never benchmark it.
-OgOptimise without destroying debug info.The default debug build.
-O1Cheap passes, little inlining.Sanitizer builds, where speed still matters.
-O2The production baseline. Inlining, and since GCC 12 a conservative auto-vectoriser.Default for everything.
-O3More inlining, more unrolling, an aggressive vectoriser.Only after measuring against -O2.
-Os / -OzOptimise for size; -Oz gives up more speed than -Os.Code-size-bound paths, rarely trading.
-Ofast-O3 plus -ffast-math plus other non-conforming behaviour.Never on a trading binary. See the arithmetic section.

-O3 is not automatically faster than -O2. The extra inlining and unrolling grow the instruction footprint, and a hot loop that no longer fits in the L1 instruction cache is slower no matter how good its scheduling is. Lesson 29 covers that trade in detail. The honest procedure is to build both and compare the p99.9, not the p50.

Clang's -O2 and -O3 differ less than GCC's, because Clang's -O2 already unrolls and vectorises. Clang additionally has -O1 as a genuine middle ground and treats -Og as an alias for -O1.

Architecture and tuning#

-march decides which instructions the compiler is allowed to emit. -mtune decides which microarchitecture it schedules for and emits no new instructions. -march=X implies -mtune=X unless you override it.

FlagMeaning
-march=nativeEverything this build machine supports. Dangerous across a fleet.
-march=icelake-serverAn explicit target. This is what you ship.
-march=x86-64-v2SSE4.2, POPCNT. Roughly Nehalem and later.
-march=x86-64-v3AVX2, FMA, BMI1/2, LZCNT. Roughly Haswell and later.
-march=x86-64-v4AVX-512F/BW/CD/DQ/VL.
-mtune=genericSchedule for no machine in particular while allowing -march instructions.
-mavx2 -mfma -mbmi2Enable individual extensions without moving the whole baseline.
-mprefer-vector-width=256Cap the vectoriser at 256 bits on AVX-512 parts. See lesson 25.

The x86-64-v2/v3/v4 levels are the useful compromise for a mixed fleet: they name a capability set rather than a chip, so the binary runs everywhere at or above that level.

Finding out what -march=native actually enabled
sh
$ gcc -march=native -Q --help=target | grep -E 'march=|mtune='
$ gcc -march=native -Q --help=target | grep -v '\[disabled\]'
$ gcc -march=native -dM -E - < /dev/null | grep -E '__AVX|__BMI|__FMA'

$ clang -march=native -### -E - 2>&1 | tr ' ' '\n' | grep -- '-target-feature'

The first two lines are the ones to remember. GCC's -Q --help=target prints every target option with its resolved value, which is the only reliable way to turn native into a name you can put in a build file. Clang has no equivalent, so you read the driver's own expanded command line with -###.

Link-time optimisation restores cross-translation-unit inlining. Profile-guided optimisation tells the compiler which branches and functions are hot so it can lay the code out accordingly. Both matter more for tail latency than any single -O bump.

PurposeGCCClang
LTO-flto=auto-flto=thin (scalable) or -flto=full
Archive supportuse gcc-ar, gcc-nm, gcc-ranlibuse llvm-ar, llvm-nm
Keep normal objects too-ffat-lto-objectsn/a
LTO cachen/a-Wl,--thinlto-cache-dir=...
PGO: instrument-fprofile-generate-fprofile-generate
PGO: mergeautomatic (.gcda)llvm-profdata merge -output=x.profdata *.profraw
PGO: use-fprofile-use -fprofile-correction-fprofile-use=x.profdata
Don't pessimise uncovered code-fprofile-partial-trainingn/a
Sampling PGO from perf-fauto-profile (via create_gcov)-fprofile-sample-use= (via create_llvm_prof)

Two rules. LTO must be passed at link time as well as compile time, with the same optimisation level, or you get either a link error or a quietly worse binary. And a PGO profile must come from a workload that resembles production; a profile gathered from a unit-test run will happily mark your error handling as hot. Sampling PGO exists precisely so you can collect the profile from a real production process with perf instead of running an instrumented binary. BOLT, which reorders the already-linked binary, is covered in lesson 29.

Catching bugs: warnings and sanitizers#

Warnings#

FlagCatches
-Wall -Wextra -WpedanticThe baseline. Not "all" warnings despite the name.
-WshadowA local that hides an outer variable or a member.
-WconversionImplicit narrowing that loses value.
-Wsign-conversionSigned/unsigned conversions. In C++ this is not implied by -Wconversion.
-Wold-style-castC casts, which can silently become const_cast or reinterpret_cast.
-Wnon-virtual-dtorDeleting through a base pointer with a non-virtual destructor.
-Wcast-alignA cast that increases required alignment.
-Wdouble-promotionAn accidental float to double promotion.
-Wnull-dereference -Wformat=2 -Wimplicit-fallthroughCheap, high signal.
-Wuseless-cast -Wduplicated-cond -Wduplicated-branches -Wlogical-opGCC only, all worth it.
-Wthread-safetyClang only; enforces lock annotations. Pairs with lesson 31.
-Wframe-larger-than=N -Wstack-usage=N -Walloca -WvlaUnbounded stack use on the hot path.
-WpaddedVery noisy. Turn it on once per struct redesign, then off. Lesson 21.

-Wsign-conversion deserves its own row because prices are std::int64_t ticks and quantities are std::uint32_t (lesson 2), so every notional calculation is a signed/unsigned boundary. Without that flag, the compiler will convert silently and the bug appears only at a size that overflows.

-Werror policy: on in CI, off in developer builds. A compiler upgrade introduces new warnings, and if -Werror is baked into the default build then that upgrade becomes an outage for everyone at once. Promote individual warnings instead, so the ones that are always bugs are always fatal:

sh
-Werror=return-type -Werror=switch -Werror=uninitialized -Werror=implicit-fallthrough

Sanitizers#

SanitizerFlagCPU costMemory costFinds
Address-fsanitize=address~2x~3xOverflow, use-after-free, use-after-return, leaks
Undefined behaviour-fsanitize=undefined1.2x to 2xsmallSigned overflow, bad shifts, misalignment, bad casts
Thread-fsanitize=thread5x to 15x5x to 10xData races, lock-order inversions
Memory-fsanitize=memory~3x~2xReads of uninitialised memory. Clang only.
Hardware address-fsanitize=hwaddress~1.1x to 2x~15%Same as ASan, AArch64 only

Those are the ballparks the upstream sanitizer documentation quotes; your numbers will differ. What matters is the ordering: UBSan is nearly free, ASan is affordable in CI, TSan is a nightly job.

Address, Thread and Memory each replace the allocator and the shadow-memory scheme, so no two of them can be combined. UBSan composes with any of the three. LeakSanitizer is already inside ASan on Linux; -fsanitize=leak on its own can be combined with TSan. MemorySanitizer only gives useful results if every library in the process, including the standard library, was also built with it, which in practice means building libc++ yourself.

Useful companions: -fno-sanitize-recover=all turns findings into aborts so CI actually fails, -fno-omit-frame-pointer makes the reports readable, and -fsanitize-address-use-after-scope extends ASan to stack objects whose lifetime ended. For a build that must stay fast, -fsanitize-trap=undefined emits a bare ud2 instead of a diagnostic call, which costs almost nothing in size or speed.

Code generation, exceptions and arithmetic#

FlagEffect
-fno-pltCall through the GOT instead of a PLT stub, removing one indirect jump per cross-library call.
-fno-semantic-interpositionLet the compiler inline your own functions inside a shared library. GCC needs this explicitly; Clang assumes it already.
-fvisibility=hidden -fvisibility-inlines-hiddenFewer dynamic symbols: smaller GOT, faster load, more devirtualization. Export deliberately with __attribute__((visibility("default"))).
-falign-functions=32 -falign-loops=32Align to cache-line and decoder boundaries. Removes some build-to-build noise. Lesson 29.
-fno-omit-frame-pointerKeeps the frame-pointer chain so perf record -g gets correct stacks for about 1% of throughput.
-ffunction-sections -fdata-sections -Wl,--gc-sectionsDrop unreferenced code, shrinking the hot text.
-Wl,-z,now -Wl,-z,relroResolve every symbol at load, not on first call.
-ftls-model=initial-execCheap thread_local access instead of a call to __tls_get_addr. Not valid for dlopened libraries.
-fno-stack-protectorRemoves a load and compare per protected frame. Keep the protector everywhere except the measured hot path.

-fno-exceptions and -fno-rtti are policy flags, discussed in lesson 18. -fno-exceptions makes throw a compile error and shrinks .eh_frame; it does not make the happy path faster, because table-driven unwinding already costs nothing when nothing throws. The win is code size and layout. -fno-rtti removes dynamic_cast and typeid on polymorphic types. Both must be set identically for every translation unit and every library in the program, or you have an ODR violation (lesson 1).

The safe subsets are worth knowing. -fno-math-errno lets sqrt compile to a single instruction instead of a call guarded by an errno check, and is safe unless you actually read errno after a maths function. -ffp-contract controls fused multiply-add formation: GCC defaults to fast (contract anywhere), Clang defaults to on (contract within one statement), and off gives bit-reproducible results across compilers. Clang also offers -ffp-model=precise|strict|fast as a single dial. On a trading hot path most of this is moot, because prices are integer ticks and not doubles at all (lesson 40).

Instrumentation for profiling and tracing#

These change what the binary can tell you about itself. Lesson 28a covers how to use the output; this is the flag lookup.

FlagCompilerEffect
-gbothDebug info. No run-time cost. There is no good reason to ship without it.
-fno-omit-frame-pointerbothKeeps a walkable stack. Costs a register and a little speed, and it is what makes production profiling possible.
-fxray-instrumentClangPatchable nop sleds at function entry and exit, inert until turned on at run time.
-fxray-instruction-threshold=NClangOnly instrument functions of at least N instructions. Without it you instrument every accessor.
-finstrument-functionsbothCalls __cyg_profile_func_enter/_exit around every function. Always on and far heavier than XRay; what uftrace builds on.
-finstrument-functions-exclude-file-list=bothNarrows the above, which it badly needs.
-pgbothgprof instrumentation. Largely superseded by sampling profilers.
-fprofile-generate / -fprofile-usebothPGO instrumentation and consumption. Lessons 29 and 29a.
-fprofile-partial-trainingGCCDo not pessimise code the profile never exercised.
-fsanitize-coverage=trace-pc-guardbothCoverage callbacks, the basis of fuzzing instrumentation.
Clang XRay: build once, decide later whether to record
sh
$ clang++ -std=c++23 -O2 -g -fxray-instrument -fxray-instruction-threshold=64       main.cpp -o trader
$ XRAY_OPTIONS="patch_premain=true xray_mode=xray-basic" ./trader
$ llvm-xray account xray-log.trader.* --sort=sum --top=20 --format=text

Asking the compiler what it did#

Never assume a loop vectorised or a function inlined. Ask.

GCC: what got vectorised, what did not get inlined
sh
$ g++ -std=c++23 -O3 -march=native -fopt-info-vec book.cpp
$ g++ -std=c++23 -O3 -fopt-info-vec-missed book.cpp 2>&1 | grep book.cpp
$ g++ -std=c++23 -O2 -fopt-info-inline-missed book.cpp
$ g++ -std=c++23 -O2 -Q --help=optimizers | grep enabled | head
Clang: the same questions
sh
$ clang++ -std=c++23 -O3 -march=native -Rpass=loop-vectorize book.cpp
$ clang++ -std=c++23 -O3 -Rpass-missed=loop-vectorize -Rpass-analysis=loop-vectorize book.cpp
$ clang++ -std=c++23 -O2 -Rpass=inline book.cpp
$ clang++ -std=c++23 -O2 -fsave-optimization-record book.cpp   # book.opt.yaml
Reading the generated code, either compiler
sh
$ g++ -std=c++23 -O2 -S -masm=intel -fverbose-asm -o - book.cpp | c++filt
$ objdump -d --no-show-raw-insn -M intel ./trader | c++filt
$ g++ -### -std=c++23 -O2 book.cpp        # the real driver invocation

-fverbose-asm annotates each instruction with the variable it came from, which turns an unreadable listing into something you can follow next to the source. Compiler Explorer does the same thing with colours and is usually faster to reach for (lesson 1).

Three command lines and the traps#

Production
sh
$ g++ -std=c++23 -O3 -march=icelake-server -mtune=icelake-server \
      -flto=auto -fno-plt -fno-semantic-interposition \
      -fvisibility=hidden -fvisibility-inlines-hidden \
      -falign-functions=32 -fno-omit-frame-pointer \
      -ffunction-sections -fdata-sections \
      -DNDEBUG -g -ggdb3 \
      -Wl,--gc-sections -Wl,-z,now -Wl,-z,relro \
      feed.cpp book.cpp strategy.cpp -o trader
Debug
sh
$ g++ -std=c++23 -Og -g3 -march=native \
      -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-conversion \
      -Wold-style-cast -Wnon-virtual-dtor -Wcast-align -Wdouble-promotion \
      -Wnull-dereference -Wimplicit-fallthrough -Wformat=2 \
      -D_GLIBCXX_ASSERTIONS -fno-omit-frame-pointer \
      feed.cpp book.cpp tests.cpp -o trader_dbg
CI, two passes because ASan and TSan cannot coexist
sh
$ g++ -std=c++23 -O1 -g -fno-omit-frame-pointer -Wall -Wextra -Werror \
      -fsanitize=address,undefined -fno-sanitize-recover=all \
      -fsanitize-address-use-after-scope \
      feed.cpp book.cpp tests.cpp -o trader_asan

$ g++ -std=c++23 -O1 -g -fno-omit-frame-pointer -Wall -Wextra -Werror \
      -fsanitize=thread -fno-sanitize-recover=all \
      feed.cpp book.cpp tests.cpp -o trader_tsan

$ ASAN_OPTIONS=detect_stack_use_after_return=1:strict_string_checks=1 \
  UBSAN_OPTIONS=print_stacktrace=1 ./trader_asan
$ TSAN_OPTIONS=second_deadlock_stack=1 ./trader_tsan

-D_GLIBCXX_ASSERTIONS adds cheap bounds and precondition checks to libstdc++ containers and does not change the ABI. Its heavier sibling -D_GLIBCXX_DEBUG does change container layout, so every translation unit and every library must agree or you get memory corruption that looks like a compiler bug.

Flags that look tempting and are traps:

Takeaways#