본문 바로가기

Effective C++/5. 구현

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

인라인함수 = 함수 호출문 -> 함수 본문으로 바꿔치기

 

  • 인라인 함수의 장점

 

인라인 함수 => 함수처럼 보이고, 함수처럼 동작하고, 매크로보다 훨씬 안전하고(항목 2 참조), 함수 호출 오버헤드도 걱정할 필요 없고

+

컴파일러 최적화는 함수 호출이 코드가 연속적으로 이어지는 구간에 보통 적용되도록 설계되었기 때문에, 인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별(context-specific) 최적화를 걸기가 쉽다.

아웃라인 함수엔 적용X

 

  • 인라인 함수의 단점

 

목적 코드의 크기가 커진다!!!

=> 제한된 메모리 환경에서는 코드가 메모리에 올라가지 못할 수도 있고, 가상 메모리 환경이더라도 페이징 횟수가 늘어나고, 명령어 캐시 적중률이 떨어질 가능성이 높다.

[운영체제 OS] 메모리 관리기법 - 페이징 (paging)이란? 내부 단편화(Internal Fragmentatoin) 대해 알아보자 (tistory.com)

심심해서 하는 블로그 :: [컴퓨터 구조] 캐시와 성능 (tistory.com)

 

=> 반대의 경우도 있다. 인라인 함수의 본문이 굉장히 짧으면, 오히려 함수 본문에 대해 만들어지는 코드의 크기가 함수 호출문에 대해 만들어지는 코드보다 작아질 수도 있다.(코드 최적화 등에 의해) 이러면 목적 코드의 크기도 작아지고, 명령어 캐시 적중률도 높아진다.

 

======================================================================

 

inline 컴파일러에 '요청' 하는 것이다. (keyword) => '명령' 아님 => 요청이라 inline 굳이 안붙여도 암시적으로 inline 되는 경우가 있고, inline 키워드로 명시할 수도 있다.

 

암시적 inline 요청 방법 : 클래스 정의 안에 함수를 바로 정의해 넣으면 된다.

 

 

클래스 정의 vs 선언, 사실 class 선언 정의는 어짜피 타입을 선언, 정의하는 것이기 때문에 같은 말인데, 여기서 굳이 구분하는 이유는 class foo {}; 처럼 클래스 이름만 선언(대게 헤더파일에 위치)하는 경우와, foo::bar() 등의 멤버 본문을 만드는 (대게 구현파일에 위치) 구분하기 위해 굳이 '클래스 정의'라고 말함

 

 

명시적 inline 요청 방법 : 함수 정의 앞에 inline 키워드를 붙인다.

예로, std::max 이런 구조이다.

 

template<class T>
inline const T& max(const T& a, const T& b)
{
   
return (a < b) ? b : a;
}

 

출처: <https://en.cppreference.com/w/cpp/algorithm/max>

 

max 템플릿이라는 때문에, '인라인 함수와 템플릿은 대게 헤더파일 안에 정의한다' 라는 이야기가 생각나는데, 이게 함수 템플릿은 반드시 인라인이어야 한다는게 아니다.

 

일단 인라인 함수 => 대체적으로 헤더 파일에 들어 있어야 한다.

? => 대부분의 빌드 환경에서 인라인을 컴파일 도중 수행하기 때문이다.

 

 템플릿 => 대체적으로 헤더 파일에 들어 있어야 한다.

? => 템플릿이 사용되는 부분에서 해당 템플릿을 인스턴스로 만들려면 그것이 어떻게 생겼는지를 컴파일러가 알아야 하기 때문이다.(이것도 대부분 환경에서 그럼)

 

=> 근데 템플릿의 인스턴스화는 인라인과 하등 상관이 없다. 함수 템플릿은 반드시 인라인이여야 한다는 것은 헛소리임, 그냥 템플릿으로 만들어지는 모든 함수가 인라인 함수였으면 싶다하면 인라인 붙이는 거임, 특정 함수에만 붙이고 싶다 -> 완전 특수화로 특정 함수에만 inline 붙이면 된다.

 

  • 결론 : 만들고 있는 함수 템플릿이 굳이 인라인될 이유가 없다면, 템플릿을 인라인으로 선언 안해도 (명시적이든 암시적이든)

 

