Swift는 객체지향언어이기에 상속이라는 개념이 있다. 자식클래스는 부모클래스의 메소드와 프로퍼티들을 오버라이드(재정의)할 수 있다. 이렇게 오버라이드를 하게 될 경우, 프로그램은 실제 함수 호출을 어떻게 할지 알아보자
class Parent {
func someMethod() {
print("parent")
}
}
class Child: Parent {
override func someMethod() {
print("child")
}
}
let object: Parent = Child()
object.someMethod()
Dispatch
는 어떤 메소드, 프로퍼티를 사용할 것인지를 결정하는 것이다. Dispatch
의 종류에는 Static
과 Dynamic
이 있다.
Static
vs Dynamic
Dispatch??
Static Dispatch
는 컴파일 타임
에 실제 호출할 함수를 결정할 수 있기 때문에 함수 호출 과정이 간단하고, 컴파일러가 이것을 최적할 수 있는 여지가 많다. 하지만, 참조 타입에 따라 호출될 함수가 결정이 되기 때문에 서브클래스의 장점을 누리기 어렵다Dynamic Dispatch
는 런타임
에 호출될 함수를 결정한다. 이를 위해서 Swift에서는 클래스마다 (Virtual Dispatch Table)이라는 것을 유지한다. 이는 함수 포인터들의 배열로 표현되며, 하위 클래스가 메소드를 호출할 때 이 배열을 참조하여 실제 호출할 함수를 결정한다. 이 모든 과정이 런타임에 결정되기 때문에 Static Dispatch에 비하면 추가적인 연산이 필요할 수밖에 없고, 컴파일러가 최적화 할 여지가 많지 않다value 타입인 struct
와 enum
은 상속을 사용할 수 없다는 특징을 가지고 있다. 즉, Static Dispatch
의 단점인 서브클래싱이 불가능하다는 단점을 완벽히 피해간다. 따라서 value 타입에는 Static Dispatch
가 적용된다
swift에서 상속을 사용할 수 있는 class
는 항상 상속의 가능성에 노출되어 있기 때문에 일반적으로는 Dynamic Dispatch
를 사용한다
protocol
구현체를 제공하지 않고, 선언만 한다. 그래서, protocol
을 채택한 타입은 이를 구현해야 하며, protocol
을 통해 호출하는 메소드는 protocol
을 채택한 타입들이 실제로 구현한 메소드들이다. 그런데 protocol
타입의 참조로만 이들을 사용해야 한다면, 해당 인스턴스의 타입에 맞는 메소드를 호출해야 한다. 이를 위해 프로토콜은 고유의 vTable을 가지게 되며, 특별히 이를 Witness Table이라고 한다. 즉, 프로토콜은 Dynamic Dispatch
를 사용한다
값 타입의 경우 struct
인데, 상속 가능성이 없기 때문에 Static Dispatch
로 수행된다
extension
으로 클래스에 추가 기능을 구현할 경우에는 오버라이드 여부에 따라 다르다
Static Dispatch
수행Dynamic Dispatch
Dynamic Dispatch
를 사용할 수 없고, Static Dispatch
가 적용된다성능이 중요한 코드에서는 Dynamic Dispatch
의 오버헤드도 거스릴 수 있다. 또한 Dynamic Dispatch
의 가능성이 있는 코드에서는 컴파일러의 최적화가 제한되어버린다. 따라서 Swift는 Dynamic Dispatch
가 필요하지 않을 경우에 사용할 수 있는 3가지의 성능 최적화 방법을 제시한다.
final 키워드를 붙여서 선언된 클래스, 메소드, 프로퍼티는 오버라이드 할 수 없게 된다. 이렇게 되면 컴파일러는 Dynamic Dispatch를 사용하지 않아도 됨을 알고 이 부분을 최적화 해줄 수 있게 됩니다.
final class Student {
func someMethod() {
}
}
따라서 한 파일내에 해당 요소에 대한 오버라이드가 없는 경우 컴파일러가 이를 자동으로 Direct Call
로 바꿔줄 수 있습니다
class Student {
private func studyHard() {
}
}
Dynamic Dispatch의 종류에도 두 가지가 있다는 것을 최근에 알게 되었다. Table Dispatch와 Message Dispatch가 바로 그 주인공들이다.
동적 디스패치의 가장 기본적인 구현 방식이라고 한다. 위에서 Dynamic Dispatch를 설명했던 것과 동일하며, 함수 포인터로 이루어진 vTable 혹은 witness Table을 통해서 함수를 부르는 것이다. 모든 서브클래스는 각자의 vTable을 가지고 있으며 재정의한 모든 메소드에 대해 다른 함수 포인터를 가지고 있다. 서브클래스에서 새로운 메소드를 추가하면 해당 메소드에 대한 함수 포인터가 배열 끝에 추가된다. 컴파일러는 런타임에 이 테이블을 사용하여 어떤 메소드를 호출할지 결정한다.
class ParentClass {
func method1() { }
func method2() { }
}
class ChildClass: ParentClass {
override func method2() { }
func method3() { }
}
컴파일러는 ParentClass
와 ChildClass
의 vTable을 생성할 것이고, 런타임시에 해당 vTable를 참고하여 타입에 맞는 함수 포인터를 따라 가서 호출하는 연산이 일어날 것이다.
jcsoohwancho - Swift의 Dispatch 규칙
Increasing Performance by Reducing Dynamic Dispatch - Swift Blog
DongMinyoon - Method Dispatch