Swift

Swift - Dispatch를 이용한 성능 개선

Goniii 2024. 12. 9. 16:15

디스패치(Dispatch)

  • 프로그램이 메서드나 함수를 호출할 때 호출 대상(함수, 메서드)을 어떤 방식으로 결정하고 실행할지를 정의하는 메커니즘
  • 성능 최적화, 코드 실행 방식, 다형성과 밀접하게 관련됨

 

Static Dispatch vs Dinamic Dispatch

1. 정적 디스패치 (Static Dispatch)

  • 메서드나 함수의 호출이 컴파일 타임에 결정됨
  • 주로 값 타입(struct, enum)이나 final로 선언된 클래스의 메서드에서 사용
  • 컴파일러가 호출할 메서드를 미리 결정하므로 인라인(inline) 최적화가 가능하며, 호출 속도가 매우 빠름

→ 컴파일 시점에 호출 대상이 결정되고 런타임에서 바로 실행되기 때문에 성능이 우수함

 

2. 동적 디스패치 (Dynamic Dispatch)

  • 메서드나 함수의 호출이 런타임에 결정됨
  • 클래스의 상속 구조에서 메서드 오버라이딩과 같은 다형성을 지원하기 위해 사용
  • 가상테이블(vtable)을 통해 호출할 메서드를 런타임에 확인하고 실행함
    • vTable(Virtual Method Table) : 클래스마다 함수 포인터 배열을 생성해 메서드 참조를 관리하는 구조

→ 호출될 때마다 vTable을 조회하므로 Static Dispatch보다 성능이 떨어짐

 

Static Dispatch와 Dynamic Dispatch의 차이 정리

디스패치 방식
결정 시점 대상 속도 다형성 지원
정적 디스패치
(Static Dispatch)
컴파일 타임 값 타입, final 메서드 매우 빠름 지원하지 않음
동적 디스패치
(Dynamic Dispatch)
런타임 클래스 메서드(특히 오버라이딩) 느림 지원

 

왜 Dynamic Dispatch가 필요한가?

  • Dynamic Dispatch는 클래스 상속 구조에서 다형성을 지원하기 위해 필수적임
  • 클래스는 “오버라이딩(상속)” 가능성이 있기 때문에 런타임에서 호출할 메서드를 결정
class Pet {
    func printName(){
        print("I'm Pet")
    }
}

class Cat : Pet {
    override func printName() { // 오버라이딩
        print("I'm Cat")
    }
}

let pet : Pet = Cat()
pet.printName() // "I'm Cat" (동적 디스패치)
  • pet 변수의 타입은 Pet이지만, 실제 인스턴스는 Cat
  • 위와 같이, 상위 클래스의 메서드가 하위 클래스에서 오버라이딩이 될 경우, 하위 클래스의 메서드를 사용해야 되기 때문에 어떤 메서드를 사용할지 결정하는 작업을 런타임에서 함성능 저하
  • 런타임 시점에 Cat의 printName() 메서드를 호출해야 하므로, vTable을 조회하여 적절한 메서드를 찾아 실행함

→ 클래스의 다형성을 지원하는 오버라이딩을 사용하기 위해 Dynamic Dispatch는 반드시 필요함

 

 

성능 최적화를 위한 Static Dispatch 사용법

  • 클래스를 사용할 때 오버라이딩이 필요 없는 경우, Static Dispatch를 통해 성능 개선 가능

1. final 키워드

  • final 키워드 : 해당 클래스, 메서드, 프로퍼티가 상속이 불가능하고 하위 클래스에서 오버라이딩 될 수 없음을 명시하는 키워드
final class Pet {
    func printName(){
        print("I'm Pet")
    }
}

class Cat : Pet { // Error : Inheritance from a final class 'Pet
    override func printName() { // Error : Instance method overrides a 'final' instance method
        print("I'm Cat")
    }
}
  • final로 선언된 Pet 클래스는 상속이 불가능!
  • 상속이 불가능하다면, 오버라이딩 가능성이 없기 때문에 컴파일러가 Static Dispatch로 동작하여 최적화 가능

 

2. Private 키워드

  • Private : 접근 제한자로, 지정된 요소는 구현한 범위 내에서만 사용할 수 있음
    • Private으로 선언된 메서드는 오버라이딩이 불가능함
    • 컴파일러는 private 키워드로 지정된 요소가 참조될 수 있는 곳에서 오버라이딩이 되는지 판단!
    • 만약, 오버라이딩 되는 곳이 없다면 final 키워드로 추론해서 Static Dispatch로 동작함
    class Pet {
        private func printName(){ // 오버라이딩 가능성이 없으므로 Static Dispatch로 동작
            print("I'm Pet")
        }
    }
    
    class Cat : Pet {
        override func printName() { // Error : Method does not override any method from its superclass
            print("I'm Cat")
        }
    }
    
    → private으로 지정된 printName()메서드가 참조 될 수 없는 곳이므로 오버라이딩 불가능

 

 

오버라이딩 여부에 따른 Static vs Dynamic Dispatch

class Pet {
    private var name = "Pet"   // 참조 가능한 곳에서 오버라이딩 안 됨 -> Static Dispatch
    private func printName(){  // 참조 가능한 곳에서 오버라이딩 됨 -> Dynamic Dispatch
        print("I'm Pet")
    }
    
    class Cat : Pet {
        override func printName() {  // 참조 가능한 곳에서 오버라이딩
            print("I'm Cat")
        }
    }
}
  • private으로 지정된 printName() 메서드가 참조될 수 있는 곳에서 오버라이딩 됨 → Dynamic Dispatch로 동작
  • private으로 지정된 name 프로퍼티는 참조될 수 있는 곳에서 오버리이딩이 되지 않으므로 final 키워드로 추론 → Static Dispatch로 동작

 

결론

  • Static Dispatch는 컴파일 타임에 호출 대상이 결정되며, 성능 우수
  • Dynamic Dispatch는 클래스의 다형성을 지원하며, 상속 구조에서 오버라이딩 메서드를 호출하기 위해 필수적
  • 성능이 중요한 경우, final 또는 private 키워드를 활용하여 Static Dispatch로 최적화 가능