[Swift] Method Dispatch 1편

어흥·2024년 6월 18일

Swift

목록 보기
24/28

⁉️ Swift에서 함수를 호출할 때 어떤 방식으로 실행되며 관리되는가? 로 부터 시작된 물음

개발자는 함수를 사용할 때, 어떤 함수가 실행할 때 빠른지에 대해서 고민하며 함수의 종류를 선택할 필요가 있음

→ 이를 위해서, method dispatch에 대해서 알아야 함

🤿 Let’s Dive into Method Dispatch

Method Dispatch

공식문서가 아닌 참고한 블로그에서 가져온 정의

Method dispatch is the algorithm used to decide which method should be invoked in response to a message. The goal of dispatch is for the program to inform the CPU where it can find the executable code for a particular method call in memory.

Method Dispatch는 함수 호출 메시지에 대한 응답으로 어떤 메서드를 실행할지 결정하는 알고리즘을 말한다. 메서드 디스패치의 목적은 특정 메서드 호출이 발생했을 때 CPU에게 메모리에서 실행할 코드의 위치를 알려주는 것이다.

즉, 메서드가 호출될 때 어떤 코드를 실행할 지 결정하는 알고리즘이 메서드 디스패치(method dispatch)라고 할 수 있다.

⁉️ 왜?? 함수 호출될 때, 어떤 코드를 실행할지 결정해야하는거지????

  1. 상속된 클래스인 경우, 부모 클래스에서 메서드를 상속받고 해당 메서드에 대해서 재정의가 가능하다.
    • 자식 클래스 인스턴스가 메서드를 호출했을 때, 어떤 메서드를 실행해야 하는가?
      • 재정의된 메서드는 자식클래스에서 정의된 메서드 코드 실행
      • 재정의가 되지 않은 메서드는 부모클래스에서 정의된 메서드 코드 실행
  2. 구조체는 상속이 불가능하므로 위의 경우를 고려하지 않아도 될 것이다.
  3. 프로토콜
    • 프로토콜 채택한 클래스의 인스턴스가 프로토콜 본체의 요구사항을 구현한 메서드를 호출한 경우
    • 클래스의 인스턴스가 프로토콜 확장에서 구현한 메서드를 호출할 경우

위에서 나열한 경우들은 각각 다른 방식을 가지고 실행할 코드 위치를 결정하기 때문에 동일한 알고리즘을 사용하면 안된다.

따라서 각각의 경우 다른 Method Dispatch 방식을 갖는데 Method Dispatch 방식은 총 3가지가 있다.

Direct dispatch (Static)

direct dispatch는 가장 빠른 method dispatch 방식이다.

  • 어떤 코드를 실행해야 하는지 너무나 명확할 때 사용
    • value type(구조체, 열거형) → 상속 없음
  • 어떤 코드를 실행할 지 이미 명확하므로 컴파일 시점에 함수의 메모리 주소나 code inlining(함수의 실행 코드를 함수 호출 위치에 심음) 한다.
  • 상속관계에서 서브 클래스에서 사용할 수 없다. → 다형성 활용에 어려움

예시

struct SomeStruct {
    func method1() { print("Struct - Direct method1") } // 함수 주소 -> 101 (간단히 가정)
    func method2() { print("Struct - Direct method2") } // 함수 주소 -> 111
}

위와 같이 SomeStruct에서 method1, method2를 정의했다.

아래와 같이 method1, method2 를 호출하면 어떤 코드를 실행하는지 결정되는지 확인해보자.

  • direct dispatch는 실행할 코드의 메모리 주소를 삽입하거나 실행할 코드를 심는 방식
let myStruct = MyStruct()
myStruct.method1() 
myStruct.method2()

이렇게 호출하면, 컴파일 시점에 어떤 코드를 실행할지 결정했을 것이다.

let myStruct = MyStruct()
myStruct.method1() // 101번 함수 주소 삽입 
myStruct.method2() // print("Struct - Direct method2") 코드 삽입

direct dispatch 방식은 가장 제한적인 dispatch 방식이다. 함수 주소나 코드를 삽입하여 동적이지 않아 서브 클래싱을 지원할 수 없기 때문이다.

Table dispatch (Dynamic)

Table Dispatch는 함수를 table로 관리하여 상황에 따라 어떤 함수를 실행할지 동적으로 결정하는 방식이다.

direct dispatch와 달리 동적으로 실행할 함수를 결정할 수 있어 서브클래싱 지원!!! → 다형성 활용, 레퍼런스 타입 인스턴스가 메서드를 호출할 때

  • 런타임에 호출될 함수 결정
  • Swift 에서는 클래스마다 Virtual Table(Virtual Dispatch Table) 유지
    • vtable: 함수 포인터들의 배열로 표현되며, 하위 클래스가 메소드를 호출할 때 이 배열을 참조하여 실제 호출할 함수를 결정

