본문 바로가기

C

C: #ifndef / likely(x) / unlikely(x) / __builtin_expect(!!(x), 1)

728x90
반응형
#if (CONFIG_COMPILER_OPTIMIZATION_PERF)
#ifndef likely
#define likely(x)      __builtin_expect(!!(x), 1)
#endif
#ifndef unlikely
#define unlikely(x)    __builtin_expect(!!(x), 0)
#endif
#else
#ifndef likely
#define likely(x)      (x)
#endif
#ifndef unlikely
#define unlikely(x)    (x)
#endif
#endif

/**
 * Macro which can be used to check the condition. If the condition is not 'true', it prints the message
 * and returns with the supplied 'err_code'.
 */
#define ESP_RETURN_ON_FALSE(a, err_code, log_tag, format, ...) do {                             \
        if (unlikely(!(a))) {                                                                   \
            ESP_LOGE(log_tag, "%s(%d): " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);        \
            return err_code;                                                                    \
        }                                                                                       \
    } while(0)

 

CONFIG_COMPILER_OPTIMIZATION_PERF 가 true일 때,

 

#ifndef likely

  - likely가 정의되어 있지 않다면

 

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

  - likely(x)를  __builtin_expect(!!(x), 1)과 같이 정의한다.

 

__builtin_expect

The __builtin_expect function is a built-in function provided by GCC (GNU Compiler Collection) and is also available in some other compilers like Clang. It's used to provide the compiler with branch prediction information. The function __builtin_expect has the following signature and usage:

long __builtin_expect(long exp, long c);

Here, exp is the expression whose value is to be tested, and c is the value that the programmer expects exp to frequently hold. This built-in function essentially tells the compiler that exp is likely to be equal to c most of the time. This hint can help the compiler optimize the code for the more likely case, potentially making the program run faster by improving branch prediction accuracy on modern processors.

즉, 특정 표현식(long exp)에 대해서 다른 표현식(long c)과

대부분의 경우 같다고 branch prediction에 사용되는 compiler 내장 함수

 

__builtin_expect(!!(x), 1)

 

 

!!(x):

  This is a common C idiom to normalize the value of x to either 0 or 1. This works by first negating x (!x), which converts

  any non-zero value to 0 and zero to 1. The second negation flips these values back, so any non-zero value becomes 1,

  and zero remains 0. This ensures that whatever the original value of x, the result used in __builtin_expect is always 

  either 0 or 1.

  예를 들어

    x가 0이면 !(x)는 1이고, 이를 다시 부정하는 !!(x)는 0. 결국 0은 0.

    x가 1이면 !(x)는 0이고, 이를 다시 부정하는 !!(x)는 1. 결국 1은 1.

    x가 0만 아니라면 !(x)는 0이고, 이를 다시 부정하는 !!(x)는 1.

    -> !!(x)의 값은 0 또는 1만 가능

__builtin_expect(!!(x), 1): This expression tells the compiler that the normalized value of x is expected to be 1 most of the time. In other words, it predicts that the condition (x) (after normalization) is true in the majority of cases.

 

즉 위의 전처리기는

CONFIG_COMPILER_OPTIMIZATION_PERF 값이 있으면,

  likely(x)와 unlikely(x)를 __builtin_expect에 따라서 처리하고

CONFIG_COMPILER_OPTIMIZATION_PERF 값이 없으면

  likely(x)와 unlikely(x)는 그냥 x를 반환

 

 

 

The optimization provided by likely and unlikely macros doesn't affect the speed of the compilation process itself; rather, it optimizes the execution speed of the compiled program, particularly on the target embedded machine where the code runs. Here's a detailed look at how these optimizations impact execution:

Branch Prediction

Modern CPUs use a feature called branch prediction to guess which way a conditional branch (like an if statement) will go before it's actually computed. This allows the CPU to continue executing instructions without waiting to see the result of the conditional statement, which could otherwise cause a stall.

How likely and unlikely Help

The likely and unlikely macros inform the compiler about the expected likelihood of conditions within if statements:

  • likely(x) tells the compiler that the condition x is expected to be true most of the time. Therefore, the compiler can arrange the code such that the instructions for the true branch are in a more predictable place, which is faster for the CPU to access if the prediction is correct.
  • unlikely(x) does the opposite; it tells the compiler that the condition x is expected to be false most of the time. Thus, the compiler can optimize the code arrangement for the scenario where x is false.

Effect on Code Execution

By using these hints, compilers can produce machine code that aligns better with how modern processors execute conditional branches:

  • Minimizing Pipeline Stalls: Processors use an instruction pipeline that can be disrupted by branch mispredictions (i.e., the processor guessed wrong). By correctly predicting branches, stalls are minimized, allowing smoother and faster instruction flow.
  • Optimizing Instruction Caching: The processor's instruction cache can be better utilized if the code that is most likely to be executed is placed where it can be prefetched efficiently.

Practical Example

Consider a loop where one branch of an if statement inside the loop is very rarely taken:

