맨위로가기

가상 메소드 테이블

"오늘의AI위키"는 AI 기술로 일관성 있고 체계적인 최신 지식을 제공하는 혁신 플랫폼입니다.
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.

1. 개요

가상 메소드 테이블(vtable)은 객체 지향 프로그래밍에서 동적으로 바인딩된 메소드의 메모리 주소를 저장하는 자료 구조이다. 각 클래스는 고유한 vtable을 가지며, 객체가 생성될 때 vtable에 대한 포인터(vpointer)가 객체 내에 저장된다. 메소드 호출 시 vtable을 통해 실제 객체의 클래스에 해당하는 메소드를 찾아 실행한다. C++에서 동적 디스패치를 구현하는 핵심적인 기술이며, 다중 상속 환경에서는 thunk를 사용하여 포인터를 조정한다. vtable은 동적 디스패치를 구현하는 일반적인 방법이지만, 이진 트리 디스패치와 같은 대안적인 방법이 존재한다.

더 읽어볼만한 페이지

  • 메소드 (컴퓨터 프로그래밍) - 소멸자 (컴퓨터 프로그래밍)
    소멸자는 객체가 메모리에서 제거되기 직전에 호출되는 멤버 함수로, 객체 자원 해제 및 정리 작업을 수행하며, C++ 등 여러 언어에서 구현되고 메모리 누수 방지에 기여한다.
  • 메소드 (컴퓨터 프로그래밍) - 동적 디스패치
    동적 디스패치는 프로그램 실행 시 호출할 메서드를 결정하는 메커니즘으로, 단일 또는 다중 객체 기준으로 선택하며, 유연성과 확장성을 높이지만 성능 오버헤드를 발생시킬 수 있다.
  • 프로그래밍 패러다임 - 지식 표현
    지식 표현은 컴퓨터가 인간의 지식을 이해하고 활용하도록 정보를 구조화하는 기술이며, 표현력과 추론 효율성의 균형, 불확실성 처리 등을 핵심 과제로 다양한 기법과 의미 웹 기술을 활용한다.
  • 프로그래밍 패러다임 - 의도적 프로그래밍
    의도적 프로그래밍은 프로그래머의 의도를 명확히 포착하고 활용하여 소프트웨어 개발 생산성을 향상시키기 위한 프로그래밍 패러다임으로, 트리 기반 저장소를 사용해 코드 의미 구조를 보존하고, WYSIWYG 환경에서 도메인 전문가와 협업하며, 코드 상세 수준 조절 및 자동 문서화를 통해 가독성과 유지보수성을 높이는 데 중점을 둔다.
  • 객체 지향 프로그래밍 - Is-a
    Is-a 관계는 객체 지향 프로그래밍에서 한 유형이 다른 유형의 하위 유형임을 나타내는 관계로, 상속, 서브타이핑, 리스코프 치환 원칙과 관련되며, C++, Python, Java 등에서 표현된다.
  • 객체 지향 프로그래밍 - 객체 (컴퓨터 과학)
    객체는 객체 지향 프로그래밍에서 데이터와 조작을 묶어 메시지를 수신하고, 프로그램의 개념을 표현하며 가시성과 재사용성을 높이는 실체이다.
