본문 바로가기
C++ 200제/코딩 IT 정보

C++ 코드최적화 2.2 - 초기화 리스트, 객체 할당 속도

by vicddory 2018. 6. 29.

C++ 코드최적화 2.2 - 초기화 리스트, 객체 할당 속도


10.3.3 멤버 초기화 리스트(Member-Initialization Lists)

당신이 4장, "Special Member Functions: Default Constructor, Copy Constructor, Destructor, and Assignment Operator,"에서 코드최적화를 읽었듯이, 멤버 초기화 리스트는 const 및 레퍼런스 데이터 멤버들의 초기화를 위하여 그리고 기저 또는 내장된 하위객체의 생성자로 인수들을 전달하기 위하여 요구된다.


만약 그렇지 않다면 데이터 멤버들은 생성자 몸체 내부에서 할당되거나 멤버 초기화 리스트에서 초기화될 수 있다.


예를 들면,


1
2
3
4
5
6
7
8
9
10
class Date              // mem-initialization version
{
private:
    int day;
    int month;
    int year;
    //constructor and destructor
public:
    Date(int d = 0int m = 0int y = 0) : day , month(m), year(y) {}
};
cs


대신에 당신은 다음처럼 생성자를 정의할 수 있다: (코드최적화 아님)


1
2
3
4
5
6
7
//assignment within the constructor body
Date::Date(int d, int m, int y)
{
    day   = d; 
    month = m; 
    year  = y;
}
cs


두 생성자 사이에 성능의 견지에서 차이가 존재하는가?

이 예에서는 차이가 존재하지 않는다. Date에 있는 데이터 멤버들 전부는 기본형 데이터 멤버들이다. 그래서 멤버 초기화 리스트에 의해 그것들을 초기화하는 것은 성능의 견지에서 생성자 몸체 내에서의 할당과 같다.


그러나 사용자 정의된 타입들에서 두 형식 사이의 차이는 분명하다. 그것을 설명하기 위하여 멤버 함수 계수 클래스인 C로 돌아가서 그것으로부터 두 개의 인스턴스들을 포함하는 또 다른 클래스를 정의하자:


1
2
3
4
5
6
7
8
9
class Person
{
private:
    C c_1;
    C c_2;
public:
    Person(const C& c1, const C& c2 ): c_1(c1), c_2(c2) {}
};
 
cs


Person의 생성자의 대안적인 코드최적화 버전은 다음과 비슷하게 보인다:


1
2
3
4
5
Person::Person(const C& c1, const C& c2)
{
    c_1 = c1; 
    c_2 = c2; 
}
cs


마지막으로, main() 드라이버는 다음과 같이 정의된다:


1
2
3
4
5
6
7
8
9
10
11
int main()
{
    C c; //created only once, used as dummy arguments in Person's constructor 
 
    for (int j = 0; j<30000000; j++)
    {
        Person p(c, c);
    }
 
    return 0;  
}
cs


두 버전은 펜티엄 II, 233MHz 컴퓨터 상에서 비교되었다.


결과들을 확인하기 위해 테스트는 다섯 번 반복되었다. 멤버 초기화 리스트가 사용되었을 때 main()에 있는 for 루프는 평균 12초가 걸렸다. 최적화되지 않는 버전은 평균 15초가 소요되었다.


다른 말로 하자면, 생성자 몸체 내부에서의 할당은 멤버-초기화된 생성자에 비해 25% 정도 더 느렸다. 멤버 함수 카운터들은 당신에게 차이의 이유에 대한 실마리를 제공한다.

표 10.1은 멤버 초기화된 생성자를 위하여 그리고 생성자의 몸체 내부에서의 할당을 위하여 클래스 C의 멤버 함수 호출들의 수를 나타낸다.


표 10.1 클래스 Person을 위하여 멤버 초기화 및 생성자의 몸체 내부에서의 할당 사이의 비교

초기화 방법

디폴트 생성자 호출들

할당 연산자 호출들

복사 생성자 호출들

소멸자 호출들

멤버 초기화 리스트

0

0

60,000,000

60,000,000

생성자 내부에서의 할당

60,000,000

60,000,000

0

60,000,000


멤버 초기화 리스트가 사용될 때, 내장객체의 복사 생성자 및 소멸자만이 호출되며(Person이 두 개의 내장 멤버들을 가지는 것에 주목하라), 이에 반해서 생성자 몸체 내부에서의 할당은 또한 내장객체당 하나의 디폴트 생성자 호출을 추가한다.


4장에서, 당신은 컴파일러가 생성자의 몸체 내부에서 어떤 사용자에 의해 작성된 코드 앞에 추가적인 코드를 어떻게 삽입하는지를 배웠다.