for (int i = 0; i < n; i++) {
    if (unlikely(i == 999)) {
        // Code that runs only when i is 999
    } else {
        // Code that runs for every other value of i
    }
}

 

In this example, the unlikely macro can help the compiler optimize the machine code layout so that the branch that handles all values of i other than 999 is prioritized, which is critical for a loop executing many times with only one rare deviation.

Conclusion

Using likely and unlikely does not speed up the compilation of the code but rather the execution of the code on the target device. These optimizations are particularly beneficial in systems like embedded devices where performance and predictable timing are crucial. The goal is to optimize how the CPU handles branching to make execution as efficient as possible, especially in critical or performance-sensitive parts of the code.

 

 

Determining when to use the likely and unlikely macros effectively in your code depends on your specific application and its performance characteristics. These macros should be used judiciously; inappropriate use can degrade performance rather than improve it. Here are some guidelines and considerations for determining when to use these branch prediction hints:

1. Profile Your Code

Before deciding to use these macros, it’s essential to understand where the performance bottlenecks in your application are. Use profiling tools to analyze your program's performance and identify critical sections where the CPU spends a lot of time, especially in branch-heavy areas like loops or conditional checks.

2. Understand Branch Behavior

Once you've identified a potential spot for optimization, analyze the behavior of the branches. likely and unlikely are most effective when you can clearly determine that one branch is taken much more frequently than another.

  • Example: In a server application, if you have a check that most requests are valid, but occasionally one might be invalid, you might mark the validity check as likely.

3. Use in Performance-Critical Sections

Focus on using likely and unlikely in parts of the code that are:

  • Performance-sensitive: Such as real-time processing loops or high-frequency operations.
  • Containing long-running loops: Where even a small percentage improvement in branch prediction could translate into significant performance gains.
  • Highly conditional code: Code with many branches, where the flow of execution is highly dependent on conditional outcomes.

4. Be Cautious with Predictability

It’s important to be reasonably sure about the predictability of the condition:

  • Data-Driven: Use these macros based on actual runtime data or predictable behavior patterns. For example, error conditions in an inner loop might be rare and can be marked as unlikely.
  • Avoid Guessing: Don’t use these hints based on guesses or assumptions without data to back up the likelihood of branch execution, as incorrect usage can lead to worse performance due to misprediction penalties.

5. Iterative Testing and Validation

Implement these macros incrementally and test the changes:

  • Test Changes: After adding likely or unlikely, benchmark your application again to see if there is a noticeable improvement or if adjustments are needed.
  • Compare Versions: Sometimes, what seems like a predictable pattern may not be as impactful due to other compiler optimizations already in place, or the hardware may not respond as expected.

6. Consider Compiler and Hardware

Different compilers and target hardware can respond differently to branch prediction hints:

  • Compiler Differences: Some compilers might be better at auto-optimizing certain kinds of branch predictions even without explicit hints.
  • Hardware Capabilities: Modern CPUs have sophisticated branch prediction units that can sometimes negate the need for manual hints, especially in less clear-cut scenarios.

Conclusion

likely and unlikely are powerful tools when used correctly in the right context, primarily in performance-critical and highly predictable branches. Their effectiveness is highly dependent on specific application behavior, compiler capabilities, and processor architecture. Regular profiling and benchmarking are essential to ensure that their use actually benefits the application’s performance.

 

unlikely(!(a))

  • !(a): This negates the condition a. If a is true, !(a) becomes false, and vice versa.
  • unlikely(...): This is a hint to the compiler that the expression inside it (here, !(a)) is expected to be false most of the time. That is, the condition a is expected to be true most of the time, and the macro is primarily concerned with handling the exceptional case where a is false.

In terms of the compiler's understanding of branch prediction,

unlikely(!(a)) and likely(a) effectively serve the same purpose.

 

Understanding unlikely(!(a)) and likely(a)

  • unlikely(!(a)):
    • This construct is used to say that it is unlikely for the negation of a to be true. In simpler terms, it predicts that a will usually be true, and it is rare for a to be false.
    • In practice, this expression is often used in error handling or exceptional cases in the code. It tells the compiler that the typical execution path will not enter the conditional block guarded by !(a) (i.e., when a is false).
  • likely(a):
    • Conversely, likely(a) directly states that the expression a is expected to be true most of the time.
    • This is generally used in situations where a guards a common or expected code path, optimizing for the scenario where a is true.

Conclusion

While unlikely(!(a)) and likely(a) express the same prediction about the condition a, their usage might be chosen based on readability or the contextual emphasis of the code (e.g., emphasizing the normal case vs. the exceptional case). Using likely and unlikely correctly can aid in performance optimization, particularly in performance-critical software like operating systems, embedded systems, or real-time applications where every cycle counts.

728x90
반응형

'C' 카테고리의 다른 글

C: bitmask forloop with enum  (0) 2024.06.09
버퍼 # buffer # memcpy # buffer overflow  (0) 2024.05.24
C: function pointer  (0) 2024.04.30
C: designated initializer # Array # Structure # 초기화  (0) 2024.04.29
C: union # struct  (0) 2024.04.26