ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Swift] Method dispatch와 V-Table
    iOS/Swift 2022. 8. 20. 22:11

    iOS 개발자라면 꼭 시청해야 한다고 생각하는 WWDC 세션인 Understanding Swift Performance의 내용 중

    Method dispatch에 대해서 공부하며 적은 글입니다.

    출처는 알아보기 쉽도록 각 단락마다 적어놓았습니다.

    UnderStanding Swift Performance는 5가지의 소주제로 나뉘어졌다.

    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

    출처 : https://www.raywenderlich.com/627-new-course-advanced-swift-3

    먼저 컴파일러에 의한 data flow를 보면 아래와 같습니다.

    출처 : https://www.raywenderlich.com/books/expert-swift/v1.0/chapters/1-introduction#toc-chapter-006-anchor-004

    여기서, 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이 필요하지 않는데 비용을 지불하고 있다면, 성능을 갉아먹고 있는 것입니다.

     

    참고

    https://zeddios.tistory.com/596

    '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

    댓글

Designed by Tistory.