C++ 코드최적화 2.2 - 초기화 리스트, 객체 할당 속도


추가적인 코드는 기저 클래스들 및 클래스의 내장객체들의 생성자들을 invoke한다. 다형적 클래스들의 경우에 이 코드는 또한 vptr를 초기화한다.


클래스 Person의 할당 생성자(assigning constructor)는 다음과 같은 어떤 것으로 변환된다:


1
2
3
4
5
6
7
8
9
10
Person::Person(const C& c1, const C& c2) //assignment within constructor body
{
    // 사용자에 의해 작성된 코드 앞에 컴파일러에 의해 삽입되는 의사 C++ 코드
    c_1.C::C();     // 내장 객체 c_1의 디폴트 생성자를 invoke
    c_2.C::C();     // 내장 객체 c_2의 디폴트 생성자를 invoke
 
    // 사용자에 의해서 작성되는 코드는 여기에 온다:
    c_1 = c1; 
    c_2 = c2; 
}
cs


내장 객체들의 디폴트 생성은 그것들이 나중에 곧바로 새로운 값들로 재할당되기 때문에 불필요하다.


다른 한편으로 멤버 초기화 리스트는 생성자에 있는 사용자에 의해서 쓰인 어떤 코드최적화 코드 앞에 나타난다.


이러면 생성자 몸체가 사용자에 의해 쓰인 어쩐 코드를 포함하지 않기 때문에 변환된 생성자는 다음과 유사하게 보인다:


1
2
3
4
5
6
7
8
Person::Person(const C& c1, const C& c2)        // 멤버 초기화 리스트 ctor
{
    // 사용자에 의해 작성된 코드 앞에 컴파일러에 의해 삽입되는 의사 C++ 코드
    c_1.C::C(c1);   // 내장 객체 c_1의 복사 생성자를 invoke
    c_2.C::C(c2);   // 내장 객체 c_2의 복사 생성자를 invoke
 
    // 사용자에 의해 작성된 코드가 여기에 온다
}
cs


당신은 하위객체들을 갖는 클래스를 위하여 멤버 초기화 리스트가 생성자의 몸체 내부에서 할당보다 더 바람직하다는 것을 이 예제로부터 결론내질 수 있다.


이러한 이유로 많은 프로그래머들은 기본형들의 데이터 멤버들을 위해서조차도 전면적으로 멤버 초기화 리스트들을 사용한다.


10.3.4 Prefix Versus Postfix Operators

prefix 연산자들 ++와 --는 postfix 연산자들이 사용될 때 오퍼랜드가 변경되기 전에 임시적인 사본이 오퍼랜드의 값을 보유하는 데 필요해지기 때문에 postfix 버전들보다 더 효율적인 경향이 있다. 코드최적화가 이뤄진다.


코드최적화 c++ 소스 속도


기본형들을 위하여 컴파일러는 여분의 사본을 제거할 수 있다. 그러나 사용자 정의된 타입들을 위하여 이것은 거의 불가능하다. 오버로드된 prefix 및 postfix 연산자들의 전형적인 구현은 두 버전 사이의 차이를 설명한다:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Date
{
private:
    //...
    int AddDays(int d); 
 
public:
    Date operator++(int unused); 
    Date& operator++(); 
};
 
Date Date::operator++(int unused)       // postfix
{
    Date temp(*this);    // 현재 객체의 사본을 생성
    this->AddDays(1);    // 현재 객체를 증가시킴
    return temp;         // 그것이 증가하기 전에 객체의 사본을 값에 의해 반환
}
 
Date& Date::operator++()     // prefix
    this->AddDays(1);          // 현재 객체을 증가
    return *this;              // 현재 객체를 레퍼런스에 의해 반환
}
cs


오버로드된 postfix ++은 두 가지 이유에서 prefix보다 훨씬 덜 효율적이다:


그것은 임시적인 사본의 생성을 요구하며, 그 사본을 값에 의해 반환한다. 그러므로, 당신이 객체의 postfix 및 prefix 연산자들 사이에서의 선택이 자유로울 때마다 prefix 버전을 선택하라.


코드 최적화(Optimization of Your Codes)


10.1 들어가며

10.2 당신의 소프트웨어를 최적화하기 전에

10.3 선언 배치(Declaration Placement)

10.4 인라인 함수들(Inline Functions)

10.5 메모리 사용을 최적화하기(Optimizing Memory Usage)

10.6 속도 최적화(Speed Optimizations)

10.7 최후의 수단(A Last Resort)

10.8 결론들(Conclusions)


C++ 코드최적화 2.2 - 초기화 리스트, 객체 할당 속도

댓글