Function Inlining: When Optimizations Change Behavior
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:
- Function pointers and addresses
- Exception handling
- Order of evaluation in expressions
- Volatile variables
- 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.