Swift: vTable, Witness Table

틀틀보·2025년 8월 25일

Swift

목록 보기
11/19

virtual Function

C++에 존재하는 상속 가능성을 의미하는 함수

C++에 존재하는 virtual Function은 일반 함수와 다르게 런타임에 실 객체 타입에 따라 호출되는 함수

#include <iostream>

class Animal {
public:
    // 1. 가상 함수: 런타임에 실제 객체 타입에 따라 호출
    virtual void makeSound() {
        std::cout << "동물이 소리를 냅니다." << std::endl;
    }

    // 2. 일반 함수: 컴파일 타임에 포인터 타입(Animal) 기준으로 호출이 결정
    void sayHello() {
        std::cout << "동물이 인사합니다." << std::endl;
    }
};

class Dog : public Animal {
public:
    // makeSound()를 자식 클래스에 맞게 재정의(override)
    void makeSound() override {
        std::cout << "멍멍!" << std::endl;
    }

    // sayHello()도 재정의
    void sayHello() {
        std::cout << "강아지가 인사합니다." << std::endl;
    }
};

int main() {
    // 부모 클래스 포인터로 자식 클래스 객체를 가리킴
    Animal* pAnimal = new Dog();

    pAnimal->makeSound(); // 출력: "멍멍!" (O)
                          // virtual 함수이므로, pAnimal이 가리키는 실제 객체 Dog의 함수가 호출됨

    pAnimal->sayHello();  // 출력: "동물이 인사합니다." (X)
                          // 일반 함수이므로, 포인터 타입인 Animal의 함수가 호출됨

    delete pAnimal;
    return 0;
}
  • virtual Function으로 선언된 함수를 override할 때는 해당 키워드를 안 쓰더라도 괜찮음. virtual 키워드도 동일. 실수 방지를 위해 명시

  • virtual 키워드로 선언하지 않은 메서드는 컴파일 타임에 선언된 포인터 타입을 기준으로 메서드 호출

  • Swift의 클래스는 C++처럼 virtual 키워드가 없어도 기본적으로 모든 메서드가 가상 함수처럼 동적 디스패치로 동작

  • 상속이 불가능한 구조체(Struct)나 열거형(Enum)은 항상 정적 디스패치로 동작하여 성능상 이점

vTable(virtual Table)

동적 디스패치에서 가상 함수를 호출할 때 어떤 함수를 호출할 지 알려주는 테이블

⚠️ Swift에서는 vTable이라는 개념은 없음. 동일한 메커니즘만 존재

왜 필요할까?

  • 컴파일러는 코드를 기계어로 번역할 때, 호출할 함수의 메모리 주소를 미리 알아야 함.

  • Animal* pAnimal = new Dog(); 와 같이 부모 타입 포인터가 자식 객체를 가리키는 경우, pAnimal->makeSound()가 Animal의 것인지 Dog의 것인지 컴파일 시점에는 절대 알 수 없음.

  • 이를 위해 런타임에 호출할 함수를 결정해야 하는 동적 디스패치를 위해 vTable로 호출해야 할 함수를 알려주는 역할

  • C++에서 virtual 메서드를 지닌 클래스는 vTable을 가진다.

  • vTable은 해당 클래스의 virtual Function들의 실제 메모리 주소를 가진 배열

  • 각 객체의 숨겨진 포인터 vPtr은 자신의 가상함수 주소를 가리키는 vTable의 주소를 가리킴.

  • 자식 클래스가 가상 함수를 재정의하면 vTable에는 재정의된 가상 함수 주소를 가리키고, 재정의하지 않았다면 부모의 가상함수 주소를 가리킴.

  • 동적 디스패치는 이를 이용해 런타임 시점에 호출되어야 할 함수를 찾음.

Swift에서의 vTable

class Animal {
    func speak() {
        print("...")
    }
}

class Dog: Animal {
    override func speak() {
        print("Woof!")
    }
}

let pet: Animal = Dog()
// 런타임에 pet 객체의 테이블을 보고 Dog의 speak()를 호출
pet.speak() // 출력: "Woof!"

Swift는 키워드 없이 내부적으로 vTable과 같은 개념을 통해 동적 디스패치가 동작.

부모 자식의 수직적 관계로 이루어지는 테이블

vTable의 성능

객체 주소 → vTable 주소 → 함수 주소로 이어지는 2단계의 경로로 이어지므로, 여러 방식에 따라 상대적으로 성능 차이가 있음.

Witness Table

프로토콜로 다형성을 구현하기 위한 테이블

vTable과 무엇이 다른가?

  • 사용처: 클래스에만 적용되는 vTable, 클래스, 구조체, 열거형을 아우르는 Witness Table

  • 생성 시점: 클래스 정의 시점에 생성되는 vTable, 프로토콜을 채택한 시점에 생성되는 Witness Table

  • 구조: 수직적인 vTable, 수평적인 Witness Table

어떻게 동작할까?

컴파일 시점

  1. 프로토콜의 요구사항 확인

  2. 해당 프로토콜이 채택된 구현부 찾기

  3. 채택된 각 타입과 프로토콜 조합으로 Witness Table 생성 및 실제 구현된 메서드의 주소를 기록

런타임 시점

프로그램이 실행될 때, 프로토콜 타입의 변수는 Existential Container라는 보이지 않는 상자에 담겨 관리

Existential Container 대한 설명
https://velog.io/@js1436kt/Swift-Existential-Container

  1. 해당 변수의 Existential Container에 접근

  2. Existential Container안에 존재하는 인스턴스를 가리키는 포인터와 타입과 프로토콜 조합으로 만든 Witness Table을 가리키는 포인터로 접근

  3. 해당 Witness Table을 찾아 호출할 메서드의 실제 주소를 가져와 실행

  4. 이때, 값 포인터는 실행될 메서드에서 사용될 데이터 제공

Witness Table의 성능

Existential Container를 위해 메모리 할당/관리, 위트니스 테이블을 조회하는 등 여러 포인터로 주소를 이동하면서 단계를 거치는 과정 때문에 오버헤드가 가장 큼.

더 알면 좋은 내용

  • final, private 키워드 등을 통한 동적 디스패치 최적화

참고

https://www.devoops.kr/52

https://programmerpsy.tistory.com/68

https://developer.apple.com/videos/play/wwdc2016/416/

https://en.wikipedia.org/wiki/Virtual_method_table

profile
안녕하세요! iOS 개발자입니다!

0개의 댓글