-Today's Learning Content-

  • Dynamic Dispatch와 Static Dispatch
  • Dynamic Dispatch with Swift

1. Method Dispatch

개념 정리

Method Dispatch란 특정 함수(혹은 메소드)를 호출할 때 어느 곳에서 호출할 것이며 어떤 함수를 호출할 것인지 결정하는 방식이다.

1) Method Dispatch가 뭐야?

오늘은 스터디 동료에게 Static Dispatch라는 키워드를 듣고 애플 블로그에서 이에 대한 지식을 접할 수 있었다.

위 블로그에서는

Increasing Performance by Reducing Dynamic Dispatch
This blog post showcases three ways to improve performance by eliminating such dynamism: final, private, and Whole Module Optimization

즉, 동적 디스패치를 줄여서 성능향상을 기대하는 내용에 대해 정리되어 있었다.

그럼 동적 디스패치는 뭐지?🤔

궁금한게 너무 많다. 그래서 오늘은 Swift에서 말하는 디스패치가 무엇인지, 성능 향상을 위해 무엇을 할 수 있는지에 대해 작성해볼 예정이다.

2) Dynamic Dispatch with Swift

디스패치란 뭘까? 사전에서는 디스패치를 아래와 같이 정의한다.

음.. 그렇다면 메소드 디스패치는 메소드의 호출방식에 대한 내용이겠지? 더 자세히 말하자면 어느 타이밍에, 어느 함수를 전달할지에 대한 내용이다.

사실 이 부분은 프로그램이 자동으로 해주기 때문에 이게 무슨 의미가 있나 의문이 생길 수 있다. 메소드의 전달 타이밍은 프로그램이 자동으로 지정하기도 하지만, 개발자가 직접 결정할 수도 있다. 그리고 이러한 결정들은 Runtime Performanc에 직접적인 영향을 미치게 된다.

즉, 메소드의 전달 타이밍(Dispatch)을 어떻게 결정하냐에 따라 프로그램의 성능에 크고 작은 영향을 준다는 의미이다.

메소드 디스패치는 크게 두 종류가 있다.
정적 디스패치(Static Dispatch)동적 디스패치(Dynamic Dispatch)로 구분되는데, 이 둘은 어떤 차이가 있을까?

우선 정적 디스패치부터 살펴보자.

Stacit Dispatch

값 타입인 struct(구조체), enum(열거형) 등 상속이 불가능한 타입들은 정적 디스패치로 동작한다. 상속이 불가능 하다는 것은 재정의(override) 또한 불가능하다는 것을 의미한다.

이 때문에 해당 타입들의 메소드는 어디서 어떤 메소드를 호출할 것인지 컴파일을 할 때 정해지고 고정되게 된다.

그래서 런타임 중 해당 타입의 메소드를 실행할 경우에는 이미 정해진 메소드를 직접 호출(direct call)하여 사용하므로 속도가 빠르고 프로그램 성능에 긍정적인 영향을 미치게 된다.
직접호출 예시

Dinamic Dispatch

동적 디스패치는 정적 디스패치와 반대로 상속이 가능한 타입 즉, 재정의(override)가 가능한 타입들이 동적 디스패치로 작동한다. 외에도 @objc를 사용한 메소드나 클로저도 동적 디스패치로 작동한다.

이러한 타입들은 컴파일 때 메소드의 디스패치가 정해지지 않고, VTable에 배열로 저장된다. 그리고 런타임 중 메소드를 호출하면 Method Table에서 해당 메소드와 프로퍼티 등을 찾고 간접호출(indirect call) 방식으로 호출하게 된다.

이 때문에 당연히 직접호출 방식보다 느리고, 컴파일러의 최적화에도 부정적인 영향을 미치게 된다.
간접호출 예시

