[Swift] Dynamic, Static Dispatch

Youngwoo Lee·2021년 3월 16일
0

스위프트 문법

목록 보기
1/4
post-thumbnail

Swift는 객체지향언어이기에 상속이라는 개념이 있다. 자식클래스는 부모클래스의 메소드와 프로퍼티들을 오버라이드(재정의)할 수 있다. 이렇게 오버라이드를 하게 될 경우, 프로그램은 실제 함수 호출을 어떻게 할지 알아보자

class Parent {
    func someMethod() {
        print("parent")
    }
}

class Child: Parent {
    override func someMethod() {
        print("child")
    }
}

let object: Parent = Child()
object.someMethod()

Dispatch

Dispatch 는 어떤 메소드, 프로퍼티를 사용할 것인지를 결정하는 것이다. Dispatch 의 종류에는 StaticDynamic이 있다.

Static vs Dynamic Dispatch??

  • Static Dispatch컴파일 타임에 실제 호출할 함수를 결정할 수 있기 때문에 함수 호출 과정이 간단하고, 컴파일러가 이것을 최적할 수 있는 여지가 많다. 하지만, 참조 타입에 따라 호출될 함수가 결정이 되기 때문에 서브클래스의 장점을 누리기 어렵다
  • Dynamic Dispatch런타임에 호출될 함수를 결정한다. 이를 위해서 Swift에서는 클래스마다 (Virtual Dispatch Table)이라는 것을 유지한다. 이는 함수 포인터들의 배열로 표현되며, 하위 클래스가 메소드를 호출할 때 이 배열을 참조하여 실제 호출할 함수를 결정한다. 이 모든 과정이 런타임에 결정되기 때문에 Static Dispatch에 비하면 추가적인 연산이 필요할 수밖에 없고, 컴파일러가 최적화 할 여지가 많지 않다

Value 타입에서의 Dispatch

value 타입인 structenum은 상속을 사용할 수 없다는 특징을 가지고 있다. 즉, Static Dispatch의 단점인 서브클래싱이 불가능하다는 단점을 완벽히 피해간다. 따라서 value 타입에는 Static Dispatch가 적용된다

Reference 타입에서의 Dispatch

swift에서 상속을 사용할 수 있는 class는 항상 상속의 가능성에 노출되어 있기 때문에 일반적으로는 Dynamic Dispatch를 사용한다

protocol에서의 Dispatch

protocol 구현체를 제공하지 않고, 선언만 한다. 그래서, protocol을 채택한 타입은 이를 구현해야 하며, protocol을 통해 호출하는 메소드는 protocol을 채택한 타입들이 실제로 구현한 메소드들이다. 그런데 protocol타입의 참조로만 이들을 사용해야 한다면, 해당 인스턴스의 타입에 맞는 메소드를 호출해야 한다. 이를 위해 프로토콜은 고유의 vTable을 가지게 되며, 특별히 이를 Witness Table이라고 한다. 즉, 프로토콜은 Dynamic Dispatch 를 사용한다

Extension에서의 Dispatch

1) value 타입

값 타입의 경우 struct 인데, 상속 가능성이 없기 때문에 Static Dispatch로 수행된다

2) class 타입

extension으로 클래스에 추가 기능을 구현할 경우에는 오버라이드 여부에 따라 다르다

  • 오버라이드 하거나, 오버라이드 되지 않는 경우 : 자기 자신 뿐 아니라 하위 타입에서도 동일한 메소드를 참조함을 보장할 수 있다. 따라서 클래스 타입이어도 Static Dispatch 수행

3) protocol 타입

  • 본체에 선언된 멤버의 디폴트 구현체를 제공하는 경우
    하위 클래스들이 디폴트 구현체를 사용할지, 오버라이드된 구현체를 사용할지 모르기 때문에 Dynamic Dispatch
  • 본체에 없는 기능을 추가한 경우
    본체에 선언하지 않고 extension을 통해서 추가한 메소드들은 Witness Table 사용 불가. 즉, Dynamic Dispatch 를 사용할 수 없고, Static Dispatch 가 적용된다

성능이 중요한 코드에서는 Dynamic Dispatch의 오버헤드도 거스릴 수 있다. 또한 Dynamic Dispatch의 가능성이 있는 코드에서는 컴파일러의 최적화가 제한되어버린다. 따라서 Swift는 Dynamic Dispatch 가 필요하지 않을 경우에 사용할 수 있는 3가지의 성능 최적화 방법을 제시한다.

Dynamic Dispatch 제한 방법

1. 오버라이드 될 필요가 없는 요소들에는 final 키워드를 붙인다

final 키워드를 붙여서 선언된 클래스, 메소드, 프로퍼티는 오버라이드 할 수 없게 된다. 이렇게 되면 컴파일러는 Dynamic Dispatch를 사용하지 않아도 됨을 알고 이 부분을 최적화 해줄 수 있게 됩니다.

final class Student {
	func someMethod() {
    
   	}
}

2. private키워드를 붙여서 선언하게 되면 해당 요소는 한 파일내에서만 참조되는 것이 자동으로 보장이 됩니다.

따라서 한 파일내에 해당 요소에 대한 오버라이드가 없는 경우 컴파일러가 이를 자동으로 Direct Call로 바꿔줄 수 있습니다

class Student {
	private func studyHard() {
   	
   	}
}

추가 내용

Dynamic Dispatch의 종류에도 두 가지가 있다는 것을 최근에 알게 되었다. Table Dispatch와 Message Dispatch가 바로 그 주인공들이다.

Table Dispatch

동적 디스패치의 가장 기본적인 구현 방식이라고 한다. 위에서 Dynamic Dispatch를 설명했던 것과 동일하며, 함수 포인터로 이루어진 vTable 혹은 witness Table을 통해서 함수를 부르는 것이다. 모든 서브클래스는 각자의 vTable을 가지고 있으며 재정의한 모든 메소드에 대해 다른 함수 포인터를 가지고 있다. 서브클래스에서 새로운 메소드를 추가하면 해당 메소드에 대한 함수 포인터가 배열 끝에 추가된다. 컴파일러는 런타임에 이 테이블을 사용하여 어떤 메소드를 호출할지 결정한다.

class ParentClass {
	func method1() { }
   	func method2() { }
}

class ChildClass: ParentClass {
	override func method2() { }
   	func method3() { }
}

컴파일러는 ParentClassChildClass의 vTable을 생성할 것이고, 런타임시에 해당 vTable를 참고하여 타입에 맞는 함수 포인터를 따라 가서 호출하는 연산이 일어날 것이다.


Message Dispatch

  • Message Dispatch는 자기 자신이 오버라이드 하거나 새로 정의한 메소드들만 테이블에 유지한다.
  • 대신 부모 타입으로의 포인터를 가지고 있어서, 부모 타입의 메소드들은 부모 타입에서 찾아서 실행한다.
  • Cocoa 프레임워크에서도 KVO, 코어데이터와 같은 곳에서 사용된다.
    또한 이 방법은 런타임에 메소드의 기능을 바꾸는 method swizzling을 가능하게 한다고 한다.(메소드의 구현을 런타임에 동적으로 변경하는 행위)
  • Objective-C 런타임은 이를 위한 인터페이스를 제공하며, Swift에서도 마찬가지로 사용할 수 있다고 한다.



참고

jcsoohwancho - Swift의 Dispatch 규칙
Increasing Performance by Reducing Dynamic Dispatch - Swift Blog
DongMinyoon - Method Dispatch

profile
iOS Developer Student

0개의 댓글