__builtin_expect vs [[likely]] in C++: What's the Difference?
Understanding Branch Prediction Hints in C++
In high-performance computing—such as deep learning libraries like PyTorch—every microsecond counts. To squeeze out maximum performance, developers often use branch prediction hints to guide the compiler. You might have seen macros like C10_LIKELY in PyTorch, which wrap GCC's __builtin_expect. With C++20, the committee introduced standard [[likely]] and [[unlikely]] attributes.
But what is the actual difference between these two approaches, and should you migrate your codebase to the modern C++20 standard?
1. Syntax and Placement: Expression vs. Statement
The most immediate difference is how and where these hints are written in your code.
The Legacy/Compiler-Specific Way: __builtin_expect
__builtin_expect is an expression-level tool. It tells the compiler what value an expression is expected to evaluate to.
#define C10_LIKELY(expr) (__builtin_expect(static_cast<bool>(expr), 1))
if (C10_LIKELY(x > 0)) {
// Hot path
}The Modern C++20 Way: [[likely]]
The C++20 attributes are statement-level annotations. Instead of wrapping the condition, you annotate the substatement (the branch itself).
if (x > 0) [[likely]] {
// Hot path
} else {
// Cold path
}2. Standardization and Portability
__builtin_expectis a compiler extension native to GCC and Clang. If you are compiling with MSVC (Microsoft Visual Studio), it is not natively supported without custom macros or workarounds.[[likely]]and[[unlikely]]are part of the ISO C++20 standard. They are fully portable across any compliant C++20 compiler, including GCC, Clang, and MSVC.
3. How the Compiler Processes Them
While they achieve similar goals—reordering assembly instructions to keep the "hot path" instructions contiguous in memory (minimizing instruction cache misses and branch mispredictions)—they do so at different stages of compilation.
- Expression-level: Because
__builtin_expectoperates on the expression itself, it can be passed through complex boolean logic (e.g.,if (__builtin_expect(a, 1) && b)). - Control-flow-level:
[[likely]]acts directly on the control flow branches. This gives the compiler's optimizer a clearer high-level picture of the execution paths, allowing for more sophisticated basic block layout optimizations.
4. Performance Pitfalls and Implementation Differences
Interestingly, newer is not always strictly better in early compiler versions. When C++20 was first introduced, some compilers had suboptimal implementations of [[likely]] that sometimes produced worse assembly than __builtin_expect. This was because [[likely]] heavily weights the annotated branch, sometimes causing the compiler to aggressively unroll loops or make optimization trade-offs that actually degraded performance elsewhere.
Today, modern compilers have largely harmonized these optimizations, but it is always recommended to benchmark your specific hot paths when choosing between them.
Summary: Which One Should You Use?
For modern, greenfield C++20 projects, you should prefer the standard [[likely]] and [[unlikely]] attributes. They are standard, portable, and convey intent clearly without relying on preprocessor macros.
However, if you are maintaining a cross-platform library that must support older C++ standards (like C++14 or C++17), or if you need to hint at expression values inside complex logical conditions, wrapping __builtin_expect in a macro (similar to PyTorch's C10_LIKELY) remains an incredibly robust and effective solution.