→ direct dispatch와 달리 호출할 함수가 런타임에 결정되므로 추가적인 연산이 필요하고 속도가 비교적 느리다.

아래와 같이 상속관계를 갖는 ParentClass, ChildClass가 있다.

class ParentClass{
    func method1() { print("Class - Table method1") }
    func method2() { print("Class - Table method2") }
}

class ChildClass: ParentClass {
    override func method2() { print("Class - Table method2-2") }
    func method3() { print("Class - Table method3") }
}

ParentClass, ChildClass가 아래와 같이 Virtual Table를 갖는다. (vtable은 함수 포인터들의 배열)

ChildClass vtable

  • method1: ParentClass에서 상속받으므로 ParentClass에서 정의된 주소 저장 (재정의된게 없음)
  • method2: ChildClass에서 재정의했으므로 ChildClass에서 정의된 주소 저장
  • method3: ChildClass에서 정의된 주소 저장

let child = ChildClass()
child.method2()

child.method2() 에서 메서드가 호출될 때 발생하는 과정은 다음과 같다. (그림 참고)

  1. 0xB00에 있는 childObject의 디스패치 테이블을 읽는다.

  2. 메서드의 인덱스를 읽는다.

    이 경우, method2의 메서드 인덱스는 1이므로 0xB00 + 1 주소를 읽는다.

  3. 0x222 주소로 점프한다.

  4. 해당 영역에서 저장된 함수 주소로 점프하여 함수 실행

table dispatch 방식은 direct dispatch 방식에 비해 2번의 추가적인 절차가 필요하여 오버헤드가 발생할 수 있고 속도가 느리다. (블로그를 보면 컴파일러가 메서드 내부에서 일어나는 일을 기반으로 최적화를 수행하므로 느리다는데 아직 잘 이해는 안간다.)

일반적으로 reference type 인스턴스가 메서드를 호출할 때 dynamic dispatch 방식으로 이루어지는데, 항상 그런 것은 아니다.

class에서 extension 기능이 있는데 이러한 경우는 어떠할까? 항상 궁금했던 것이 확장에서 구현한 메서드는 상속했을 때 왜 재정의가 안될까라는 의문이 있었다.
method dispatch 방식을 살펴보면 그 의문을 해결할 수 있다!!!

Class의 Extension에 대한 method dispatch

class의 Extension에 속한 메서드가 호출될 때 Direct Dispatch 형식으로 진행됩니다. Direct Dispatch 방식은 컴파일 되었을 때 코드 영역의 메모리 주소만 전달하면 되는 거였습니다.

상속이 가능한 건 컴파일 되었을 때 코드 영역의 메모리 주소만 전달하면 되므로 확장의 메서드도 실행할 함수의 주소를 알게되므로 메서드도 상속이 가능합니다.

다만 재정의가 불가능한 건, 메서드 테이블에서 동작하는게 아니고 Direct로
상속이 가능한건 그냥 컴파일 되었을때의 코드 영역의 메모리 주소만 전달하면 되는거니까 (어떤 함수를 실행시킬지) 확장의 메서드도 상속이 가능한거죠.
다만 재정의가 불가능한건, 메서드 테이블에서 동작하는게 아니고 항상 Direct로 동작하니까 (새로운 테이블을 만들 수 없으니) 재정의를 할 수 없는 것이다.

Message dispatch

Message dispatch 방식은 주로 Objective-C 에서 사용되는 방식이다.

  • 런타임에 상속 구조를 스캔하며 어떤 부모 클래스의 메서드를 사용할지 결정
  • 런타임에 호출할 메서드를 결정하기 위해 클래스 계층 구조를 탐색하므로 가장 느림

Swift에서 메서드 디스패치 방식을 이용하고 싶다면 dynamic 키워드를 사용하면 된다.

class ParentClass {
    @objc dynamic func method1() { print("Class - Message method1") }
    @objc dynamic func method2() { print("Class - Message method2") }
}

class ChildClass: ParentClass {
    @objc dynamic override func method2() { print("Class - Message method2-2") }
    @objc dynamic func method3() { print("Class - Message method3") }
}

Reference.

Method Dispatch in Swift
Method Dispatch in Swift, and its effect on performance
wwdc2016_Understanding Swift Performance
Swift의 Dispatch 규칙
앨런 Swift문법 마스터 스쿨

0개의 댓글