스토리텔링 개발자

[Effective C++] 30. 인라인 함수 본문

개발/Effective C++

[Effective C++] 30. 인라인 함수

김디트 2024. 6. 26. 11:23
728x90

항목 30. 인라인 함수는 미주알 고주알 따져서 이해해 두자.

 

 

 

인라인 함수
  • 함수처럼 보이고, 함수처럼 동작하고, 매크로보다 훨씬 안전하고 쓰기 좋다.(항목 2 참조)
  • 함수 호출 비용이 면제된다.(함수 호출 시 발생하는 오버헤드 걱정이 없다.)
  • 컴파일러가 함수 본문에 대해 문맥별(context-specific) 최적화를 해줄 여지가 높아진다.
    • 대체적으로 컴파일러 최적화는 함수 호출이 없는 코드가 연속적으로 이어지는 구간에 적용되도록 설계된다.
    • 인라인 함수는 함수 호출이 없는 것과 마찬가지로 인식되므로 최적화될 확률이 높다.
    • 실제로 대부분의 컴파일러는 아웃라인(outline) 함수 호출(일반적인 함수 호출)에 대해서는 이런 최적화를 적용하지 않는다.

 

 

 

인라인 함수와 프로그램 크기
  • 인라인 함수 아이디어는 함수 호출문을 그 본문으로 바꿔치기하자는, 단순한 방식이다.
  • 대부분의 경우 인라인을 사용하면 목적 코드의 크기가 커진다.
    • 메모리 제한 컴퓨터에서는 인라인을 남발할 시 프로그램 크기가 그 기계 허용 공간을 초과할 수 있다.
    • 가상 메모리를 사용하는 환경이라 메모리 제한에 대한 걱정이 없는 상황이라 쳐도, 수행 성능 하락은 피할 수 없다.
      • 페이징 횟수 증가
      • 명령어 캐시 적중률 하락
  • 본문 길이가 굉장히 짧은 인라인 함수의 경우
    • 교체되는 인라인 함수 본문의 크기가 함수 호출문에 대해 만들어지는 코드보다 작을 수도 있다.
    • 이 경우 인라인 함수를 사용하면 되려 프로그램 크기가 작아진다!

 

 

 

인라인 정의 방법
  1. 암시적 방법
    • 클래스 선언부에 함수를 바로 정의해 넣으면 인라인으로 취급한다.
    • class Person
      {
      public:
          ...
          int age() const { return theAge; } // 암시적 인라인 요청
          ...
      private:
          int theAge;
      };
    • 대부분 멤버 함수겠지만, 프랜드 함수도 클래스 내부에서 정의될 수 있는데, 이 경우도 마찬가지이다.(항목 46 참조)
  2. 명시적 방법
    • 함수 선언부 혹은 정의부 앞에 inline 키워드를 붙인다.
    • template<typenameT>
      inline const T& std::max(const T& a, const T& b) // 명시적 인라인 요청
      {
          return a< b ? b : a;
      }

 

 

 

인라인 함수와 템플릿은 대개 헤더 파일 안에 정의한다
  • 대부분의 빌드 환경에서 인라인을 컴파일 타임에 수행한다.
    • 그러므로 인라인 함수는 대체적으로 헤더 파일에 인라인 함수가 있는 것이 맞다.
    • 인라인 함수 호출을 함수 본문으로 바꿔치기 할 때 컴파일러가 함수 형태를 알아야 하기 때문이다.
  • 템플릿을 인스턴스로 만드는 작업은 컴파일 타임에 수행한다.
    • 그러므로 템플릿은 대체적으로 헤더 파일에 있는 것이 옳다.
    • 템플릿을 사용하는 부분에서 해당 템플릿을 인스턴스로 만들려면 컴파일러가 템플릿의 형태를 알아야 하기 때문이다.
  • 하지만, 템플릿 인스턴스와와 인라인은 서로 전혀 관련이 없다.
    • 즉, 함수 템플릿이 반드시 인라인 함수여야 할 인과관계는 없다.
    • 함수 템플릿에 인라인을 사용할지는 비용을 계산해서 처리해야 할 문제이다.

 

 

 

