컴퓨터공학/C++

[More Effective C++] 24 : 가상함수, 다중상속, 가상 기본 클래스, RTTI에 들어가는 비용을 제대로 파악하자

Pyxis 2024. 11. 3. 15:03

[ 24 : 가상함수, 다중상속, 가상 기본 클래스, RTTI에 들어가는 비용을 제대로 파악하자 ]

 

 

1) 어떤 클래스에 가상 함수가 하나 이상 있는 경우, 그 클래스의 가상함수 테이블이 만들어진다. 가상함수 테이블은 클래스에 있는 모든 가상함수들의 (함수)포인터들을 모아둔 배열이다.

가상함수 테이블의 크기는 그 클래스가 갖는 가상함수의 개수(기본 클래스에서 상속받는 것까지 포함)에 비례하며, 가상 함수를 갖는 클래스마다 하나씩 만들어진다. 프로그램에 들어가는 클래스의 수가 매우 많거나, 클래스에 가상함수의 수가 매우 많은 경우, 무시 못할 크기의 메모리 부담이 될 수 있다.

 

2) 가상 함수를 하나 이상 갖는 클래스 타입의 객체마다, 가상함수 테이블의 시작 주소를 가리키는 멤버 포인터(가상테이블 포인터 : vptr)가 추가된다.

크기가 작은 객체일수록 가상 테이블의 포인터 크기가 부담이 될 수 있다. 예를 들어 x86에서 데이터 멤버의 크기가 4바이트밖에 되지 않는 작은 객체는 가상테이블 포인터(vptr)때문에 크기가 2배 증가하게 된다. 소형 시스템에서는 이 비용이 크다.

메모리가 큰 시스템이라고 해도, 속도 측면에서 손해를 본다. 덩치가 큰 객체는 캐시나 가상 메모리의 페이지에 잘 맞지 않게 되는데, 결국 운영체제의 page activity의 증가를 불러일으키기 때문이다.

 

3) 가상 함수는 일반 함수를 호출할 때 보다, 많은 비용이 든다. 하지만....?

C1 타입의 포인터 pC1으로 부터, C1클래스의 가상함수 f1을 호출한다고 가정해보자.

1. pC1이 가리키는 객체의 vptr을 따라 vtbl로 이동하고,2. vtble내에서 호출해야 하는 함수 (f1)에 해당하는 포인터를 찾아서3. 그 포인터가 가리키는 함수를 호출한다.

C1* pC1; 

/* ... */

pC1->f1();

위 명령문에 대해 생성된 코드는

(*pC1->vptr[i])(pC1);

이다.

이는 비가상 함수 호출과 거의 같은 효율성입니다. 대부분의 머신에서 몇 개의 명령어만 더 실행합니다. 따라서 가상 함수를 호출하는 비용은 기본적으로 함수 포인터를 통해 함수를 호출하는 비용과 같습니다. 가상 함수 자체는 일반적으로 성능 병목 현상이 아닙니다.

 

4) 가상 함수의 실제 런타임 비용은 인라인과의 상호 작용과 관련이 있습니다.

모든 실제적인 목적을 위해 가상 함수는 인라인되지 않습니다. "인라인"은 "컴파일 중에 호출 사이트를 호출된 함수의 본문으로 대체"를 의미하지만 "가상"은 "런타임까지 기다려 어떤 함수가 호출되는지 확인"을 의미하기 때문입니다. 컴파일러가 특정 호출 사이트에서 어떤 함수가 호출될지 모른다면 해당 함수 호출을 인라인하지 않는 이유를 이해할 수 있습니다. 이것이 가상 함수의 세 번째 비용입니다. 사실상 인라인을 포기하는 것입니다. (가상 함수는 객체를 통해 호출될 때 인라인될 수 있지만 대부분의 가상 함수 호출은 객체에 대한 포인터나 참조를 통해 이루어지며 이러한 호출은 인라인되지 않습니다. 이러한 호출이 일반적이기 때문에 가상 함수는 사실상 인라인되지 않습니다.)

→ 현대 컴파일러들은 인라인될 수 있는 함수는 대부분 자체적으로 인라인하여 최적화를 하기 때문에 가상함수는 이것을 포기하는 것.

 

5) 보통 RTTI에 사용되는 타입 정보는 vtbl의 요소로 포함시킵니다.

예를 들어 객체의 타입 정보를 vtbl의 첫 번째 요소로 집어넣습니다. 따라서 RTTI를 사용하게 되면 vtbl의 크기가 약간 늘어납니다. vtbl을 이용하여 RTTI를 구현하기 때문에 RTTI를 사용하려면 그 클래스에 가상 함수가 적어도 하나 이상은 있어야 합니다.

 

[ 다중 상속 ]

6)  지금까지 알아본 사항은 단일 상속과 다중 상속 모두 적용되는 것이었는데, 다중 상속까지 생각하면 더 복잡해 집니다. 그 결과, 클래스별 오버헤드와 객체별 오버헤드가 동시에 늘어나고, 런타임 생성 혹은 호출에 들어가는 비용도 그만큼 늘어납니다.
다중 상속을 받은 경우 vptr의 위치를 찾는 오프셋 계산이 더욱 복잡해 집니다. 한 객체 안에 vptr이 여러개 들어있고(상속받은 클래스에서 하나씩 가져온 결과), 파생 클래스에 대한 vtbl 외에 기본 클래스에 대한 vtbl까지 꼬여 있어서 아주 심난할 지경입니다.

 

7)
다중 상속에 그림자처럼 따라오는 것이 가상 기본 클래스(virtual base class)입니다. 가상 기본 클래스도 그 나름대로의 비용 부담을 가져옵니다. 데이터 멤버의 중복을 피하기 위해 가상 기본 클래스 부분에 대한 포인터를 객체에 넣어 둔다던지 해야 하기 때문입니다.