똑바른 날개

Rust에서의 Dispatch(Static dispatch vs dynamic dispatch) 본문

프로그래밍/Rust

Rust에서의 Dispatch(Static dispatch vs dynamic dispatch)

Upright_wing 2025. 7. 17. 11:36
반응형

그전글 Rust에서의 Zero-cost Abstraction ㅇ에서 다루었던 Zero-cost Abstraction에서 dynamic dispatch와 static dispatch의 개념이 등장하여, 추가 작성한다.

Dispatch란?

dispatch는 단순히 말해 “어떤 함수가 호출되는지를 결정하는 과정”이다. Rust에서는 이 dispatch 방식이 정적으로 결정되는 Static Dispatch와, 런타임에 결정되는 Dynamic Dispatch로 나뉜다.

이러한 디스패치 방식은 trait 기반의 polynominal과 직접적인 관련이 있으며, 성능이나 표현력에 있어 트레이드 오프가 있다.

 

Static Dispatch

정의

Static dispatch는 함수가 호출될 구체적인 타입이 컴파일 시점에 결정되는 방식이다. 일반 함수 호출이나 제네릭을 사용할 때 발생한다.

특징

  • 컴파일 타임에 어떤 함수가 호출될지 결정됨
  • monomorphization을 통해 제네릭 함수가 타입별로 복제됨
  • 런타임 오버헤드 없음

예시: 제네릭 기반 trait 호출

trait Speak {
    fn speak(&self) -> String;
}

struct Dog;
struct Cat;

impl Speak for Dog {
    fn speak(&self) -> String {
        "Woof".into()
    }
}

impl Speak for Cat {
    fn speak(&self) -> String {
        "Meow".into()
    }
}

fn make_noise<T: Speak>(animal: &T) {
    println!("{}", animal.speak());
}

이 경우 컴파일러는 make_noise::<Dog> 와 make_noise::<Cat>을 각각 생성한다. 이것이 monomorphization이며, 추상화된 trait 호출이 실제 함수 포인터 없이 구체 함수로 변환된다.

Dynamic Dispatch (동적 디스패치)

정의

Dynamic dispatch는 어떤 메서드를 호출할지 런타임에 결정하는 방식이다. trait object (dyn Trait)를 통해 사용되며, 호출 시 vtable을 이용한다.

특징

  • 런타임에 어떤 함수가 호출될지 결정됨
  • trait object 사용 (&dyn Trait, Box<dyn Trait> 등)
  • 런타임 오버헤드 존재 (간접 함수 호출, vtable lookup)
  • trait이 object-safe 해야 함

예시: trait object 사용

fn make_noise(animal: &dyn Speak) {
    println!("{}", animal.speak());
}

이 호출은 vtable을 통해 런타임에 Dog 또는 Cat의 speak()를 결정한다.


Trait Object와 Wide Pointer

Trait object는 일반 포인터가 아닌 Wide Pointer를 사용한다. 이는 다음 두 가지 정보를 가진다.

  • 데이터 주소 (&Dog, &Cat 등의 실제 인스턴스 주소)
  • vtable 주소 (trait 메서드들이 구현된 테이블)

즉, &dyn Trait은 일반 포인터가 아닌 fat pointer이며, trait object는 Size가 정해지지 않는다. 이는 동적 디스패치가 필요한 이유이자 제약 조건이기도 하다.


Sized를 trait에 사용하는 의미

기본적으로 trait는 size가 정해져 있지 않으며, 이 말은 trait 자체의 크기를 컴파일 타임에 알 수 없음을 의미한다.

trait MyTrait: Sized {
    fn only_static(&self);  // trait object로는 호출 불가
}

그러나 Sized bound는 dynamic dispatch를 명시적으로 막는다. 또는 trait 내 일부 메서드만 static dispatch를 허용할 수도 있다.

trait MyTrait {
    fn can_use_dyn(&self);
    fn only_for_sized(&self) where Self: Sized;
}

이 방식은 trait object를 만들 수는 있지만, 특정 메서드는 trait object로는 사용할 수 없도록 제약을 걸 수 있다.

이렇게 Sized가 bound된 함수나 trait은 tait object로 사용될수 없으므로, dynamic dispatch로 사용될 수 없다.

성능 비교

  Static Dispatch Dynamic Dispatch
Trait 제한 없음 Object-safe 필요
함수 호출 시점 컴파일 타임 런타임
실행 속도 빠름 (직접 호출) 느림 (간접 호출 + vtable)
코드 크기 커질 수 있음 (모노모피제이션) 작을 수 있음 (단일 함수)
Inlining 가능성 높음 거의 불가능
추론의 복잡도 높음 (컴파일 시간 증가) 낮음

마무리

Rust의 dispatch는 유저가 성능과 가독성을 적절하게 조절할 수 있게 해준다.

  • 빠르고 최적화된 코드가 필요하면 static dispatch + generics
  • 유연하고 추상적인 인터페이스가 필요하면 dynamic dispatch + trait object

출처

https://softwaremill.com/rust-static-vs-dynamic-dispatch/

반응형