Item 30: 인라인 함수는 미주알고주알 따져서 이해해 두자

인라인 함수에 대한 좋은 점은 Item2에서 간략하게 다뤘다. 이번 장에서는 Inilne 함수에 대해서 좀 더 자세하게 알아본다.

단순하게 인라인 함수를 사용하면 본문을 그대로 넣어준다라는 개념으로 이해하기 보다. 대체적으로 컴파일러 최적화는 함수 호출이 없는 코드가 연속적으로 이어지는 구간에 적용되도록 설계되었기 때문에, 인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기 용이해진다는 개념이 있다. 아웃라인 함수(일반 함수)는 이런 최적화를 적용하지 않는다.

그러나 항상 트레이드 오프가 일상인 프로그래밍 세계에서 인라인 함수도 피해갈 수 없다. 인라인 함수는 결국 함수의 호출문을 함수의 본문으로 바꿔치기 하는 것이라 목적 코드의 크기가 커지는 것을 피할 수 없다.

즉, 메모리가 제한된 컴퓨터에서 아무 생각 없이 남발했다간 프로그램 크기가 그 기계에서 쓸 수 있는 공간을 넘어갈 수 있다. 페이징 횟수가 늘어나고, 명령어 캐시 적중률이 떨어질 가능성이 있다.

반대의 경우로 본문 길이가 매우 짧은 인라인 함수를 사용하면, 함수 본문에 대해 만들어지는 코드의 크기가 함수 호출문에 대해 만들어지는 코드보다 작아질 수 있다. 이런 경우에는 목적 코드의 크기도 작아지며, 명령어 캐시 적중률도 높아진다.

결국 inline함수는 컴파일러에 ‘요청’하는 것이지, ‘명령’이 아니다. 이 요청은 inline을 붙이지 않아도 암시적으로 되는 경우도 있고, 명시적으로 되는 경우도 있다.

암시적 inline

1
2
3
4
5
6
class Person {
public:
    int age() const { return theAge; }
private:
    int theAge;
};

이런 함수는 암시적으로 inline함수로 선언된다.

명시적 inline

1
2
3
4
5
template<typename T>
inline const T& min(const T& a, const T& b)
{
    return a < b ? a : b;
}

여기선 inline함수로 나오지만 C++ 11이상 constexpr을 사용하면 더 좋다.

인라인 함수는 대체적으로 헤더 파일에 들어 있어야 하는게 맞다. 왜냐하면 대부분의 빌드 환경에서인라인을 컴파일 도중에 수행하기 때문이다. 인라인 함수 호출을 그 함수의 본문으로 바꿔치기 하려면, 일단 컴파일러는 그 함수가 어떤 형태인지 알고 있어야 한다.

템플릿 역시 헤더파일에 들어 있어야 한다. 템플릿이 사용되는 부분에서 해당 템플릿을 인스턴스로 만들려면 그것이 어떻게 생겼는지를 컴파일러가 알아야 하기 때문이다.

inline은 컴파일러 선에서 무시할 수 있는 요청

대부분의 컴파일러의 경우 아무리 인라인 함수로 선언되어 있어도 자신이 보기에 복잡한 함수는 절대로 인라인 확장의 대상에 넣지 않는다. C++ 14부터는 조건문이나 반복문도 inline으로 선언할 수 있다.

  • virtual의 의미가 “어떤 함수를 호출할지 결정하는 작업을 런타임에 한다”
  • inline의 의미가 “함수 호출 위치에 호출된 함수를 끼워 넣는 작업을 컴파일 타임에 한다”

결국 인라인 함수가 되냐 안되느냐는 전적으로 개발자가 사용하는 빌드 환경에 달렸다.

함수 포인터와 inline

어떤 인라인 함수의 주소를 취하는 코드가 있으면, 이 코드를 위해 아웃라인 함수 본문을 만들 수 밖에 없을 것이다. 있지도 않은 함수에 대해 주소를 취하는 것은 불가능하다.

1
2
3
4
5
6
7
8
9
10
inline void f()
{
    ...
}

void (*pf)() = f;
...
f(); // 인라인

pf(); // 아웃라인

생성자와 소멸자 inline

먼저 말하지만 생성자와 소멸자는 인라인 하기 좋은 함수가 아니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
    ...
    
private:
  std::string bm1, bm2;
};

class Derived : public Base {
public:
    Derived() {}
    ...
private:
    std::string dm1, dm2, dm3;
};

C++은 객체가 생성되고 소멸될 때 일어나는 일들에 대해 여러 가지 보장을 준비해놨다. new을 통해 동적으로 생성하는 경우 자동으로 생성자를 호출해주거나 delete를 통해 소멸하는 경우 소멸자를 호출해준다. 이런 일들은 컴파일러가 자동으로 처리해주는 것은 C++에 깔아둔 보장이다.

어떤 객체를 생성하면 그 객체의 기본 클래스 부분과 그 객체의 데이터 멤버들이 자동으로 생성되며, 그 객체가 소멸할 때 이에 반대되는 순서로 소멸 과정이 일어나는 것도 마찬가지다. 또한 생성 과정에서 예외가 발생하더라도 이미 생성된 부분은 말끔하게 소멸자를 호출해주는 것도 마찬가지다.

핵심은 C++은 위와 같이 무엇을 해야 하는지는 정해두었지만, 어떻게 해야 하는지는 정해두지 않았다. (컴파일러 구현자에게 달림)

Derived 클래스의 생성자는 최소한 자신의 데이터 멤버와 기본 클래스 부분에 대해 생성자를 호출해 주어야 하고, 이들 생성자를 호출해야 하기 때문에 인라인이 적합하지 않다.

정리

이처럼 인라인 함수는 생각보다 따질 것이 많다. 개발에서 이를 명확하게 알고 필요시 사용하는 것이 중요하다. getter, setter함수에만 사용하는 것이 좋을 것 같다..!

  • constexpr
    • 상수 표현식으로 계산 가능한 함수가 필요할 때.
    • 컴파일 시간 최적화와 상수 초기화가 중요한 경우.
    • 예: 수학적 계산, 상수 값 생성.
  • inline 사용:
    • 작은 함수에서 호출 오버헤드를 줄이고 싶을 때.
    • 코드 크기가 크지 않은 단순한 유틸리티 함수에 적합.
    • 예: 단순한 getter, setter, 연산 함수.
  • 함수 인라인은 작고, 자주 호출되는 함수에 대해서만 하는 것으로 묶는다. 이렇게 되면 디버깅 및 라이브러리의 바이너리 업그레이드가 용이해지고, 자칫 생길 수 있는 코드 부풀림 현상을 최소화하며, 프로그램의 속력이 더 빨라질 수 있는 여지가 최고로 많아진다.
  • 함수 템플릿이 대개 헤더 파일에 들어간다는 일반적인 부분만 생각해서 이들을 inline으로 선언하면 안 된다.

태그: ,

카테고리:

업데이트:

댓글남기기