GCC `__builtin_expect`: Moving Error Handling Off the Hot Path
Learn how to use GCC's `__builtin_expect` to keep rare error-handling code out of the hot path, when it beats profile-guided optimization, and how to verify the generated assembly.
14 Oct 2026, 01:05 UTC

The problem: defensive code in the middle of your hot path
Most C and C++ functions carry defensive checks: null pointers, length limits, unexpected enum values. When a function is called millions of times, those checks execute every time, but the error branch almost never fires. The compiler, however, does not know that. It lays out basic blocks—straight-line sequences of instructions with no internal branches—in source order or according to its own cost model. The result is that cold error-handling code can sit between the hot path's entry and its exit, increasing the function's instruction-cache footprint and pushing the common case across extra cache lines.
__builtin_expect is GCC's answer to this layout problem. It lets you annotate a conditional expression with the expected outcome, giving the optimizer a strong hint about which basic block is hot. The semantics of your program do not change; only the generated code layout does.
How the hint works and why the double negation matters
The builtin takes two arguments: a condition and an expected value (0 or 1). Because the condition is not guaranteed to be a strict boolean, the idiomatic wrapper normalizes it with !!—double logical negation—so that any non-zero value becomes exactly 1 and zero becomes 0. Without this, a pointer or integer expression could produce a value other than 0 or 1, and the comparison against the expected value would behave unexpectedly.
The usual macros look like this:
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
These macros work identically in the C and C++ front ends. They require no special language standard and no compiler flag beyond normal optimization; the optimizer's block-reordering passes simply consume the hint when they run.
A worked example: validating a record before parsing
Consider a function that validates a fixed-size record header before parsing it. The validation fails only when a caller passes corrupt data, which is rare in a healthy system.
#define MIN_RECORD_SIZE 16
int process_record(const unsigned char *data, size_t len) {
if (unlikely(data == NULL || len < MIN_RECORD_SIZE)) {
return -1;
}
/* Hot path: parse fields and update state. */
unsigned int id = (data[0] << 24) | (data[1] << 16) |
(data[2] << 8) | data[3];
/* ... more hot-path work ... */
return 0;
}
To see what GCC does with the hint, compile to assembly. In a terminal with normal user permissions, run:
gcc -O2 -S -o process_record.s process_record.c
Open process_record.s and locate the process_record label. The key check is whether the fall-through path—the instructions immediately following the conditional branch—contains the hot parsing code, while the return -1 error block is moved after the function epilogue or into a separate cold section. GCC does not guarantee the reordering; it treats the hint as a strong suggestion, and its own analysis can override it, especially at higher optimization levels. If the layout looks wrong, inspect the installed compiler's documentation for your specific version, because code-layout heuristics evolve between releases.
For a runtime check, load the binary into gdb from the same terminal and run disassemble process_record. You are looking for the same structural signal: the cold block should not be interleaved with the hot block.
Trade-offs: static hints versus real profiles
The main alternative is profile-guided optimization (PGO). With PGO, you compile with -fprofile-generate, run a representative workload, and then recompile with -fprofile-use. The optimizer then makes layout decisions based on measured branch frequencies rather than your annotations. For an application where you can capture a realistic workload, PGO usually produces better results because it sees the whole call graph.
__builtin_expect remains attractive when a profiling run is impractical—most notably in library code, where the caller's workload is unknown at build time. It also avoids the two-phase build complexity. The cost is that you are guessing. If you mark a branch as unlikely when it is actually taken half the time, you pessimize performance by scattering hot code across cache lines and adding taken branches to the common path.
Other limitations are worth noting. At -O0 the builtin has no effect because the optimizer's block-reordering passes do not run. Overusing the macros also hurts readability; reserve them for clear, stable hot/cold splits such as error handling, defensive assertions, and rare edge cases.
Verification and when to reach for it
Before trusting a hint, verify the generated layout. Compile with -O2 -S and read the assembly, or use gdb to disassemble the final binary. If you are chasing a specific performance change, a microbenchmark compared under perf stat can show whether instruction-cache misses actually moved, though access to hardware counters depends on your kernel's perf_event_paranoid setting and may require adjusted permissions. Microbenchmark results are also noisy and may not reflect production workloads.
Use __builtin_expect when you have a rare, well-understood cold path and no profiling data. Skip it when the branch is genuinely unpredictable, when you already have PGO data, or when the code is not performance-critical. And always check your compiler version's documentation—the builtin has been part of GCC for a long time, but layout heuristics are an implementation detail that can shift between releases.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.