가상 메소드 테이블
가상 함수 테이블
다른 이름가상 메서드 테이블, 가상 호출 테이블
개념객체 지향 프로그래밍에서 동적 디스패치를 구현하는 데 사용되는 메커니즘
사용 목적런타임에 호출할 메서드를 결정
구현 방식각 클래스에 대해 컴파일러가 생성하는 함수 포인터 테이블
테이블 항목클래스에서 정의된 각 가상 함수에 대한 포인터
각 객체클래스에 대한 포인터를 포함
테이블 접근객체를 통해 테이블에 접근
호출할 메서드 결정테이블에서 해당 메서드에 대한 포인터를 찾아 호출
장점
유연성런타임에 객체의 실제 타입에 따라 다른 메서드를 호출할 수 있음
확장성기존 코드를 수정하지 않고 새로운 클래스를 추가하고 기존 클래스의 기능을 확장할 수 있음
다형성다양한 타입의 객체를 동일한 방식으로 처리할 수 있음
단점
오버헤드가상 함수 호출은 일반 함수 호출보다 약간의 오버헤드가 발생
메모리 사용량각 클래스마다 가상 함수 테이블을 저장해야 하므로 메모리 사용량이 증가할 수 있음
구현
C++C++에서는 `virtual` 키워드를 사용하여 가상 함수를 선언. 가상 함수를 포함하는 클래스는 가상 함수 테이블을 가짐.
JavaJava에서는 모든 메서드가 기본적으로 가상 함수임 (final로 선언된 메서드 제외).
C#C#에서는 `virtual` 키워드를 사용하여 가상 함수를 선언.
DelphiDelphi에서는 `virtual`, `dynamic`, `override` 키워드를 사용하여 가상 함수를 선언.
예제 (C++)
기본 클래스
| public:
| virtual void speak() { std::cout << "Meow!" << std::endl; }
파생 클래스| class HouseCat : public Cat {
| public:
| void speak() override { std::cout << "Meow! (but from a house)" << std::endl; }
또 다른 파생 클래스| class Lion : public Cat {
| public:
| void speak() override { std::cout << "Roar!" << std::endl; }
사용 예시
| myCat->speak(); // 출력: Meow! (but from a house)
대안
클래스 테이블Efficient Dynamic Dispatch without Virtual Function Tables: The SmallEiffel Compiler

2. 구현

객체의 디스패치 테이블은 객체의 동적으로 바인딩된 메소드들의 주소를 포함한다. 메소드 호출은 객체의 디스패치 테이블에서 메소드의 주소를 가져와 수행된다. 이 디스패치 테이블은 같은 클래스에 속한 모든 객체들에서 같으며, 그러므로 보통 그들끼리는 공유된다.[11] 타입 호환이 되는 클래스(예: 상속 계층 구조의 형제 클래스)에 속하는 객체들은 같은 레이아웃의 디스패치 테이블을 가질 것이다. 주어진 메소드의 주소는 모든 타입 호환이 되는 클래스의 같은 오프셋에 나타난다. 그래서 주어진 디스패치 테이블 오프셋에서 메소드의 주소를 꺼내오는 것은 객체의 실제 클래스와 상응하는 메소드를 갖게 되는 것이다.[11]

C++] 표준은 동적 디스패치를 어떻게 구현해야 하는지를 명확하게 규정하지 않지만, 컴파일러들은 일반적으로 유사한 기본 모델을 사용한다.[2]

일반적으로, 컴파일러는 각 클래스에 대해 별도의 가상 메소드 테이블을 생성한다. 객체가 생성될 때, 이 테이블에 대한 포인터인 '''가상 테이블 포인터''' ('''vpointer''' 또는 '''VPTR''')가 이 객체의 숨겨진 멤버로 추가된다. 컴파일러는 또한 각 클래스의 생성자에 "숨겨진" 코드를 생성하여, 객체의 vpointer를 해당 클래스의 가상 메소드 테이블 주소로 초기화한다.[2]

많은 컴파일러는 가상 테이블 포인터를 객체의 마지막 멤버로 배치한다. 다른 컴파일러는 이를 첫 번째 멤버로 배치한다. 이식 가능한 소스 코드는 어느 쪽이든 작동한다.[3] 예를 들어, g++은 이전에 포인터를 객체의 끝에 배치했다.[4]

2. 1. 가상 함수 테이블의 구조

컴파일러는 일반적으로 각 클래스마다 별도의 가상 함수 테이블(vtable)을 생성한다. 객체가 생성될 때, 이 가상 함수 테이블을 가리키는 포인터인 '''가상 테이블 포인터''' ('''vpointer''' 또는 '''VPTR''')가 객체의 숨겨진 멤버로 추가된다.[2] 그리고 컴파일러는 각 클래스의 생성자에 "숨겨진" 코드를 추가하여, 객체의 vpointer가 해당 클래스의 가상 함수 테이블 주소로 초기화되도록 한다.[3]

같은 클래스에 속하는 모든 객체는 동일한 가상 함수 테이블을 공유한다.[2] 상속 관계에 있는 클래스들(예: 부모-자식 클래스)은 호환되는 레이아웃의 가상 함수 테이블을 가지며, 주어진 메서드의 주소는 모든 호환되는 클래스에서 동일한 오프셋에 위치한다.[2] 따라서 가상 함수 테이블의 특정 오프셋에서 메서드 주소를 가져오면 객체의 실제 클래스에 해당하는 메서드를 얻게 된다.[2]

많은 컴파일러들이 vpointer를 객체의 마지막 멤버로 배치하지만,[3] g++처럼 객체의 첫 번째 멤버로 배치하는 컴파일러도 있다.[4]

C++ 표준은 동적 디스패치의 구현 방법을 명확하게 규정하지 않지만, 컴파일러들은 일반적으로 유사한 모델을 사용한다.[3]

2. 2. 다중 상속과 Thunk

g++ 컴파일러는 클래스 `D`가 클래스 `B1`과 `B2`를 다중 상속할 때 각 기본 클래스(`B1`, `B2`)에 대해 별도의 가상 메소드 테이블을 사용하여 구현한다. (다중 상속을 구현하는 다른 방법도 있지만, 이 방식이 가장 일반적이다.) 이 때문에 형변환 시 "포인터 수정", 즉 thunk가 필요하다.

다음 C++ 코드를 보자.

```cpp

D *d = new D();

B1 *b1 = d;

B2 *b2 = d;

```

이 코드를 실행하면 `d`와 `b1`은 같은 메모리 위치를 가리키지만, `b2`는 `d+8` 위치(`d`의 메모리 위치에서 8바이트 뒤)를 가리킨다. 따라서 `b2`는 `B2`의 인스턴스처럼 보이는, 즉 `B2` 인스턴스와 동일한 메모리 레이아웃을 갖는 `d` 내부 영역을 가리킨다.

3. 예시

cpp

class B1 {

public:

void f0() {} // 가상 함수가 아님

virtual void f1() {} // 가상 함수

int int_in_b1;

};

class B2 {

public:

virtual void f2() {} // 가상 함수

int int_in_b2;

};

class D : public B1, public B2 {

public:

void d() {} // 가상 함수가 아님

void f2() {} // B2::f2()를 오버라이드

int int_in_d;

};

```

위 클래스들을 이용하여 객체를 생성한다.

```cpp

B2 *b2 = new B2();

D *d = new D();

```

GCC의 g++ 3.4.6은 객체 `b2`와 `d`에 대해 다음과 같은 32비트 메모리 레이아웃을 생성한다.[14]
b2 객체 메모리 레이아웃```

b2:

+0: B2의 가상 메소드 테이블에 대한 포인터

+4: int_in_b2의 값

B2의 가상 메소드 테이블:

+0: B2::f2()

```
d 객체 메모리 레이아웃```

d:

+0: D의 가상 메소드 테이블에 대한 포인터 (B1용)

+4: int_in_b1의 값

+8: D의 가상 메소드 테이블에 대한 포인터 (B2용)

+12: int_in_b2의 값

+16: int_in_d의 값

총 크기: 20바이트.

D의 가상 메소드 테이블 (B1용):

+0: B1::f1() // B1::f1()은 재정의되지 않음

D의 가상 메소드 테이블 (B2용):

+0: D::f2() // B2::f2()는 D::f2()에 의해 오버라이드됨

```

`f0()`와 `d()`처럼 선언에 `virtual` 키워드가 없는 함수들은 일반적으로 가상 메소드 테이블에 포함되지 않는다.

클래스 `D`에서 `f2()` 메소드 오버라이딩은 `B2`의 가상 메소드 테이블을 복사하고 `B2::f2()`에 대한 포인터를 `D::f2()`에 대한 포인터로 대체하여 수행된다.

객체의 가상 메소드 테이블은 객체의 동적으로 바인딩된 메소드의 주소를 포함한다. 메소드 호출은 객체의 가상 메소드 테이블에서 메소드의 주소를 가져와 수행된다. 가상 메소드 테이블은 동일한 클래스에 속하는 모든 객체에 대해 동일하며, 따라서 일반적으로 이들 간에 공유된다. 상속 계층 구조의 형제 클래스처럼 타입 호환 클래스에 속하는 객체는 동일한 레이아웃의 가상 메소드 테이블을 갖는다. 따라서 가상 메소드 테이블 내의 주어진 오프셋에서 메소드의 주소를 가져오면 객체의 실제 클래스에 해당하는 메소드를 얻을 수 있다.[2]

3. 1. 호출

가상 메소드 호출은 가상 함수 테이블을 통해 이루어진다.

`d->f1()` 호출은 `d`의 `D::B1` 가상 포인터를 역참조하고, 가상 메소드 테이블에서 `f1` 항목을 찾은 다음, 해당 포인터를 역참조하여 코드를 호출하여 처리된다.
단일 상속단일 상속(또는 단일 상속만 있는 언어)의 경우, 가상 포인터가 항상 `d`의 첫 번째 요소인 경우 (많은 컴파일러에서 그렇듯이), 이는 다음과 같은 의사 C++로 축소된다.

```cpp

(*((*d)[0]))(d)

```

여기서 `*d`는 `D`의 가상 메소드 테이블을 가리키고, `[0]`은 가상 메소드 테이블의 첫 번째 메소드를 가리킨다. 매개변수 `d`는 객체의 this 포인터가 된다.
다중 상속일반적으로 `B1::f1()` 또는 `D::f2()`를 호출하는 것은 더 복잡하다.

```cpp

(*(*(d[0]/*D의 가상 메소드 테이블 포인터 (B1의 경우)*/)[0]))(d) /* d->f1() 호출 */

(*(*(d[8]/*D의 가상 메소드 테이블 포인터 (B2의 경우)*/)[0]))(d+8) /* d->f2() 호출 */

```

`d->f1()` 호출은 `B1` 포인터를 매개변수로 전달하고, `d->f2()` 호출은 `B2` 포인터를 매개변수로 전달한다. 두 번째 호출은 올바른 포인터를 만들기 위해 수정이 필요하다. `D`를 구현할 때 오버라이드 되었기 때문에 `B2::f2`를 호출하는 것은 불가능하며, `B2::f2`의 위치는 `D`의 가상 테이블에 없다.

이에 비해 비가상 함수인 `d->f0()` 호출은 훨씬 간단하다.

```cpp

(*B1::f0)(d)

4. 효율성

가상 함수 호출은 일반 함수 호출에 비해 추가적인 연산이 필요하여 본질적으로 느리다. 가상 함수 호출에는 최소한 추가적인 인덱스 간접 참조와 "fixup" 추가가 필요하기 때문이다.[2]

1996년에 수행된 실험에 따르면, 실행 시간의 약 6~13%가 올바른 함수로 디스패칭하는 데 소요되며, 오버헤드는 최대 50%까지 발생할 수 있다.[7] 하지만, 현대의 중앙 처리 장치 아키텍처에서는 더 큰 캐시와 더 나은 분기 예측 덕분에 가상 함수 호출 비용이 상대적으로 낮아질 수 있다.

또한, JIT 컴파일을 사용하지 않는 환경에서는 가상 함수 호출이 일반적으로 인라인 확장될 수 없다. 컴파일러는 가상 함수 테이블 사용을 피함으로써 성능을 최적화하려고 시도한다. 예를 들어, 컴파일러가 특정 시점에서 변수가 특정 클래스만을 가질 수 있다는 것을 알거나, 특정 함수를 오버라이드하는 하위 클래스가 없다는 것을 탐지할 수 있는 경우, 가상 함수 테이블 조회를 생략할 수 있다.

5. 대안과의 비교

가상 메소드 테이블(vtable)은 일반적으로 동적 디스패치를 구현하기 위한 좋은 성능 절충안이지만, 바이너리 트리 디스패치와 같은 대안도 존재한다.[1][8][15]

그러나 가상 메소드 테이블은 특수한 "this" 매개변수에 대한 단일 디스패치만 허용하며, 모든 매개변수의 유형을 디스패치에 고려하는 다중 디스패치(CLOS, Dylan, Julia)와는 대조적이다.

가상 메소드 테이블은 또한 디스패치가 컴파일 시간에 알려진 메소드 집합으로 제한되는 경우에만 작동하므로, 컴파일 시간에 구축된 간단한 배열에 배치할 수 있다. 이는 덕 타이핑 언어(Smalltalk, Python, JavaScript)와는 대조적이다.

이러한 기능 중 하나 또는 둘 다를 제공하는 언어는 종종 해시 테이블이나 다른 방법을 사용하여 문자열을 검색하여 디스패치한다. 이를 더 빠르게 만들기 위한 다양한 기술(예: 인터닝/토큰화 메소드 이름, 조회 캐싱, JIT 컴파일)이 있다.

참조

[1] 서적 Efficient Dynamic Dispatch without Virtual Function Tables: The SmallEiffel Compiler -- 12th Annual ACM SIGPLAN Conference on Object-Oriented Programming Systems, Languages and Applications (OOPSLA'97), ACM SIGPLAN, Oct 1997, Atlanta, United States. pp.125-141. inria-00565627 https://hal.inria.fr[...] Centre de Recherche en Informatique de Nancy Campus Scientifique, Bâtiment LORIA 1997
[2] 서적 1990
[3] 웹사이트 C++ Reference Guide: The Object Model II http://www.informit.[...] 2003
[4] 웹사이트 C++ ABI Closed Issues http://www.codesourc[...] 2011-06-17
[5] 문서
[6] 웹사이트 C++ - why there are two virtual destructor in the virtual table and where is address of the non-virtual function (gcc4.6.3) https://stackoverflo[...]
[7] 논문 The Direct Cost of Virtual Function Calls in C++ https://dl.acm.org/d[...] OOPSLA 1996
[8] 간행물 Stress-testing Control Structures for Dynamic Dispatch in Java http://www.usenix.or[...] Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02) 2002
[9] 논문 The Direct Cost of Virtual Function Calls in C++ http://www.cs.ucsb.e[...] OOPSLA 1996
[10] 간행물 Stress-testing Control Structures for Dynamic Dispatch in Java http://www.sagecerti[...] Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02) 2002
[11] 서적 1990
[12] 문서
[13] 웹사이트 C++ ABI Closed Issues https://web.archive.[...]
[14] 문서
[15] 간행물 Stress-testing Control Structures for Dynamic Dispatch in Java http://www.usenix.or[...] Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02) 2002



본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.

문의하기 : help@durumis.com