Question
likely and unlikely Macros in C: How Branch Prediction Hints Work
Question
In Linux kernel code, it is common to see conditions written like this:
if (unlikely(fd < 0))
{
/* Do something */
}
or:
if (likely(!err))
{
/* Do something */
}
These macros are typically defined as:
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
I understand that they are used for optimization, but how do they actually work? What benefit do they provide, and how much improvement in performance or code size can usually be expected? Also, are they worth using in user-space programs, especially in performance-critical code, even though they reduce portability?
Short Answer
By the end of this page, you will understand what likely() and unlikely() mean in C, how they use __builtin_expect() to give the compiler branch prediction hints, and why that can affect generated machine code. You will also learn when these hints are useful, when they are unnecessary, and how real projects use them carefully rather than everywhere.
Concept
likely() and unlikely() are macros used to tell the compiler which outcome of a condition is expected to happen most of the time.
In GCC- and Clang-style C code, they usually wrap __builtin_expect():
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
Here is the key idea:
likely(x)means: "this expression is usually true"unlikely(x)means: "this expression is usually false"
What __builtin_expect() does
__builtin_expect(actual_value, expected_value) returns actual_value, but also gives the compiler a hint about which value is more probable.
So this:
if (unlikely(fd < 0))
becomes a hint saying:
- the expression
fd < 0is usually false - the error path is rare
Likewise:
if (likely(!err))
says:
!erris usually true- the success path is common
Why this matters
Modern CPUs run instructions in pipelines and try to predict branches before the condition is fully known. If the CPU predicts correctly, execution continues smoothly. If it predicts incorrectly, the pipeline may need to be flushed, which costs time.
Compilers can also use branch-likelihood information to:
- arrange machine code so the common path is the straight-line path
- move rare error handling code out of the hot path
- improve instruction-cache usage
- sometimes reduce extra jumps
So the benefit is often not that the condition itself becomes faster, but that the generated machine code is laid out better for the common case.
Why the !! is used
The macros use !!(x) to convert any nonzero value to exactly 1, and zero to 0.
Example:
int n = 42;
!!n // becomes 1
This makes the expression fit naturally with the expected values 1 and 0 passed to __builtin_expect().
Important limitation
These are only hints, not commands.
The compiler may use the information, ignore it, or combine it with other optimization data. Also, modern CPUs already have sophisticated hardware branch predictors, so manual hints often help less than people expect.
In practice, likely() and unlikely() are most useful when:
- one path is truly rare, such as error handling
- the branch is in hot code that runs very often
- profiling shows branch behavior clearly
- code layout matters for performance
They are much less useful when:
- the branch is not performance-critical
- both outcomes happen often
- you are guessing instead of measuring
- readability suffers
Mental Model
Think of a branch like a hallway with two doors:
- one door is used 99% of the time
- the other door is only for emergencies
likely() and unlikely() are like putting a sign on the hallway for the architect:
- "Most people go through this door"
- "The emergency door is rarely used"
The sign does not force anyone to choose a door. Instead, it helps the architect design the building so the normal route is smooth and direct, while the rare route can be off to the side.
That is what the compiler does with these macros: it tries to make the common path cheaper and keep uncommon paths out of the way.
Syntax and Examples
The common Linux-style definitions are:
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
Basic syntax
if (likely(count > 0)) {
process_items();
} else {
handle_empty_case();
}
This tells the compiler that count > 0 is usually true.
Example: rare error path
#include <stdio.h>
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
int divide(int a, int b) {
if (unlikely(b == 0)) {
return 0; // error case
}
return a / b;
}
int main(void) {
printf("%d\n", divide(10, 2));
return 0;
}
What this means
b == 0is treated as a rare case- the compiler may place the division path as the main straight-line path
- the error handling path may be arranged as a side branch
Equivalent logical behavior
These macros do not change program logic.
This:
if (unlikely(fd < 0)) {
handle_error();
}
behaves the same as:
if (fd < 0) {
handle_error();
}
The difference is only in optimization hints, not correctness.
Portable fallback pattern
In user-space code, developers sometimes define a fallback:
#if defined(__GNUC__) || defined(__clang__)
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
#else
# define likely(x) (x)
# define unlikely(x) (x)
#endif
This preserves portability by making the macros harmless on compilers that do not support the builtin.
Step by Step Execution
Consider this example:
#include <stdio.h>
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
int read_value(int ok) {
if (unlikely(!ok)) {
return -1;
}
return 42;
}
Step-by-step for read_value(1)
okis1!okbecomes0!!(!ok)is still0__builtin_expect(0, 0)returns0- The
ifcondition is false - The function returns
42
Step-by-step for read_value(0)
okis0!okbecomes1!!(!ok)is1__builtin_expect(1, 0)returns1- The
ifcondition is true - The function returns
-1
Important observation
__builtin_expect() returns the actual expression value.
So:
- it does not force the branch result
- it does not alter correctness
- it only attaches probability information for optimization
Why the compiler cares
If the compiler believes !ok is rare, it may:
- keep the
return 42;path in the main instruction flow - put the
return -1;path somewhere less central - generate fewer jumps for the common case
Real World Use Cases
likely() and unlikely() are most useful in places where one branch is strongly dominant.
Common real scenarios
Error handling in hot code
if (unlikely(ptr == NULL)) {
return ERROR;
}
In many systems, failure is rare, so the normal path should stay fast.
Fast-path vs slow-path logic
if (likely(cache_hit)) {
return cached_value;
}
return recompute_value();
If cache hits are common, it makes sense to optimize that path.
Kernel and systems programming
Operating systems, device drivers, schedulers, and memory allocators often have:
- very hot code paths
- tight loops
- rare exceptional conditions
This is where branch hints are more likely to matter.
Network packet processing
if (unlikely(packet == NULL)) {
drop_packet();
}
When millions of packets are processed, even small layout improvements can matter.
Input validation with rare failure
if (unlikely(length > MAX_SIZE)) {
return ERR_TOO_LARGE;
}
Validation still happens, but the normal valid case is expected most of the time.
Real Codebase Usage
In real projects, developers usually do not wrap every if with likely() or unlikely().
They apply these hints selectively.
Common patterns
Guard clauses for rare failures
int process(struct item *item) {
if (unlikely(item == NULL)) {
return -1;
}
if (unlikely(item->data == NULL)) {
return -2;
}
return do_work(item);
}
This keeps the common valid path clean.
Fast path / slow path separation
if (likely(state == READY)) {
run_fast_path();
} else {
run_slow_path();
}
Validation in performance-sensitive functions
if (unlikely(size > limit)) {
log_error();
return 0;
}
Error handling macros and helpers
Large C codebases often combine branch hints with logging or return macros to keep hot code readable.
Best practice in real codebases
- use them only in measured hot paths
- prefer them for rare error checks or very common success checks
- avoid guessing branch probabilities
- rely on profiling when possible
- let readability win unless performance evidence says otherwise
Profile-guided optimization is often better
In production builds, compilers can often do better with real profile data than with manual hints. Manual hints are most valuable when you know the runtime behavior well and the code is extremely performance-sensitive.
Common Mistakes
1. Thinking they change logic
These macros do not change what the program does.
Broken assumption:
if (unlikely(x > 0)) {
// some developers think this is treated differently logically
}
Reality:
- the condition is still evaluated normally
- true is still true, false is still false
- only optimization metadata changes
2. Using them everywhere
Overusing branch hints can make code noisy and harder to read.
Poor style:
if (likely(a > b)) {
...
}
if (likely(i < n)) {
...
}
if (unlikely(flag)) {
...
}
If every branch is annotated, the hints lose meaning and readability suffers.
3. Guessing the wrong branch probability
If you mark the uncommon path as likely, you can hurt performance.
if (likely(error_occurred)) {
handle_error();
}
If errors are actually rare, this hint is misleading.
4. Ignoring portability
__builtin_expect() is compiler-specific.
Safer pattern:
#if defined(__GNUC__) || defined(__clang__)
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
#else
# define likely(x) (x)
# define unlikely(x) (x)
#endif
5. Expecting large performance wins
These hints usually give:
- tiny improvements
- no measurable difference
- or occasionally a regression
They are not a substitute for algorithmic improvements.
6. Using expressions with side effects carelessly
The macros evaluate x once in the usual definitions, which is good. But you should still keep conditions clear.
if (unlikely(get_value() < 0)) {
...
}
This is valid, but if the expression is complex, readability may suffer. Prefer simple, understandable conditions.
Comparisons
| Concept | What it does | Changes logic? | Typical use |
|---|---|---|---|
Plain if | Normal branch | No | Default choice |
likely(x) | Hints that x is usually true | No | Common fast path |
unlikely(x) | Hints that x is usually false | No | Rare error path |
| Profile-guided optimization (PGO) | Uses real runtime data | No | Whole-program optimization |
likely/unlikely vs plain if
if (x) { ... }
vs
if (likely(x)) { ... }
Both are logically identical. The second version gives the compiler extra information.
likely/unlikely vs profile-guided optimization
Manual hints:
- rely on the programmer's guess or domain knowledge
- are local to a branch
- can become wrong as code changes
PGO:
- uses measured execution data
- often gives better optimization decisions
- requires extra build/test steps
likely() vs unlikely()
- use
likely(x)whenxis usually true - use
unlikely(x)whenxis usually false
Example:
if (unlikely(err)) {
return -1;
}
This is often clearer than trying to invert the condition awkwardly.
Cheat Sheet
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
Quick rules
likely(x):xis usually trueunlikely(x):xis usually false- they do not change correctness
- they are compiler hints, not guarantees
- they are most useful in hot code with strongly biased branches
Why !!(x)?
- converts any nonzero value to
1 - converts zero to
0
Best uses
- rare error handling
- hot fast-path code
- low-level systems code
- branches confirmed by profiling
Avoid when
- code is not performance-sensitive
- branch probabilities are unclear
- readability would get worse
- you expect big gains without measuring
Portable fallback
#if defined(__GNUC__) || defined(__clang__)
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
#else
# define likely(x) (x)
# define unlikely(x) (x)
#endif
Key takeaway
Use these macros sparingly and measure the result.
FAQ
What do likely() and unlikely() do in C?
They wrap __builtin_expect() to tell the compiler which branch is expected to happen most often.
Do likely() and unlikely() make code faster?
Sometimes, but usually only by a small amount. They help most in hot code where one branch is clearly much more common than the other.
Do these macros affect program correctness?
No. They do not change the meaning of the condition. They only provide optimization hints.
Why does the Linux kernel use them so much?
The kernel contains performance-sensitive low-level code where error paths are often rare and fast paths are very important.
Should I use likely() and unlikely() in user-space code?
Only if the code is performance-critical and profiling or domain knowledge shows a strong branch bias. Otherwise, plain if is usually better.
Are likely() and unlikely() portable?
Not fully. They rely on compiler support for __builtin_expect(). Many projects add fallback macros for unsupported compilers.
How much performance improvement should I expect?
Usually little or none unless the code is very hot and the branch behavior is strongly predictable. In some cases, the wrong hint can even hurt performance.
Is profile-guided optimization better than manual branch hints?
Often yes, because it uses real execution data instead of assumptions.
Mini Project
Description
Build a small C program that processes integer inputs and treats invalid values as rare errors. The project demonstrates how unlikely() can be used to mark uncommon validation failures while keeping the normal processing path straightforward.
Goal
Create a program that validates numbers, processes valid input, and uses unlikely() only on the rare error path.
Requirements
Requirement 1 Requirement 2 Requirement 3
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.