동적 바인딩 덕분에 프로그래머는 형식 확인을 컴파일러에게 맡기게 되었고 많이 편해졌다. 반대로 동적 바인딩은 성능에 부정적인 영향을 끼칠 수 있다.
가상 함수 기법 만약 가상 함수를 사용하고 싶지 않다면, 직접 형식 확인 코드를 작성하여 동적 바인딩을 흉내를 낼수있다.
다음은 동물원 동물을 클래스로 표현한 것이다.
1
2
3
4
5
6
7
8
class ZooAnimal {
public:
ZooAnimal(int zooType) : myType(zooType) {}
virtual void draw() = 0;
int resolveType() { return myType; }
private:
int myType;
};
동물원 동물 클래스의 파생 클래스로 Bear와 Monkey가 있다.
1
2
3
4
5
6
7
8
class Bear : public ZooAnimal {
public:
Bear() : myName(""), ZooAnimal(BEAR) {}
Bear(const char *name) : myName(name), ZooAnimal(BEAR) {}
void draw();
private:
std::string myName;
};
1
2
3
4
5
6
7
8
class Monkey : public ZooAnimal {
public:
Monkey() : myName(""), ZooAnimal(MONKEY) {}
Monkey(const char *name) : myName(name), ZooAnimal(MONKEY) {}
void draw();
private:
std::string myName;
};
Bear와 Monkey 뿐만 아니라 앞으로 추가되는 ZooAnimal은 resolveType 메소드를 사용하여 각 객체를 구분할 수 있으며, 이를 위해서 각 동물 클래스의 생성자는 자신의 적절한 타입을 설정해줘야 한다. 이제 다양한 종류의 ZooAnimal을 담은 vector를 통해서 각 동물에 맞는 draw 메소드를 사용해서 동물을 그릴 수 있다.
아래와 같은 코드를 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void drawAllAnimals(std::vector<ZooAnimal *> animalList)
{
for (auto it = animalList.begin(); it != animalList.end(); ++it)
{
switch ((*it)->resolveType()) {
case BEAR:
(dynamic_cast<Bear*>(*it))->draw();
break;
case MONKEY:
/* */
default:
break;
}
}
}
하지만 위와 같은 코드는 유지보수하기 까다롭다. ZooAnimal에서 동물이 빠질 때 마다 위의 코드의 switch 구문에서 해당 동물을 제거 해야 한다. 또한 추가된 동물은 switch 구문에서 추가하고, 새로운 동물 타입과 생성자 코드를 추가해 주어야 한다.
가상 함수의 동적 바인딩을 사용한다면 위와 같은 의존성 문제점을 해결할 수 있다. 현재 ZooAnimal의 draw 메소드는 가상 함수이기 때문에, 동적 바인딩을 실행 시에 사용할 수 있으며 아래와 같다.
1
2
3
4
5
6
void drawAllAnimals(std::vector<ZooAnimal *> animalList) {
for (auto it = animalList.begin(); it != animalList.end(); ++it)
{
(*it)->draw();
}
}
가상 함수의 동적 바인딩을 수행하기 위해서 컴파일 타임이 아닌 런타임에 각 객체에 맞는 메소드를 확인할 수 있어야 한다. 이를 위해서 가상함수를 정의하고 있는 클래스이거나 그러한 클래스로부터 파생되었다면, 컴파일러는 해당 클래스를 위한 가상 함수 테이블(vtbl)을 생성한다. 가상 함수 테이블은 이 클래스가 정의한 모든 가상 함수들에 대한 포인터를 가지고 있다. 클래스 마다 하나의 테이블이 존재하고, 각 객체는 자신의 클래스의 가상 함수 테이블에 대한 숨겨진 포인터를 가지게 된다. 컴파일러는 가상 함수 테이블 포인터(vptr)에 대한 오프셋을 알고 있고, 가상 함수 테이블 포인터를 초기화 하기 위해서 객체 생성자에 초기화 코드를 삽입한다. 가상 함수는 성능상에 부하가 생기는 부분이 있다.
- 가상 함수 테이블 포인터를 생성자에서 초기화 해야 한다.
- 가상 함수는 포인터 간접 참조를 통해서 호출되며 올바른 오프셋을 통해서 함수에 접근해야 한다.
- 인라인은 컴파일 타임에 결정되지만, 가상 함수는 런타임에 확인할 수 있는 가상 함수는 인라인을 사용할 수 없다. 여기에서 처음 2가지 항목은 성능 부하라고 보기 힘들다.
가상 함수를 사용하지 않은 상황에서도 가상 함수 테이블을 생성자에서 초기화하는 부하는 동적 바인딩을 피하는 코드에서도 각 동물의 타입을 멤버에 초기화하는 부하와 동일하다.
두번째 항목은 switch 구문을 사용한 것과 동일한 성능 부하가 생긴다.
즉, 가상 함수의 성능 부하는 인라인으로 해당 함수를 만들지 못 하여 발생하는 부하와 동일하다. 만약 특정 가상 함수가 성능상의 문제가 있다면, 가상 함수 호출을 제거하기 위해서 컴파일러가 함수 바인딩을 컴파일 타임에 수행하도록 해야 한다. 이를 위해서 하드코딩하거나, 클래스를 템플릿 매개변수로 전달하여 동적 바인딩을 무시할 수 있다. 키 포인트
- 가상 함수의 부하는 인라인을 사용할수 없기 때문에 발생
- 템플릿은 상속 계층보다 더욱 성능에 좋다