인라인 = 분명 cost 존재한다. 아무렇게 붙이는게 아니다. 코드 비대화(특히 템플릿에 경우) + 하나의 cost 존재한다. 이건 있다 알아보자.

 

======================================================================

 

cost cost지만, "inline 컴파일러 선에서 무시할 있는 요청이다"  => 아무리 인라인 함수로 선언되있어도 컴파일러가 보기에 너무 복잡한 함수는 절대 인라인 확장의 대상이 아니다.(루프가 있거나, 재귀 함수인 경우 특히 그렇다.) 특히 가상 함수 호출 같은 것은 절대로 인라인 해주지 않는다. 아무리 간단해도? ?

 

virtual 의미 = "어떤 함수를 호출할지 결정하는 작업을 실행 한다."

inline 의미 = "함수 호출 위치에 호출된 함수 본문을 끼워 넣는 작업을 프로그램 실행 한다"

 

가상함수가 inline 되는거 자체가 말이 안됨

 

  • 결론 : 인라인 함수가 실제로 인라인될지는 컴파일러에게 달렸음
    • 다행스럽게 요청한 인라인이 실패했을 경우 경고메세지를 내주는 진단 설정 기능이 왠만한 컴파일러에는 있음(항목 53 참고)

 

======================================================================

 

인라인 함수가 아닐 없는, 완벽하게 인라인 조건을 갖춘 함수들도 컴파일러가 인라인을 안해주는 경우가 있다. ( 인라인이 되기위한 어떤 조건이 숨겨져있다는 뜻이지 않을까?)

 

예를 들어, 어떤 인라인 함수의 주소를 취하는 코드가 있으면, 컴파일러는 코드를 위해 아웃라인 함수 본문을 만들수 밖에 없다. 있지도 않은 함수에 어떻게 포인터를 가지고 오겠냐는 말이다.

+

인라인 함수로 선언된 함수를 포인터를 통해 호출하는 경우도 왠만하면 인라인 되지 않는다.

 

  • 결론 : 확실한 인라인 함수도 '어떻게 호출되느냐' 따라 인라인이 되기도 하고 안되기도 한다.

 

코드를 통해 간단히 예를 살펴보자.

 

 

 

 

인라인 되지 않은 인라인 함수는 우리가 함수 포인터를 사용하지 않아도 문제가 수 있다. 프로그래머만 함수 포인터를 쓰는게 아니다. 프로그래머가 함수 포인터를 사용하지 않는다고 해서 컴파일러가 함수 포인터를 안쓴다는게 말이 아니다.

 

예를 들어 우리는 어떤 객체의 생성자 소멸자를 inline으로 만들어도, 컴파일러가 outline 함수 본문을 만들 수도 있다.

=> 이게 적용되는 대표적인 예가 어떤 배열의 원소가 객체인 경우이다.

배열을 구성하는 객체들을 생성하고 소멸시킬 생성자/소멸자의 함수 포인터를 얻어내야 하는데, 함수 본문이 반드시 필요하다.

 

 

======================================================================

 

생성자와 소멸자 => 인라인하기 적합하지 않다.

 

 

Derived 생성자는 인라인하기 좋은 인상이긴 하다. 아무 코드도 안들어있기 때문이다. 하지만 이는 속임수이다.

 

C++ 객체가 생성되고 소멸될 일어나는 일들에 대해 여러 가지 보장(guarantee) 준비해 놓는다. 예를 들면,

  • new 하면 동적으로 만들어지는 객체를 생성자가 자동으로 초기화해주는
  • delete 하면 소멸자가 호출되는
  • 객체를 생성할 기본 클래스 부분과 데이터 맴버들이 자동으로 초기화 되는것
  • 객체가 소멸될 이에 반대되는 순서대로 소멸하는
  • 객체가 생성되는 도중에 예외가 던져지더라도, 이미 생성 완료된 부분을 자동으로 소멸시킴

 

