The Unexpected Consequences of Function Inlining

Function inlining is one of the oldest and most important compiler optimizations. By replacing a function call with the body of the called function, it eliminates call overhead and enables further optimizations. However, inlining isn’t just a performance enhancement – it can sometimes change observable program behavior.

Let’s look at a subtle example involving function pointers:

#include <stdio.h>

__attribute__((noinline))
void actual_function(int x) {
    printf("Value: %d\n", x);
}

inline void wrapper_function(int x) {
    actual_function(x);
}

int main() {
    // Get function pointers
    void (*fp1)(int) = &wrapper_function;
    void (*fp2)(int) = &wrapper_function;
    
    // Compare them
    if (fp1 == fp2) {
        printf("Function pointers are equal\n");
    } else {
        printf("Function pointers are different\n");
    }
    
    return 0;
}

What does this program output? It depends on compiler optimizations. With optimization disabled, both function pointers point to the wrapper_function and are equal. With optimization enabled, the compiler might inline wrapper_function wherever it’s called directly, but still needs to generate code for it since we’re taking its address.

However, the compiler might generate different versions of wrapper_function when taking its address in different places, resulting in different function pointers. I’ve seen this occur with GCC, where -O2 optimization produces “Function pointers are different” while -O0 produces “Function pointers are equal.”

Another interesting case involves exception handling in C++:

#include <iostream>
#include <stdexcept>

void helper(bool should_throw) {
    if (should_throw) {
        throw std::runtime_error("Error");
    }
}

void func_with_catch() {
    try {
        helper(true);
    } catch (const std::exception& e) {
        std::cout << "Caught in func_with_catch: " << e.what() << std::endl;
    }
}

inline void func_without_catch() {
    helper(true);
}

void outer_function() {
    try {
        func_without_catch();
    } catch (const std::exception& e) {
        std::cout << "Caught in outer_function: " << e.what() << std::endl;
    }
}

int main() {
    func_with_catch();
    outer_function();
    return 0;
}

When func_without_catch is inlined into outer_function, the exception is properly caught in outer_function. But if you prevent inlining (using __attribute__((noinline)) or equivalent), the stack unwinding behavior may change, affecting which catch block handles the exception.

These behaviors highlight an important principle: compiler optimizations aren’t just about performance – they can affect semantics, especially in edge cases involving:

  1. Function pointers and addresses
  2. Exception handling
  3. Order of evaluation in expressions
  4. Volatile variables
  5. Signal handling

For robust code, avoid writing programs that depend on these edge cases. When you can’t avoid them, use appropriate compiler directives and understand your compiler’s optimization behavior.