Rust 제네릭 소개 [2/2]: 특성 개체(정적 대 동적 디스패치)

원래 내 블로그에 게시됨: https://kerkour.com/rust-generics-trait-objects

Rust 제네릭 소개:
  • Traits
  • Trait Objects (Static vs Dynamic dispatch)

  • This post is an excerpt from my course Black Hat Rust



    이제 궁금해하실 수 있습니다. 주어진 특성을 만족하는 다양한 구체적인 유형을 포함할 수 있는 컬렉션을 만드는 방법은 무엇입니까? 예를 들어:

    trait UsbModule {
        // ...
    }
    
    struct UsbCamera {
         // ...
    }
    
    impl UsbModule for UsbCamera {
        // ..
    }
    
    impl UsbCamera {
        // ...
    }
    
    struct UsbMicrophone{
         // ...
    }
    
    impl UsbModule for UsbMicrophone {
        // ..
    }
    
    impl UsbMicrophone {
        // ...
    }
    
    let peripheral_devices: Vec<UsbModule> = vec![
        UsbCamera::new(),
        UsbMicrophone::new(),
    ];
    


    불행하게도 이것은 Rust에서 그렇게 간단하지 않습니다. 모듈의 메모리 크기가 다를 수 있으므로 컴파일러에서 이러한 컬렉션을 만들 수 없습니다. 벡터의 모든 요소는 같은 모양을 갖지 않습니다.

    특성 개체는 런타임에 계약(특성)을 준수하는 다양한 구체적인 유형(다양한 모양)을 사용하려는 경우 이 문제를 정확하게 해결합니다.

    개체를 직접 사용하는 대신 컬렉션의 개체에 대한 포인터를 사용할 것입니다. 이번에는 모든 포인터의 크기가 같으므로 컴파일러가 코드를 수락합니다.

    실제로 이를 수행하는 방법은 무엇입니까? 스캐너에 모듈을 추가할 때 아래에서 볼 수 있습니다.

    정적 대 동적 디스패치



    그렇다면 일반 매개변수와 특성 객체의 기술적인 차이점은 무엇입니까?

    일반 매개변수를 사용하는 경우(여기서는 process 함수용):
    ch_04/snippets/dispatch/src/statik.rs

    trait Processor {
        fn compute(&self, x: i64, y: i64) -> i64;
    }
    
    struct Risc {}
    
    impl Processor for Risc {
        fn compute(&self, x: i64, y: i64) -> i64 {
            x + y
        }
    }
    
    struct Cisc {}
    
    impl Processor for Cisc {
        fn compute(&self, x: i64, y: i64) -> i64 {
            x * y
        }
    }
    
    fn process<P: Processor>(processor: &P, x: i64) {
        let result = processor.compute(x, 42);
        println!("{}", result);
    }
    
    pub fn main() {
        let processor1 = Cisc {};
        let processor2 = Risc {};
    
        process(&processor1, 1);
        process(&processor2, 2);
    }
    


    컴파일러는 함수를 호출하는 각 유형에 대한 특수 버전을 생성한 다음 호출 사이트를 이러한 특수 함수에 대한 호출로 바꿉니다.

    이것은 단형화로 알려져 있습니다.

    예를 들어 위의 코드는 대략 다음과 같습니다.

    fn process_Risc(processor: &Risc, x: i64) {
        let result = processor.compute(x, 42);
        println!("{}", result);
    }
    
    fn process_Cisc(processor: &Cisc, x: i64) {
        let result = processor.compute(x, 42);
        println!("{}", result);
    }
    


    이러한 기능을 직접 구현하는 것과 같습니다. 이를 정적 디스패치라고 합니다. 유형 선택은 컴파일 타임에 정적으로 이루어집니다. 최고의 런타임 성능을 제공합니다.

    반면에 특성 개체를 사용하는 경우:
    ch_04/snippets/dispatch/src/dynamic.rs

    trait Processor {
        fn compute(&self, x: i64, y: i64) -> i64;
    }
    
    struct Risc {}
    
    impl Processor for Risc {
        fn compute(&self, x: i64, y: i64) -> i64 {
            x + y
        }
    }
    
    struct Cisc {}
    
    impl Processor for Cisc {
        fn compute(&self, x: i64, y: i64) -> i64 {
            x * y
        }
    }
    
    fn process(processor: &dyn Processor, x: i64) {
        let result = processor.compute(x, 42);
        println!("{}", result);
    }
    
    pub fn main() {
        let processors: Vec<Box<dyn Processor>> = vec![
            Box::new(Cisc {}),
            Box::new(Risc {}),
        ];
    
        for processor in processors {
            process(&*processor, 1);
        }
    }
    


    컴파일러는 1개의 함수process만 생성합니다. 런타임에 프로그램은 어떤 종류의 Processorprocessor 변수인지, 따라서 어떤 compute 메서드를 호출할지 감지합니다. 이것은 알려진 동적 디스패치입니다. 유형 선택은 런타임에 동적으로 이루어집니다.

    특성 개체&dyn Processor의 구문은 특히 덜 장황한 언어에서 오는 경우 약간 무거워 보일 수 있습니다. 나는 그것을 개인적으로 좋아한다! 한 눈에 dyn Processor 덕분에 함수가 특성 개체를 허용한다는 것을 알 수 있습니다.

    Rust는 각 변수의 정확한 크기를 알아야 하므로 참조&가 필요합니다.
    Processor 특성을 구현하는 구조의 크기가 다를 수 있으므로 유일한 해결책은 참조를 전달하는 것입니다. Box , Rc 또는 Arc 와 같은 (스마트) 포인터일 수도 있습니다.

    요점은 processor 변수가 컴파일 시간에 알려진 크기를 가져야 한다는 것입니다.

    이 특정 예에서는 &*processor 함수에 대한 참조를 전달하기 위해 먼저 Box를 역참조해야 하기 때문에 process를 수행합니다. 이것은 process(&(*processor), 1) 와 동일합니다.

    동적으로 파견된 함수를 컴파일할 때 Rust는 후드 아래에 a vtable라고 하는 것을 만들고 런타임에 이 vtable을 사용하여 호출할 함수를 선택합니다.

    This post is an excerpt from my course Black Hat Rust



    마무리 생각



    절대적인 성능이 필요할 때는 정적 디스패치를 ​​사용하고 더 많은 유연성이 필요하거나 동일한 동작을 공유하는 개체 모음이 필요할 때는 특성 개체를 사용하세요.

    좋은 웹페이지 즐겨찾기