ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Class와 Performance in Swift
    iOS 2023. 2. 2. 14:16

     

    Reducing Dynamic Dispatch

    Swift는 기본적으로 Objective-C처럼 동적인(dynamic) 언어입니다. 하지만 Objective-C와 달리 Swift에서는 프로그래머가 필요에 따라 dynamism을 제거하거나 줄여서 런타임 성능을 향상시킬 수 있습니다.

     

    Dynamic Dispatch

    클래스는 프로퍼티와 메서드에 접근하기 위해 기본적으로 동적 디스패치를 사용합니다. 따라서 아래와 같은 코드에서 동적 디스패치를 통해 a.aProperty, a.doSomething(), 그리고 a.doSomethingElse()에 접근합니다.

    class A {
        var aProperty: [Int]
        func doSomething() { ... }
        dynamic doSomethingEles() { ... }
    }
    
    class B: A {
        override var aProperty {
        	get { ... }
            set { ... }
        }
        
        override func doSomething() { ... }
    }
    
    func usingAnA(_ a: A) {
        a.doSomething()
        a.aProperty = ...
    }

    Swift에서, 기본적으로 동적 디스패치는 vtable을 통해 간접 호출합니다. 간접 호출을 수행하는 오버헤드 외에도 컴파일러의 최적화를 방해하므로 직접 호출하는 방식보다 속도가 느립니다. 따라서 성능이 중요한 코드에서는 동적인 동작을 제한하면 이점을 누릴 수 있습니다.

     

    vtable

    virtual method table 또는 vtable이란, 타입의 메서드 주소들을 가지고 있는 인스턴스가 참조하는 테이블입니다. 동적 디스패치는 우선 객체로부터 테이블을 찾은 다음, 테이블에서 메서드를 찾는 방식으로 진행됩니다.

     

    Advice: override하지 않을 것이라면 'final'을 사용하세요.

    final 키워드는 클래스, 메서드, 프로퍼티의 선언부에 사용해서 override할 수 없도록 제한할 수 있습니다. 이는 컴파일러가 간접 호출 대신 직접 호출이 가능해진다는 내용을 내포하고 있습니다.

    final class C {
      // No declarations in class 'C' can be overridden.
      var array1: [Int]
      func doSomething() { ... }
    }
    
    class D {
      final var array1: [Int] // 'array1' cannot be overridden by a computed property.
      var array2: [Int]      // 'array2' *can* be overridden by a computed property.
    }
    
    func usingC(_ c: C) {
      c.array1[i] = ... // Can directly access C.array without going through dynamic dispatch.
      c.doSomething() = ... // Can directly call C.doSomething without going through virtual dispatch.
    }
    
    func usingD(_ d: D) {
      d.array1[i] = ... // Can directly access D.array1 without going through dynamic dispatch.
      d.array2[i] = ... // Will access D.array2 through dynamic dispatch.
    }

    usingC(_ c:)에서, c.array1 프로퍼티와 c.doSomething() 메서드에 직접 접근합니다. 클래스 C가 final로 선언되어 있기 때문입니다.

    usingD(_ d:)에서, d.array1 프로퍼티에 직접 접근합니다. 프로퍼티가 final로 선언되어 있기 때문입니다. 하지만 d.array2 프로퍼티는 동적 디스패치를 통해서 호출됩니다.

     

    Advice: 외부에서 접근할 필요가 없을 때 'private'과 'fileprivate' 접근 제어 키워드를 사용하세요.

    접근 제어란 코드끼리 상호작용할 때 파일 간 또는 모듈 간에 접근을 제한할 수 있는 기능입니다. 객체 지향 프로그래밍에서 중요한 캡슐화와 은닉화라는 개념은 불필요한 접근으로 의도치 않은 결과를 초래하거나 꼭 필요한 부분만 제공해야 하는데 불필요한 코드가 노출될 가능성이 있으므로 접근 제어를 이용합니다.

     

    private

    private 접근 수준은 가장 한정적인 범위입니다. 그 기능을 정의하고 구현한 범위 내에서만 사용할 수 있습니다.

    fileprivate

    fileprivate 접근 수준으로 지정된 요소는 그 요소가 구현된 소스 파일 내부에서만 사용할 수 있습니다.

     

    private class E {
      func doSomething() { ... }
    }
    
    class F {
      fileprivate var myPrivateVar: Int
    }
    
    func usingE(_ e: E) {
      e.doSomething() // There is no sub class in the file that declares this class.
                      // The compiler can remove virtual calls to doSomething()
                      // and directly call E's doSomething method.
    }
    
    func usingF(_ f: F) -> Int {
      return f.myPrivateVar
    }

    위의 코드에서, 클래스 E는 private으로 선언되어 있고, 클래스 F의 myPrivateVar 프로퍼티는 fileprivate으로 선언되어 있으므로 다른 파일에서 접근하지 못합니다. 따라서 컴파일러는 같은 파일에 E와 F의 overriding 선언이 없는 것을 보고 final 키워드를 추론하게 되고, 직접 접근이 가능해집니다.

     

    그렇다면 앞의 내용을 토대로 다음과 같이 코드를 작성하면, 아마도 a.doSomething()은 vtable을 통해 간접 호출을 하게 될 것 같습니다.

    private class A {
        private let myProperty: Int
        
        init(myProperty: Int) {
            self.myProperty = myProperty
        }
        
        func doSomething() {
            print("Hello")
        }
    }
    
    private class B: A { // private final class
        fileprivate let myProperty2: Int
        
        init(myProperty: Int, myProperty2: Int) {
            self.myProperty2 = myProperty2
            super.init(myProperty: myProperty)
        }
        
        override func doSomething() {
            print("World")
        }
    }
    
    private func usingA(_ a: A) {
        a.doSomething() // A --> B(overriding), 따라서 vtable을 통한 간접 호출
    }
    
    private func usingB(_ b: B) -> Int {
        b.doSomething() // 직접 접근
        return b.myProperty2 // 직접 접근
    }
    
    private let a = A(myProperty: 5)
    private let b = B(myProperty: 8, myProperty2: 10)
    
    usingA(a)
    usingB(b)
    
    print(a.myProperty, b.myProperty) // 접근 불가

    원문

    Apple github document

    (https://github.com/apple/swift/blob/main/docs/OptimizationTips.rst)

     

    참고

    스위프트 프로그래밍 3판(야곰 지음) - 접근 제어

    댓글

Designed by Tistory.