인라인 함수(inline function)
inline 함수란, 컴파일 타임에 함수 호출 위치에 함수 전체 코드를 삽입하는 것으로, 함수 호출에 의한 오버헤드를 줄이기 위해 사용되는 함수입니다.
따라서, 너무 많은 인라인 함수 사용은 프로그램의 크기가 커지게 될 수 있습니다. 가상 메모리를 사용하는 환경에서는 페이징 횟수가 늘어나 성능에 영향을 줄 수 있다고도 합니다.
이 인라인 함수를 선언하는 방식으로는 함수 앞에 inline
키워드를 사용하는 명시적 inline과 컴파일러의 자체적인 판단으로 inline 처리하는 암시적 inline이 있습니다.
함수 호출에 의한 오버헤드의 예)
- 스택 프레임 생성과 해제: 함수가 호출될 때마다 스택 프레임이 생성되고 함수가 종료되면 이 스택 프레임이 해제됩니다.
- 매개변수 전달: 함수 호출 시에는 매개변수가 스택이나 레지스터를 통해 전달됩니다. 이 과정은 메모리를 사용하고 시간이 소요됩니다.
- 리턴 값 처리: 함수가 값을 반환할 경우, 이 값은 레지스터나 스택을 통해 전달되어야 합니다.
- 컨텍스트 스위칭: 함수 호출로 인해 CPU 레지스터의 값이 변경될 수 있으며, 이전 상태를 복원하는 데에도 비용이 듭니다.
명시적 inline
명시적 inline이란, 함수 선언이나 정의에 명시적으로 직접 inline 키워드를 사용하여 해당 함수를 인라인 함수로 만드는 것입니다.
이렇게 하면 컴파일러에게 이 함수를 인라인으로 처리하도록 요청할 수 있습니다.
단, 이것은 컴파일러에게 “이 함수를 inline으로 처리하면 좋겠다”라고 요청하는 것일 뿐, 컴파일러는 자체적인 판단 하에 인라인 처리를 무시할 수 있습니다.
1
2
3
4
inline void MyFunction() // 명시적으로 inline 선언
{
// 코드
}
암시적 inline
암시적 inline은 명시적으로 inline 키워드를 사용하지 않아도 컴파일러가 자동으로 인라인 처리를 하는 경우를 말합니다.
1. 클래스 멤버 함수
클래스 내부에서 멤버 함수를 정의하면 그 함수는 암시적으로 inline이 됩니다.
즉, inline 키워드를 명시적으로 사용하지 않아도 컴파일러는 멤버 함수를 inline으로 취급합니다.
물론, 이 경우에도 모두 인라인화 되는건 아닙니다.
1
2
3
4
5
6
7
8
class MyClass
{
public:
void myFunction() // 암시적 inline
{
// 코드
}
};
2. 템플릿 함수
템플릿 함수는 여러 타입에 대해 동일한 로직을 적용할 수 있도록 설계된 함수입니다.
예를 들어, 아래와 같은 코드는 정수나 실수에 대한 함수를 따로 만들지 않아도 템플릿 함수를 사용하여 두 경우를 모두 처리할 수 있습니다.
즉, 필요한 타입에 따라 컴파일러가 자동으로 해당 타입에 맞는 함수를 생성합니다.
1
2
3
4
5
template <typename T>
T add(T a, T b)
{
return a + b;
}
이 과정에서 컴파일러는 종종 이러한 템플릿 함수를 암시적으로 인라인 처리할 수 있습니다.
다시 말해, 컴파일러가 템플릿 함수를 인라인화하는 것이 더 좋다고 판단하면, 함수 호출 구문 대신 해당 함수의 실제 코드를 삽입하여 인라인 처리를 할 수 있습니다.
이 역시 컴파일러가 최적화 단계에서 템플릿 함수의 인라인화가 성능 향상에 도움이 될 것으로 판단할 경우에 이루어지고, 그렇지 않다면 인라인화 하지 않습니다.
3. 람다 함수
컴파일러는 람다 함수 또한 암시적으로 인라인 처리합니다.
람다 함수란, 함수를 별도로 선언하지 않고, 필요한 지점에서 바로 함수를 직접 정의해 쓸 수 있는 일종의 익명 함수라고 할 수 있습니다.
람다 함수를 사용하는 방법은 auto
키워드를 사용해 익명 객체에 저장하고 사용하는 경우, 함수 포인터 사용하는 경우, rvalue로 사용해 임시 객체로써 사용하는 경우가 있습니다.
이 중에서 auto
키워드 사용하는 경우와 rvalue로 사용되는 경우가 인라인 처리 될 가능성이 높습니다.
함수 포인터의 경우에는 함수의 주소가 메모리 어딘가에 실제로 존재해야하기 때문에 인라인화 되지 않을 가능성이 높습니다.
1
2
3
4
auto lambda = [](int x, int y) { return x + y; }; // 암시적 inline
int i = ([](int x, int y) { return x + y; })(3, 4); // 암시적 inline
cout << i << endl;
그 외에도 컴파일러의 최적화 전략에 따라 여러가지 경우가 있을 수 있습니다.
inline 처리가 안되는 경우
inline 키워드는 컴파일러에게 인라인 처리를 하도록 “요청”하는 것일 뿐, 반드시 인라인화 되는 것은 아닙니다.
다음과 같은 경우들이 대표적으로 인라인화 되지 않는 경우들 입니다.
1. 함수가 너무 복잡하거나 코드가 긴 경우
일반적으로, 함수가 많은 수의 루프, 조건문, 지역 변수를 가지고 있다면 인라인화 되지 않습니다.
인라인화 되기에는 코드가 너무 복잡하거나 프로그램의 크기를 너무 키울 수 있을거라는 판단을 하게 되면 인라인화를 하지 않는 것 같습니다.
2. 함수가 재귀 호출을 하는 경우
재귀 호출을 하는 함수를 인라인화하면, 각 재귀 호출마다 코드가 삽입되어야 하므로, 이는 무한한 코드 확장을 초래할 수 있습니다.
3. 함수가 static 변수를 포함하는 경우
함수 내 static 변수는 함수가 호출될 때마다 이 변수의 상태가 유지되어야 합니다.
즉, static 변수가 함수 호출 간에 상태를 유지해야 하는데, 인라인 함수는 호출 지점에 코드가 삽입되므로, static 변수의 상태를 유지하는 것이 복잡해질 수 있습니다.
4. 함수가 switch, goto 등의 제어문을 포함하는 경우
switch와 goto의 경우에는 해당 분기에 대한 주소나 오프셋을 계산하여 기계어 코드에 삽입합니다.
그러나 이러한 분기 주소가 인라인 함수 내부에 있을 경우, 문제가 발생할 수 있습니다.
인라인 함수는 여러 위치에서 호출될 수 있으므로, 하나의 switch나 goto 분기 주소가 여러 호출 지점에 적합하지 않을 수 있습니다. 따라서 이러한 제어문을 포함하는 함수를 인라인 처리하는 것은 코드를 복잡하게 만들 수 있으므로 인라인 처리가 되지 않을 수 있습니다.
5. 해당 함수의 주소가 다른 곳에서 함수 포인터로 사용되는 경우
함수의 주소가 함수 포인터로 사용되면, 그 함수는 메모리 어딘가에 실제로 존재해야 합니다. 따라서, 함수를 호출 지점에 단순히 코드로 삽입하는 것이 불가능해지기 때문에 인라인 처리가 되지 않을 수 있습니다.
inline 키워드들
inline
- C++이나 C99에서 지원되는 인라인 키워드
- 컴파일러에게 인라인 최적화를 요청합니다.
__inline
- C에서 사용하는 인라인 키워드
- _inline과 동의어
__forceinline
- 컴파일러의 최적화 분석을 무시하고 프로그래머의 판단에 의존하여 인라인 시키는 인라인 키워드 입니다.
_forceinline
과 동의어- 물론,
__forceinline
키워드를 사용하더라도 인라인화 되기 어려운 경우에는 무시될 수 있습니다.- 재귀 호출이면서 특별한 옵션(#pragma inline_recursion(on))을 사용하지 않은 경우
- 가변 인수 리스트를 가진 경우
- 가상 함수로 사용되는 경우
- 함수 포인터로 사용되는 경우
인라인 키워드들은 컴파일러에 따라 키워드들이 다를 수 있습니다.
위 키워드들은 MSVC를 기준으로 했습니다.
인라인 함수 vs 매크로 함수
C에서는 인라인 함수 대신 매크로 함수로 비슷한 효과를 볼 수 있습니다. 물론, 차이점이 존재하고 각각의 장단점이 존재합니다.
매크로 함수
- 매크로 함수는 전처리기에서 처리됩니다.
- 때문에, inline처럼 컴파일러에 의해 무시되는 경우가 없습니다.
- 매개변수의 타입 검사를 수행하지 않기 때문에, 자료형과 무관하게 정의할 수 있습니다.
- 매개변수의 타입 검사를 수행하지 않기 때문에, 안전하지 않습니다.
- 디버깅이 어렵습니다.
- 단순 치환이기 때문에, 연산자 우선순위에 의한 여러 사이드 이펙트가 발생할 수 있습니다.
인라인 함수
- inline 키워드는 컴파일 단계에서 처리됩니다.
- 따라서, 컴파일러의 최적화 전략에 따라 인라인화가 무시되기도 합니다.
- 암시적 inline에 의해 임의로 최적화가 되기도 합니다.
- 매개변수의 타입 검사를 수행하기 때문에, 매크로 함수보다 안전하게 사용할 수 있습니다.
- 컴파일러나 디버깅 환경에 따라 디버깅이 가능할 수 있습니다.
- 자료형과 무관하게 정의하고 싶다면 템플릿(Template)을 사용할 수 있습니다.