[Effective C++] #28_예외 안전성 확보
Scott Meyers의 "Effective C++" 를 통해, C++ 구현에 필요한 개념들을 이해하고, 기록하기 위함입니다. 해당 항목은 5장 '구현', 항목 30 "인라인 함수는 미주알고주알 따져서 이해해 두자" 에 해당하는 내용입니다.
인라인 함수
인라인 함수는 함수 호출 시 발생하는 오버헤드를 줄이기 위해 함수를 호출하는 대신 함수가 호출되는 곳마다 함수 본문의 코드를 복사해 넣어 주는 방법입니다. 함수처럼 보이고, 함수처럼 동작하며, 메크로보다 훨씬 안전하고 쓰기 좋습니다. 책에서 얘기하는 '인라인 함수'의 장점은 이와 같습니다. 컴파일러 최적화는 함수 호출이 없는 코드가 연속되어 이어지는 구간에 적용되도도록 설계되어있고, 인라인 함수를 사용하면 컴파일러가 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 우리가 기본적으로 사용하는 일반 함수 혹은 아웃라인 함수 호출에 대해 컴파일러는 이러한 최적화를 적용하지 않죠. 그럼에도 불구하고 우리가 간과해서는 안될 인라인 함수의 단점은 분명 존재합니다. 인라인 함수로 인해 목적 코드의 크기가 커진다는 것입니다. 이로인해 페이징 횟수가 늘어나고, 명령어 캐시 적중률(Hit)이 떨어질 가능성도 높습니다. 반면, 짧은 인라인 함수 사용은 코드의 크기가 오히려 작아지며, 명령어 캐시 적중률도 덩달아 높아지죠.
암시적 사용과 명시적 사용
//암시적 사용
class Person {
public:
...
int age() const {return theAge};
...
private:
int theAge;
};
//명시적 사용
template<typename T>
inline const T& std::max(const T& a, constT& b)
{return a<b ? b : a;}
먼저, 암시적인 방법으로 클래스 정의 안에 함수를 바로 정의해 넣는 경우입니다. 이럴 경우, 컴파일러는 해당 함수를 인라인 함수 후보로 고려합니다. 다음은 명시적인 방법으로 함수 정의 앞에 inline 키워드를 붙이는 경우입니다.
인라인 확장의 대상이 되는 경우와 아닌경우
책에서 저자는 'inline은 컴파일러 선에서 무시할 수 있는 요청이다'라고 언급했습니다. 대부분의 컴파일러는 복잡해 보이는 코드는 절대로 인라인 확장의 대상에 넣지 않기때문이죠. 예를들면, 루프가 들어있거나, 알고 보니 재귀 함수인 경우가 있습니다. 더불어, 가상 함수의 경우, 컴파일러는 절대로 인라인 해 주지 않습니다. 왜일까요? 'virtual'의 의미와 'inline'의 의미를 비교해보죠. 'virtual'이 의미하는 바는 "어떤 함수를 호출할지 결정하는 작업을 실행 중에 한다"입니다. 반면에, 'inline'의 의미는 "함수 호출 위치에 호출된 함수를 끼워 넣는 작업을 프로그램 실행 전에 한다"입니다. 이럴 경우, 컴파일러가 함수 본문을 인라인 하지 않더라도 어쩔 수 없다고 합니다. 따라서, 인라인 함수가 실제로 인라인되느냐 안되느냐는 어느정도 개발자가 사용하는 빌드 환경에 달려있다고 할 수 있습니다.
복잡한 코드, 혹은 가상 함수의 경우 인라인 확장의 대상이 아닙니다.
1. 인라인이 안되는 경우
inline void f() {...} //명시적인 인라인 함수 정의
void (*pf) () = f; //f를 가리키는 함수 포인터 pf
...
f(); //inline 성공
pf(); //inline 실패
"f" 함수를 확실한 인라인 함수로 가정합시다. 이때, 인라인 함수의 주소를 취하는 "pf"(f를 가리키는 함수 포인터)를 호출하게 되면, 컴파일러는 이 코드를 위해 아웃라인 함수 본문을 만들 수 밖에 없겠죠. 인라인 함수를 자신을 가리키는 함수 포인터를 통해 호출하는 경우 대게 인라인 되지 않는다고 합니다. '어떻게 호출하느냐'도 고려해야겠죠.
2. 인라인이 안되는 경우
class Base {
public:
...
private:
std::string bm1, bm2;
};
class Derived : public Base {
public:
Derived() {}
...
private:
std::string dm1, dm2, dm3;
};
"생성자"와"소멸자"는 인라인하기에 좋지 않은 함수입니다. C++는 우리가 new를 하면 동적으로 만들어지는 객체를 생성자가 자동으로 초기화하도록 합니다. 더불어, 이들을 delete하면 이에 대응되는 소멸자가 호출되도록 보장하죠. 여기서 우리가 알아야 할 점은 C++는 '무엇을' 해야 할지는 정해두지만, '어떻게' 해야 하는지는 정하지 않습니다. '어떻게'는 컴파일러 구현자에게 달려있습니다. 우리 눈에 보이지 않는 어떤 코드가 우리 프로그램에 포함되어야 하고, 컴파일러가 컴파일 도중에 우리 소스 코드에 삽입하는 코드들이 존재합니다. 이러한 코드 삽입 장소가 "생성자"와 "소멸자"가 될 수 있죠. "Derived" 클래스의 생성자는 자신이 속한 클래스 데이터 멤버와 기본 클래스(Base 클래스)에 대해 생성자를 호출해 주어야 하고, 이들의 생성자 또한 호출해야 하기 때문에 인라인이 난감해지는거죠.
생성자와 소멸자는 인라인되지 않습니다
3. 인라인에 대해 고민해봐야 할 점
저자는 라이브러리 설계와 인라인이 미치는 영향에 대해 설명합니다. 인라인 함수에 대해서는 라이브러리 차원에서 바이너리 업그레이드를 제공할 수 없다고 나와있습니다. 예를들면, 라이브러리의 "F"라는 인라인 함수를 이용해, 사용자가 "F"의 본문을 컴파일해서 응용프로그램을 설계했다고 가정합시다. 만약, 라이브러리의 설계자가 "F"의 내부를 바꾸겠다고 결정했다면, 사용자는 각자의 소스 코드를 다시 컴파일해야하는 상황이 벌어집니다. 반면, "F"가 보통 함수일 경우, 사용자들은 링크만 다시 해주면 되는것이죠. 개발 작업이 더 투명해질 것입니다.
우선 아무것도 인라인 하지 않기로 하죠. 다만!
작고, 자주 호출되는 함수만 인라인으로 사용합시다. 성능 향상에 매우 큰 도움이 될것입니다.
'언어 > Effective C++' 카테고리의 다른 글
[Effective C++] #32_public 상속 (0) | 2022.01.03 |
---|---|
[Effective C++] #31_파일 사이의 컴파일 의존성 (0) | 2021.12.31 |
[Effective C++] #29_예외 안전성 확보 (0) | 2021.12.28 |
[Effective C++] #28_내부에서 사용하는 객체에 대한 핸들 반환 (0) | 2021.12.28 |
[Effective C++] #27_캐스팅 (0) | 2021.12.27 |