Question

likely and unlikely Macros in C: How Branch Prediction Hints Work

cgcclinux-kernelbranch-predictionlikely-unlikely

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 < 0 is usually false
  • the error path is rare

Likewise:

if (likely(!err))

says:

  • !err is 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.

Take Quiz

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 == 0 is 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)

  1. ok is 1
  2. !ok becomes 0
  3. !!(!ok) is still 0
  4. __builtin_expect(0, 0) returns 0
  5. The if condition is false
  6. The function returns 42

Step-by-step for read_value(0)

  1. ok is 0
  2. !ok becomes 1
  3. !!(!ok) is 1
  4. __builtin_expect(1, 0) returns 1
  5. The if condition is true
  6. 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.

Take Quiz

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

ConceptWhat it doesChanges logic?Typical use
Plain ifNormal branchNoDefault choice
likely(x)Hints that x is usually trueNoCommon fast path
unlikely(x)Hints that x is usually falseNoRare error path
Profile-guided optimization (PGO)Uses real runtime dataNoWhole-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) when x is usually true
  • use unlikely(x) when x is usually false

Example:

if (unlikely(err)) {
    return -1;
}

This is often clearer than trying to invert the condition awkwardly.

Take Quiz

Cheat Sheet

#define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

Quick rules

  • likely(x): x is usually true
  • unlikely(x): x is 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.

Take Quiz

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

Take Quiz

Keep learning

Related questions

View more