VTable(Virtual Table) 이란?
클래스의 메소드 호출을 효율적으로 관리하기 위한 데이터 구조로, 런타임에 동적 디스패치를 구현하는 핵심 메커니즘이다.

  • 클래스의 메소드에 대해 런타임에 호출할 메소드를 동적으로 결정하기 위해 사용
  • 각 클래스는 메소드 포인터의 테이블(VTable)을 유지하고, 런타임에 이 테이블을 참조

Static Dispatch vs Dynamic Dispatch

FeatureStatic DispatchDynamic Dispatch
결정 시점컴파일 시점런타임 중
성능빠름(인라인 최적화 가능)상대적으로 느림(VTable 참조 필요)
유연성제한적(상속, 오버라이드 불가)높은 유연성(다형성 구현)
사용 사례구조체, 열거형 메소드, final 메소드클래스 메소드(특히 @objc 메소드)

3) Swift와 Dispatch

Swift에서는 기본적으로 Dynamic Dispatch 방식을 사용하고 있다. 왜 성능에 유리한 정적 디스패치가 아닌 동적 디스패치를 사용할까?

그것은 Swift가 객체 지향(OOP) 언어들과 마찬가지로 다형성을 지원하기 때문이다. Swift는 클래스를 통해 상속과 재정의를 가능하게 하였고, 서브 클래스가 부모 클래스의 프로퍼티나 메소드를 그대로 사용할 수도 있다. 덕분에 다양한 표현과 확장성이 있는 프로그래밍이 가능해졌지만, 이를 위해 동적 디스패치 방식을 사용해야 했고, Runtime Performance의 성능 감소를 감수해야 했던 것이다.

이처럼 동적 디스패치는 다형성이 필요한 경우를 제외하고는 사용을 지양하는 것이 프로그램 성능에 좋은 영향을 미칠 수 있다고 생각할 수 있다.

그렇다면 성능향상을 위해 무엇을 할 수 있을까?
지금부터 알아가보도록 하자.


2. Reducing Dynamic Dispatch

개념 정리

Apple에서는 다음의 방식들을 통해 동적 디스패치를 줄이는 것을 권장하고 있다.

1) final을 이용한 성능 향상

Use final when you know that a declaration does not need to be overridden.

우리는 final 키워드를 통해 더이상 해당 클래스가 상속과 재정의를 불가하도록 설정할 수 있다. 이것은 컴파일러에게 해당 클래스가 동적 디스패치를 필요로하지 않다고 명시적으로 설명하여 동적 디스패치를 안전하게 제거할 수 있다.

예시로 확인해보자.

class ParticleModel {
    var point = (x: 0.0, y: 0.0)
    var velocity = 100.0
    
    func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }
    
    func update(newP: (Double, Double), newV: Double) {
        updatePoint(newPoint: newP, newVelocity: newV)
    }
}

위의 코드에서 ParticleModel이라는 클래스에는 2개의 프로퍼티와 2개의 메소드가 존재한다. 이 때, 클래스 내의 모든 값들은 재정의 가능성이 있기 때문에 모두 Dynamic Dispatch를 통해 호출되게 된다.

이제 다음 코드를 보자.

class ParticleModel {
    final var point = (x: 0.0, y: 0.0)
    final var velocity = 100.0
    
    final func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }
    
    func update(newP: (Double, Double), newV: Double) {
        updatePoint(newPoint: newP, newVelocity: newV)
    }
}

위 코드에서는 update()는 재정의가 가능하여 Dynamic Dispatch를 통해 호출되지만, 다른 값들은 final을 붙여 재정의가 되지 않기 때문에 컴파일 시점에서 해당 값들의 위치를 정의하게 된다.
때문에 당연히 직접 호출하는 방식으로 변하게 되고, 오버헤드가 줄어 성능이 향상된다.

클래스 자체에 final을 적용하는 방식도 사용 가능하다. 이 때, 클래스 내에 구현된 모든 프로퍼티와 메소드들은 직접 호출 방식으로 변경되고 재정의가 불가능 해진다.

final class ParticleModel {
    var point = (x: 0.0, y: 0.0)
    var velocity = 100.0
    // ...

2) private를 이용한 성능 향상

