본문 바로가기
ios/swift

클래스의 성능을 높이는 방법

by kimyounggyun 2022. 9. 28.

정적 디스패치(Static Dispatch)와 동적 디스패치(Dynamic Dispatch)

스위프트는 C++, 자바 등 많은 언어들처럼 클래스 상속을 지원하고, 슈퍼 클래스를 상속한 서브 클래스에서 프로퍼티와 메서드 오버라이딩을 할 수 있다. 따라서 프로그램은 런타임 단계에서 동일한 이름의 메서드들 중 어떤 메서드를 실행시켜야 할지 결정해야 한다. 이 과정을 동적 디스패치(Dynamic Dispatch)라고 한다. 스위프트의 동적 디스패치는 기본적으로 vtable를 참조하는 간접 호출(Indirect Call)이다. 따라서 컴파일 단계에서 호출될 함수를 결정하여 직접 호출(Direct Call)하는 정적 디스패치(Static Dispatch) 보다 느리다.

 

class A {
  var aProperty: [Int]
  func doSomething() { ... }
  dynamic doSomethingElse() { ... }
}

class B: A {
  override var aProperty {
    get { ... }
    set { ... }
  }

  override func doSomething() { ... }
}

func usingAnA(_ a: A) {
  a.doSomething()
  a.aProperty = ...
}

- a.aProperty, a.doSomething(), a.doSomethingElse() 모두 동적 디스패치가 발생한다. -

 

값 타입(Value Type)인 구조체와 열거형은 상속이 불가능하므로 메서드 호출이 정적 디스패치로 이루어진다. 하지만 참조 타입인 클래스는 상속을 지원하므로 오버라이딩의 가능성이 있어 동적 디스패치가 사용된다. 프로그램을 작성하면서 모든 클래스가 무조건 상속을 하는 것은 아니다. 그렇다면 클래스의 프로퍼티나 메서드가 오버라이딩의 가능성이 없다는 것을 컴파일러에게 알려준다면 정적 디스패치가 사용되지 않을까?

동적 디스패치를 줄여 클래스 성능을 높여보자.

스위프트는 Objective-C처럼 매우 동적인 언어이다. Objective-C와 다르게 스위프트는 런타임 과정에서 일어나는 동적임(Dynamism)을 줄여 런타임 성능을 향상할 수 있다.

상속이 필요 없는 클래스는 'Final' 키워드를 사용하자.

final 키워드는 해당 클래스가 상속될 수 없음을 나타낸다. 상속될 수 없으므로 오버라이딩이 불가능하다. 따라서 컴파일러는 해당 클래스의 프로퍼티와 메서드를 직접 호출한다. 아래 코드에서 C.array1D.array1은 직접 호출되지만 D.array2는 vtable를 참조하여 간접 호출된다.

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.
}

'private', 'fileprivate'를 사용해 가시성(visibility)를 제한하자.

privatefileprivate 사용하면 프로퍼티와 메서드의 가시성이 현재 파일로 제한된다. 이를 통해 컴파일러는 파일 내에서 잠재적으로 가능한 오버라이딩의 가능성을 찾을 수 있고 만약 찾지 못했다면 final 키워드를 자동으로 유추하고 프로퍼티와 메소드를 직접 호출로 바꾼다.

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
}

WMO(Whole Module Optimization)을 사용한다면 'internal' 키워드를 사용하자.

스위프트는 모듈의 파일들을 개별적으로 컴파일하기 때문에 다른 모듈에서 상속해 오버라이딩을 한다면 컴파일 과정에서 발견할 수 없다. 하지만 WMO을 사용하면 모듈 전체에 가시성을 갖게 된다. 만약 internal 키워드를 사용한다면 파일의 가시성이 내부 모듈로 제한되어 컴파일러는 모듈 내 잠재적인 오버라이딩의 가능성이 없다면 final 키워드를 유추하고 직접 호출로 바꾼다. 스위프트는 기본 접근 제한이 internal이기 때문에 WMO만 활성화시키면 된다.

Reference

댓글