Inline Specifier Compilation in C/C++

Introduction

The intent of the inline specifier is to serve as a hint for the compiler to perform optimizations, such as function inlining, which usually require the definition of a function to be visible at the call site. So with the inline specifier, the compiler might create an optimized program which runs faster.

In this blog post, I would like to discuss the inline optimization mechanism and the best practice of creating inline functions for programs.

Inline Mechanism

To understand how the inline specifier helps to optimize the program, we have to understand the memory model for modern computer programs.

C Memory Model

The memory model for modern computer programs has stack. When a function is called during runtime, some necessary information, such as the return address, local variables, and function arguments, will be allocated and pushed to the stack.

Consider computing a the factorial of $n$ using a recursive function fact, for each recursion, a fact function information will be pushed onto the stack.

C Memory Model

So when there are too many recursions during runtime and the function does not get return before the program used up the available memory space for stack, we will get “stack overflow” error. Iterative function will not have this kind of problem.

Inline Optimization

The “negative” effect of calling nested functions from the current function is that it introduced overhead time for pointing to the function code, pushing and pop information from the stack. If we only have a single main function in our program without any auxillary functions, which surely we can write, the program would be “linearly” executed, the overhead time I mentioned above could be eliminated, and thus the program should be faster. However, because there is only one main function and we could not reuse the code from auxillary function, the code size on the memory could be too big to fit into the memory.

So one way to optimize the program is to reduce the number of push and pop from stack, but not to reduce all of them. The inline specifier on the function will ask the compiler to “unroll” the nested function and replace the nested function call in the function with the equivalent implementations.

This is very useful for some functions that will call many small functions during runtime. With the inline of the small functions, the code size on the memory will not grow too large, and the number of push and pop from stack is reduced significantly.

Example

The following example has the library header file foo.h, the library source file foo.cpp, the main program source file main.cpp, and the CMake file CMakeLists.txt. We would like to make some of the functions or methods inline.

Code Files

foo.h
1
2
3
4
5
6
7
8
class Foo
{
public:
int add(int x, int y);
int minus(int x, int y);
};

int multiply(int x, int y);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "foo.h"

int Foo::add(int x, int y)
{
return x + y;
}

int Foo::minus(int x, int y)
{
return x - y;
}

int multiply(int x, int y)
{
return x * y;
}
main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "foo.h"
#include <iostream>

int main()
{
Foo foo;
int x{1};
int y{2};

int sum{foo.add(x, y)};
std::cout << "The sum of " << x << " and " << y << ":" << std::endl;
std::cout << sum << std::endl;

int difference{foo.minus(x, y)};
std::cout << "The difference of " << x << " and " << y << ":" << std::endl;
std::cout << difference << std::endl;

int product{multiply(x, y)};
std::cout << "The product of " << x << " and " << y << ":" << std::endl;
std::cout << product << std::endl;
}
CMakeLists.txt
1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.13.0)

project(INLINE_TEST VERSION 0.0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 14)

add_library(foo STATIC foo.cpp)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE foo)

Build

1
2
3
4
$ mkdir build
$ cd build
$ cmake ..
$ make

Incorrect Inline Method

To make the function inline, if we add inline to the beginning of either the function declaration or the function definition in either foo.h or foo.cpp. When we compile the program, we would get the linker error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ make
Scanning dependencies of target foo
[ 25%] Building CXX object CMakeFiles/foo.dir/foo.cpp.o
[ 50%] Linking CXX static library libfoo.a
[ 50%] Built target foo
[ 75%] Linking CXX executable main
CMakeFiles/main.dir/main.cpp.o: In function `main':
main.cpp:(.text+0x176): undefined reference to `multiply(int, int)'
collect2: error: ld returned 1 exit status
CMakeFiles/main.dir/build.make:84: recipe for target 'main' failed
make[2]: *** [main] Error 1
CMakeFiles/Makefile2:109: recipe for target 'CMakeFiles/main.dir/all' failed
make[1]: *** [CMakeFiles/main.dir/all] Error 2
Makefile:83: recipe for target 'all' failed
make: *** [all] Error 2

This is a common error for implementations that uses the inline specifiers.

Correct Inline Method

The correct inline method is to move the definition of the functions that we would like to inline to the header.

foo.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Foo
{
public:
inline
int add(int x, int y)
{
return x + y;
}
int minus(int x, int y);
};

inline
int multiply(int x, int y)
{
return x * y;
}
foo.cpp
1
2
3
4
5
6
#include "foo.h"

int Foo::minus(int x, int y)
{
return x - y;
}

Final Remarks

The inline specifier is only a suggestion to the compiler and the compiler might just ignore it. The modern compiler might even inline function for us even if we did not specify inline at all. I have heard that it is not necessary to use the inline specifier for conventional programs because the compiler will take care the optimization for us.

References

Author

Lei Mao

Posted on

02-05-2020

Updated on

05-01-2021

Licensed under


Comments