Infer final on declarations referenced in one file by applying the private keyword.

private 접근제한자는 키워드를 적용함으로써 키워드가 적용된 대상을 참조할 수 있는 범위를 현재 코드블록 내로 제한할 수 있다.
때문에 컴파일러는 private 키워드가 참조될 수 있는 곳에서 잠재적으로 재정의가 가능한지 판단하고, 재정의 하는 곳이 없다면 자동으로 final을 추론하여 Dinamic Dispatch를 제거할 수 있다.

// 접근제한자 사용 예시
class ParticleModel {
    private var point = ( x: 0.0, y: 0.0 )
    private var velocity = 100.0

    private func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    func update(newP: (Double, Double), newV: Double) {
	updatePoint(newP, newVelocity: newV)
    }
}

final 때와 마찬가지로 클래스 자체에 접근제한자를 설정하여 사용하여도 문제 없다.

private class ParticleModel {
    var point = (x: 0.0, y: 0.0)
    var velocity = 100.0
    // ...

3) WMO(Whole Module Optimization)

Use Whole Module Optimization to infer final on internal declarations.

Swift의 컴파일러는 기본적으로 모듈별 컴파일을 진행한다. 때문에 internal access level(default)은 정의된 모듈 내에서만 접근이 가능하게 된다.
그리고 internal access level에 대해서 서로 다른 파일에서 override 되었는지 확인이 불가능하다.

하지만 WMO를 사용하면 모든 모듈을 한번에 컴파일하게 되고, internal access level에 대해 재정의가 되는지 추론을 할 수 있게 된다. 때문에 만약 재정의가 되지 않는다면 내부적으로 final을 사용하게 된다.

즉, 만약 컴파일러가 파일을 하나하나 컴파일할 경우 각 파일만 검사하므로, A 파일에 있는 클래스가 B 파일에서 재정의 되는지를 파악할 수 없기 때문에 Dynamic Dispatch로 동작하게 된다.
그러나 WHO를 사용하면 모든 모듈을 검사하고 컴파일 하기 때문에 재정의의 여부를 파악할 수 있고, 재정의가 되지 않는 경우 불필요하게 Dynamic Dispatch를 사용하지 않고 Static Dispatch로 동작하게 된다.

단, 이는 internal access level일 때의 상황으로 만약 접근 제한 레벨이 open일 경우 외부 모듈에서 참조, 상속이 가능하기 때문에 WHO를 적용하여도 Dynamic Dispatch로 동작하게 된다.

public class ParticleModel {
    var point = ( x: 0.0, y: 0.0 )
    var velocity = 100.0

    func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
	point = newPoint
	velocity = newVelocity
    }

    public func update(newP: (Double, Double), newV: Double) {
	updatePoint(newP, newVelocity: newV)
    }
}

var p = ParticleModel()
for i in stride(from: 0.0, through: times, by: 1.0) {
    p.update((i * sin(i), i), newV:i*1000)
}

위 예시에서 클래스에 public 접근제한자를 사용한 것을 확인할 수 있는데, 이 때 WHO를 사용하게 된다면 point, velocity, updatePoint에 대해 자동으로 fianl을 추론하게 된다.

그럼 WHO는 어떻게 사용하면 되는걸까?
다행히 우리가 직접 선언해주지 않아도 Xcode 8 이후부터는 WHO가 자동으로 켜져있기 때문에 별도의 설정을 해줄 필요는 없다.


-Today's Lesson Review-

오늘은 SwiftMethod Dispatch에 대해 학습했다.
평소 접근제한자나 final 같은 키워드를 단순한 목적으로만 사용했는데,
이렇게 성능과 직접적인 관련이 있다는 것을 확인하고 나니
앞으로 이런 부분도 고려해서 설계를 진행해야겠다는 생각을 하게 되었다.
profile
이유있는 코드를 쓰자!!

0개의 댓글