인라인은 컴파일러가 무시할 수 있는 요청이다.
  • 인라인 함수라도 복잡한 함수는 절대 인라인 확장의 대상이 아니다.
    • 루프가 들어 있다거나 재귀함수인 경우
  • 가상 함수 호출은 인라인 해 주지 않는다.
    • virtual의 의미는 "어떤 함수를 호출할지 결정하는 작업을 런타임에 결정한다"이다.
    • inline의 의미는 "함수 호출 위치에 호출된 함수를 끼워넣는 작업을 프로그램 실행 전에 한다."이다.
    • 서로간의 의미가 상충되므로 인라인 요청이 무시되는 것.
  • 인라인 함수가 실제로 인라인 되는가는 컴파일러에 달려 있다.
    • 대부분의 컴파일러는 인라인 실패 시 경고 메세지를 띄워주는 설정이 가능하다.(항목 53 참조)

 

 

 

완벽한 인라인 조건이라도 인라인 되지 않는 상황
  1. 인라인 함수의 주소를 취하는 코드가 있을 경우
    • 있지도 않은 함수의 주소를 취할 수는 없기 때문이다.
  2. 인라인 함수를 함수 포인터로 호출하는 경우
    • inline void f() { ... } // 컴파일러가 반드시 인라인 해준다고 가정해보자.
      
      void (*pf)() = f; // f를 가리키는 함수 포인터
      ...
      f(); // 인라인 된다.
      
      pf(); // 인라인 되지 않는다.
    • 인라인 되지 않는 인라인 함수 문제
      • 사용자가 사용하지 않더라도, 컴파일러가 함수 포인터를 필요로 하는 경우가 있을 수 있다.
      • 예를 들면) 컴파일러가 어떤 객체의 생성과 소멸 상황에서 생성자 / 소멸자의 함수 포인터를 요구한다.

 

 

 

생성자 / 소멸자는 인라인되기 좋지 않다.
class Base
{
public:
    ...
private:
    string bm1, bm2;
};

classs Derived : public Base
{
public:
    Derived() {} // Derived의 생성자가 비어 있다? 정말 비어있을까?
    ...
private:
    string dm1, dm2, dm3;
};
  • 위 코드만 보면 Derived의 생성자는 아무 코드도 없으니까 인라인 하기 딱 좋다고 오해할 수 있다.
  • 하지만 C++은 객체 생성, 소멸 시 반드시 이루어져야 하는 보장이 존재한다.
    • 생성자, 소멸자가 호출되며, 데이터 멤버들을 생성하고 소멸하는 등 보이지 않는 보장들.
    • 이는 컴파일러가 알아서 생성하는 코드가 존재함을 의미한다.
    • 아마도 컴파일러는 이런 식의 코드를 만들어낼 것이다.
    • Derived:Derived()
      {
          Base::Base();
          
          // dm1의 생성과 예외 처리
          try
          {
              dm1.string::string();
          }
          catch (...)
          {
              Base::~Base();
              throw;
          }
          
          // dm2의 생성과 예외 처리
          try
          {
              dm2.string::string();
          }
          catch (...)
          {
              dm1.string::~string();
              Base::~Base();
              throw;
          }
          
          // dm3의 생성과 예외 처리
          try
          {
              dm3.string::string();
          }
          catch (...)
          {
              dm1.string::~string();
              dm2.string::~string();
              Base::~Base();
              throw;
          }
      }
       
  • 즉 비어있어 보여도 생각보다 비대하고, 상속 트리가 복잡할수록 더더욱 비대할 것이므로 인라인으론 적합하지 않다.

 

 

 

라이브러리 설계 시 인라인 함수
  • 라이브러리 설계 시 함수를 인라인 선언하면 바이너리 업그레이드를 제공할 수 없다.
    • 라이브러리 업그레이드 시 컴파일을 다시 해야 한다.
    • 인라인이 아니라면 링크 과정만 다시 해주면 된다.

 

 

 

인라인 함수에 대한 디버깅 문제
  • 대부분의 디버거가 디버깅하기 곤란해한다.
    • 있지도 않은 함수에 중단점을 어떻게??

 

 

 

인라인 함수 선언 전략
  1. 우선 아무것도 인라인하지 않는다.
  2. 꼭 인라인 해야 하는 함수(항목 46 참조) 나 정말 단순한 함수에 한해서만 인라인하는 것으로 시작한다.
  3. 디버깅하고 싶은 부분에서 디버거를 제대로 쓸 수 있도록 만들어 두고, 정말 필요할 때만 인라인 함수화 한다.
728x90
Comments