C++ '무엇을' 해야 하지는 정해 두었지만(이게 보장임), '어떻게' 해야 하는지는 정해두지 않았다.

=> 이런 보장들이 '스스로' 일어나지는 않는다.

 

결론 :  우리 눈에 보이지 않지만, 이런 보장을 가능케 하는 코드들(컴파일러가 만들어서 컴파일 도중에 삽입하는 코드) 우리의 소스코드에 포함되어야 한다.(스스로 되는게 아니라, 우리가 만든 코드에 따라 이런 보장들이 삽입 되는 것이다.)

 

그러니까, 비어있다고 생각되던 Derived 생성자는 사실, 구현환경에 따라 다음과 같은 코드가 삽입되었을 있다는 뜻이다.

 

 

 

실제 이렇다는 아니다. 다만 이런식으로 동작은 한다.(C++ 보장)

 

Base 생성자의 경우에도 똑같이 생각하면 된다. 그러니까 Base 생성자가 인라인 되면, 여기 있는 Base 생성자 호출문이 전부 함수 본문 코드로 바뀌는 것이다. 그리고 string 생성자도 어쩌다가 인라인되면 Derived 생성자는 똑같은 함수 구문을 다섯개!! 갖게 되는 것이다.(Base string 2, Derived 생성자 3)

 

결론 : 생성자( 소멸자) 인라인 하지 않는게 좋다.

 

======================================================================

 

라이브러리를 설계할 때도 특히 함수의 inline 선언을 신중하게 고민해야 한다. 만약 f()라는 인라인 함수가 있고, 라이브러리를 쓰는 사용자가 f 본문을 컴파일해서 응용프로그램을 만들었다고 치면, 나중에 라이브러리 개발자가 f() 바꾸겠다고 결정하면, f() 썼던 사용자들은 다시 컴파일 해야됨

<=> f() 보통함수이면 링크 다시 해주면 된다. 만약 라이브러리가 동적 링크 방식을 취하고 있다면 사용자는 가만히 있어도

 

인라인함수도 따질게 상당히 많다. 근데 실전에서는 인라인 함수 고민하는 이유가 솔직히 한가지임 => 인라인 함수는 디버깅하기 매우 불편하다!

 

어떤 빌드 환경은 인라인 함수의 디버깅을 어떻게든 해주기도 하는데, 왠만한 디버거는 그냥 디버그 빌드를 인라인을 비활성화 해주는 정도만으로 끝남 => 이게 어디야

 

정리

 

기본 전략

  • 아무것도 인라인 하지 마라
  • 매우 단순한 함수(이번 항목의 Person::age() 같은 함수) 한해서만 inline 함수로 선언하는 것을 시작으로 하자.
  • 인라인을 주의해서 사용해서, 디버깅하고 싶은 부분에서 디버거를 제대로 있게 하자.
  • 정말 필요한 위치에 인라인 함수를 놓도록 하자. (수동으로 최적화하는 )
  • 80-20 법칙을 잊지말자. 전체 코드 결과의 80% 20% 코드가 원인이다. 20% 코드를 찾아서 최적화하자.
  • 함수 자체를 똑바로 만드는걸 중요시 하자.

 

======================================================================

 

  •  함수 인라인은 작고, 자주 호출되는 함수에 한해서만 하는 것으로 전략을 묶어두자. 이렇게 하면 디버깅 라이브러리의 바이너리 업그레이드가 용이해지고, 자칫 생길 있는 코드 부풀림 현상이 최소회되며, 프로그램의 속력이 빨라질 있는 여지가 최고로 많아진다.
  • 함수 템플릿이 대게 헤더 파일에 들어간다는 일반적인 부분만 생각해서 이들을 inline으로 선언하면 안된다.