-
[Swift] Method dispatch와 V-TableiOS/Swift 2022. 8. 20. 22:11
iOS 개발자라면 꼭 시청해야 한다고 생각하는 WWDC 세션인 Understanding Swift Performance의 내용 중
Method dispatch에 대해서 공부하며 적은 글입니다.
출처는 알아보기 쉽도록 각 단락마다 적어놓았습니다.
Static Dispatch
컴파일 타임에, 실행될 구현체를 결정할 수 있습니다.
런타임 시 올바른 구현체로 직접 jump 할 수 있습니다. (메모리 상에서 해당 코드의 주소로 이동하는 것이 jump로 표현되는 듯하다. 어셈블리어와 비슷한 느낌)
이 경우 컴파일러가 어떤 구현체가 실행될 것인지에 대해서 가시성을 가질 수 있습니다. (어떤 구현체가 실행될지 안다)
또, inlining과 같은 매우 적극적인 최적화 기법을 사용할 수 있습니다.
Dynamic Dispatch
컴파일 타임에, 실행될 구현체를 결정할 수 없습니다.
따라서 런타임 시 구현체를 찾고, 해당 구현체로 jump 합니다.
Dynamic dispatch 그 자체는 static보다 그렇게 비용이 많이 들지는 않습니다.
왜냐하면 참조가 한 단계밖에 없고, reference counding이나, heap allocation에서 발생하는 스레드 동기화 오버헤드가 없기 때문입니다.
하지만, dynamic dispatch는 컴파일러의 가시성을 차단하므로, static에서 할 수 있는 적극적인 최적화 기법(inlining 등)을 사용할 수 없습니다.
여기서 말하는 inlining이란?
아래 예제를 보면 쉽게 이해할 수 있습니다.
이렇게 구조체로 되어있을 때, drawAPoint 함수를 그 구현체로 대체하고
point.draw메서드를 실제 구현체로 대체하여
함수의 단계적인 호출 없이, 해당 구현체를 코드 안에 넣어 대체하는 것을 inlining이라고 합니다.
이를 통해서 위 예제에서는 두 가지 static dispatch overhead(이게 뭘까요?)와 관련된 call stack을 설정하고 해제하는 작업을 하지 않을 수 있습니다.
중간 정리
이것이 바로 dynamic diapatch보다 static이 빠른 이유라고 할 수 있습니다.
하지만 single static dispatch와 single dynamic dispatch은 그렇게 차이가 나지 않습니다.
다른 점은 static dispatch는 컴파일러가 전체 체인을 통해서 가시성을 가질 수 있다는 점입니다. (위의 예제에서의 함수 호출의 연속적인 인과관계를 체인이라고 하는 것 같다.)
이를 통해서 컴파일러는 static dispatch의 체인을 축소하여 마치 하나의 구현체처럼 만드는데, call stack overhead가 없어진다는 장점이 있습니다.
반면에 dynamic dispatch는 상위 레벨에서 차단되어 매 단계마다 추론을 하지 못합니다. ( 아마 위의 예제처럼 단계마다 추론을 통해서 구현체로 대체해야 하는데, 여기서는 첫 단계에서부터 추론을 할 수 없다는 뜻인 것 같다.)
Dynamic Dispatch의 필요성 - 다형성(Polymorphism)
Drawable 클래스와, 이를 상속받는 Point, Line 클래스가 있습니다.
그리고 Drawable을 담고 있는 배열을 만들었습니다. Drawable은 클래스라서 배열은 참조를 저장합니다.
배열의 요소들은 각자의 draw를 호출할 것입니다.
여기서 컴파일러가 왜 컴파일 타임에 실행할 적절한 구현체를 결정하지 못하는지 이해할 수 있습니다.
왜냐하면 d.draw는 point.draw가 될 수도 있고, line.draw가 될 수도 있기 때문입니다.
(그니깐, 프로그램이 동작하면서 배열에 이것저것 넣을 수 있기 때문에 컴파일 타임에는 어떤 요소가 배열에 들어있는지 절대 결정할 수 없다는 뜻인 것 같다.)
런타임 시 실행할 적절한 구현체를 결정하는 방법은 이렇습니다.
일단, 컴파일러는 클래스 필드에 type의 정보를 저장합니다. 이 정보는 static memory에 저장되어 있습니다.
그리고 우리가 draw를 실행했을 때, 컴파일러가 생성한 virtual method table에서 적절한 구현체를 조회합니다.
그리고 실제 인스턴스를 암시적 self 매개변수로 전달합니다.(???)
virtual table
먼저 컴파일러에 의한 data flow를 보면 아래와 같습니다.
여기서, SILGen에 의해서 생성된 Swift Intermediate Language(SIL)에 virtual table과 관련된 내용이 있습니다.
SIL은 Swift Type, referece counting, dispatch rule을 이해고 계산하기 위한 basic block을 포함하고 있습니다.
위 코드를 SIL로 변환하고, 보기 쉽게 나타내면 오른쪽 사진과 같습니다.
Smith클래스의 v-table을 보면 각 줄은, 부모 클래스인 Agent의 메서드로 시작하고, Smith에서 구현된 메서드로 끝납니다.
따라서 override 된 jump메서드만 다른 것을 볼 수 있습니다.
또한, final로 정의된 block메서드는 vtable에서 볼 수 없습니다. (매핑이 필요 없기 때문에)
이로서 컴파일 타임에 v-table이 만들어지고, static memory에 만들어진다는 것을 확인할 수 있었습니다.
정리
- polymorphism을 위해서 dynamic dispatch를 사용하고 있습니다.
- v-table을 통해 런타임에 적절한 메서드를 선택해 실행합니다.
- final 키워드를 사용하면 v-table에 포함되지 않게 되고, 최적화가 가능합니다.(하지만 굳이 final키워드를 추가하지 않아도 WMO을 사용한다면 컴파일러가 추론을 통해 final, private키워드를 추가해줍니다.) -> 즉 class의 메서드라도 dynamic dispatch가 아닌 static dispatch로 동작합니다.
- 인스턴스 메소드를 호출할 때, static / dinamic dispatch중 어떤 것에 해당하느냐에 따라서 비용과 속도가 달라집니다.
- dynamism이 필요하지 않는데 비용을 지불하고 있다면, 성능을 갉아먹고 있는 것입니다.
참고
'iOS > Swift' 카테고리의 다른 글
[Swift] Generic에서 Method Dispatch (1) 2022.08.22 [Swift] Protocol에서 Method dispatch (0) 2022.08.21 [백준] 18258 큐 2 - Swift (0) 2022.06.10 [Swift Grammar] Guard구문에서의 non-Optional 선언 (0) 2022.05.11 [TIL] 참조와 캡쳐 (0) 2022.01.11