GCC's `__builtin_expect`: Telling the Compiler What You Already Know
A practical guide to using GCC's `__builtin_expect` to move cold error paths out of hot loops, with verification steps and honest trade-offs.
04 Oct 2025, 20:48 UTC

You are profiling a tight loop that processes millions of records. A bounds check, a null-pointer guard, or an allocation failure almost never fires, yet the compiler has laid out the error-handling code right next to your hot path. The result is worse instruction-cache locality and a branch the CPU keeps predicting incorrectly because the layout gives it no hint about which way the code usually flows. You already know the failure is rare. The compiler does not. That is the gap __builtin_expect is designed to close.
The takeaway is simple: when profiling shows a heavily skewed branch in a hot path, you can encode that knowledge with a GCC builtin, verify the effect in the generated assembly, and remove the hint if profile-guided optimization already infers the same probabilities.
What the builtin actually does
__builtin_expect is a compiler intrinsic—a function the compiler understands natively rather than linking from a library. It takes two arguments: a branch condition and the integer result you expect that condition to evaluate to most of the time. It does not change program semantics. Your code still executes the same branches; only the machine code layout and scheduling change.
GCC uses the hint to make several low-level decisions. It can place the expected, or hot, path inline in the natural fall-through of the function, and move the unexpected, or cold, path to a separate region such as .text.unlikely. That keeps the hot instructions contiguous, reducing instruction-cache pressure. The hint also feeds into register allocation and basic-block ordering, though those effects are harder to observe directly.
The likely / unlikely idiom
The raw syntax is noisy, so most projects wrap it in two macros:
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
The double negation !! normalizes any scalar expression to a strict boolean 0 or 1, which matches what the builtin expects. Put these in a shared internal header so the intent stays consistent across translation units. Both the C and C++ front ends support the builtin, and it has been available in GCC for many major releases, so it is a stable tool for performance-sensitive code.
A worked example: moving OOM handling out of the hot path
Consider a frame-processing function that allocates a scratch buffer on every call. In a healthy process, malloc succeeds; failure is a cold, terminal path.
#include <stdlib.h>
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
#define SCRATCH_SIZE (64 * 1024)
void process_frame(const void *input, size_t len)
{
unsigned char *scratch = malloc(SCRATCH_SIZE);
if (unlikely(scratch == NULL)) {
/* Cold path: log, clean up, and bail out. */
return;
}
/* Hot path: decode, transform, and encode in place. */
/* ... */
free(scratch);
}
Without the hint, GCC may interleave the out-of-memory cleanup with the decode logic, or at least keep the blocks close enough that the hot path is not contiguous. With unlikely, the compiler has permission to treat the malloc failure as rare and relocate that block away from the fall-through path. The decision is a hint, not a command: GCC can still override it based on target-specific cost models.
Verifying the hint instead of guessing
Because compiler behavior varies across GCC versions and targets, treat the hint as an experiment. Three checks give you evidence.
Assembly inspection
Run the compiler in your source directory; no elevated permissions are required:
gcc -O2 -S -o frame.s frame.c
Open frame.s and look at the layout of process_frame. Check whether the malloc-failure branch jumps to a block placed after the function epilogue or in a separate section, while the hot decode path remains inline. Section names and exact layout differ by target, so compare the assembly with and without the hint rather than looking for a fixed string.
Branch-miss profiling
On a representative workload, compare branch-miss rates before and after adding the hint. Run perf stat as the user that owns the workload:
perf stat -e branches,branch-misses ./your_workload
On some systems you may need to adjust /proc/sys/kernel/perf_event_paranoid to allow unprivileged access; check your distribution's documentation before changing it. Look for a reduction in branch-misses relative to total branches. If the number does not improve, the hint is not helping on that target.
Letting PGO decide
Modern GCC also uses static heuristics and profile-guided optimization (PGO) to infer branch probabilities automatically. Build an instrumented binary, run it on a realistic workload—this run is slower than production and writes profile data such as .gcda files at exit—then rebuild with the profile:
gcc -O2 -fprofile-generate -o frame_pgo frame.c
./frame_pgo
gcc -O2 -fprofile-use -o frame_optimized frame.c
If the PGO build already places the cold path out of line, your manual hint is redundant. PGO usually wins when you can run a representative workload, but it requires a training phase and adds build complexity.
Trade-offs: when the hint hurts
The builtin is a hint, not a guarantee. Marking a frequently taken branch as unlikely can actively degrade performance by increasing branch mispredictions and polluting the instruction cache. Overuse on cold or unpredictable branches adds noise without benefit and makes the code harder to maintain. Because the optimization is a hint, incorrect hints do not introduce bugs, but they can quietly cost you cycles.
Microbenchmark results also may not generalize. A hint that helps on one GCC version or CPU architecture can be neutral or counterproductive on another. Verify on the toolchain and hardware you actually ship.
An actionable checklist
- Profile first. Apply
likely/unlikelyonly to branches that are measurably skewed—typically well above 90% in one direction—and sit in a hot loop or error-handling path. - Inspect the assembly with
gcc -O2 -Sto confirm the cold path moved. - Measure branch misses with
perf staton a representative workload. - Compare against a
-fprofile-usebuild. If PGO already captures the same layout, drop the manual hint to reduce clutter.
Branch hints are not a substitute for measurement, but when profiling already tells you the answer, __builtin_expect is a low-risk way to make sure the compiler hears it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.