ABOUT ME

-

Total
-
  • Rust 문법: Box
    컴퓨터/Rust 2023. 5. 13. 22:47
    728x90
    반응형

    Box 이해하기


    Rust의 Box는 언어의 메모리 관리 모델에서 핵심 개념입니다.

    이는 값들을 힙에 할당하는 방법을 제공하여 더 유연한 메모리 사용을 가능하게 합니다.

    Rust에서의 Box 타입은 힙 할당을 위한 스마트 포인터입니다.

    단순한 스택 할당과 달리, Box를 사용하면 대량의 데이터를 저장하거나 값을 오랫동안 메모리에 유지하거나 런타임에서만 알 수 있는 타입의 값을 저장할 수 있습니다.


    Box란 무엇인가?


    Box는 특정 타입 T의 힙 할당을 고유하게 소유하는 포인터 타입입니다.

    값이 Box에 들어가면 힙으로 이동되고 소유권이 Box로 이전됩니다.

    Box가 범위를 벗어날 때 소멸자가 호출되며 힙 메모리가 해제되고 값이 삭제됩니다.


    Box가 제공하는 메서드


    Box는 생성, 사용 및 메모리 관리를 위해 여러 가지 메서드를 제공합니다. 알아야 할 주요 메서드는 다음과 같습니다:

    • new: 이 메서드는 힙에 메모리를 할당한 후 x를 해당 메모리에 배치합니다. 만약 T가 크기가 0이면, 이 메서드는 실제로는 어떠한 메모리도 할당하지 않습니다.
    • new_uninit: 이것은 테스트용 API로, 초기화되지 않은 내용물을 가진 새로운 Box를 생성합니다.
    • new_zeroed: 이것은 또한 테스트용 API로, 초기화되지 않은 내용물을 가진 새로운 Box를 생성하며, 메모리는 0으로 채워집니다.
    • pin: 새로운 Pin를 생성합니다. T가 Unpin을 구현하지 않는 경우, x는 메모리에 고정되어 이동할 수 없게 됩니다. Box의 생성 및 고정은 두 단계로 수행될 수도 있습니다: Box::pin(x)는 Box::into_pin(Box::new(x))와 동일한 작업을 수행합니다. Box가 이미 있는 경우나 Box를 Box::new로 생성하는 방식과는 다른 방식으로 (고정된) Box를 생성하려면 into_pin을 사용하는 것이 좋습니다
    • try_new: 이 메서드는 힙에 메모리를 할당한 후 x를 해당 메모리에 배치하며, 할당이 실패할 경우 오류를 반환합니다. 만약 T가 크기가 0이면, 실제로는 어떠한 메모리도 할당하지 않습니다.
    • try_new_uninit: 이 메서드는 초기화되지 않은 내용물을 가진 새로운 Box를 힙에 생성하려고 시도하며, 할당이 실패할 경우 오류를 반환합니다.
    • try_new_zeroed: 이 메서드는 초기화되지 않은 내용물을 가진 새로운 Box를 힙에 생성하며, 메모리는 0으로 채워집니다. 할당이 실패할 경우 오류를 반환합니다.
    • new_in: 이 메서드는 주어진 할당기에서 메모리를 할당한 후 x를 해당 메모리에 배치합니다. 만약 T가 크기가 0이면, 실제로는 어떠한 메모리도 할당하지 않습니다.
    • try_new_in: 이 메서드는 주어진 할당기에서 메모리를 할당한 후 x를 해당 메모리에 배치하며, 할당이 실패할 경우 오류를 반환합니다. 만약 T가 크기가 0이면, 실제로는 어떠한 메모리도 할당하지 않습니다.
    • new_uninit_in: 이 메서드는 주어진 할당기에서 초기화되지 않은 내용물을 가진 새로운 Box를 생성합니다.
    • try_new_uninit_in: 이 메서드는 주어진 할당기에서 초기화되지 않은 내용물을 가진 새로운 Box를 생성하며, 할당이 실패할 경우 오류를 반환합니다.
    • new_zeroed_in: 이 메서드는 주어진 할당기에서 초기화되지 않은 내용물을 가진 새로운 Box를 생성하며, 메모리는 0으로 채워집니다.
    • try_new_zeroed_in: 이 메서드는 주어진 할당기에서 초기화되지 않은 내용물을 가진 새로운 Box를 생성하며, 메모리는 0으로 채워집니다. 할당이 실패할 경우 오류를 반환합니다.

     

    예제

    Rust에서 Box를 사용하는 몇 가지 일반적인 상황을 살펴보겠습니다:

    1. 스택 오버플로우를 피하기 위해 큰 데이터 구조를 Box에 넣기


     때로는 프로그램에서 사용하는 데이터 구조가 매우 큰 경우가 있습니다.

    이러한 경우 스택에 할당하면 스택 오버플로우가 발생할 수 있습니다.

    이럴 때 Box::new를 사용하여 큰 데이터 구조를 힙에 할당할 수 있습니다.

    struct LargeStruct {
        data: [i32; 1000000],
    }
    
    let large_struct = Box::new(LargeStruct { data: [0; 1000000] });



       이 코드는 LargeStruct 인스턴스를 힙에 저장하기 때문에 스택 오버플로우가 발생하지 않습니다.



    2. 재귀적인 데이터 구조 생성을 위한 Boxing



     Box는 종종 연결 리스트나 트리와 같은 재귀적이거나 복잡한 데이터 구조를 생성하는 데 사용됩니다.

    단순한 예로 단일 연결 리스트를 생각해보겠습니다:

    enum List<T> {
        Cons(T, Box<List<T>>),
        Nil,
    }
    
    use List::{Cons, Nil};
    
    let list: List<i32> = Cons(1, Box::new(Cons(2, Box::new(Nil))));



       이 예제에서 리스트의 각 요소(마지막 요소 제외)는 값과 다음 요소를 가리키는 박스를 포함하는 Cons변형입니다.

      마지막 요소는 Nil로 리스트의 끝을 나타냅니다.


    3. 클로저를 저장하기 위해 Box 사용하기


    Rust에서 클로저는 런타임에 크기가 다양하며 항상 구조체 필드로 직접 저장할 수 없는 경우가 있습니다.

    Box를 사용하여 클로저를 힙에 할당된 객체로 저장할 수 있습니다.

    struct MyStruct {
        closure: Box<dyn Fn()>,
    }
    
    let my_struct = MyStruct {
        closure: Box::new(|| println!("Hello from closure!")),
    };
    
    (my_struct.closure)();



       이 예제에서 Box는 인수를 받지 않고 반환값이 없는 모든 클로저를 저장할 수 있게 합니다. </dyn fn()>

      이러한 예는 Rust에서 Box를 사용할 수 있는 다양한 방법 중 일부에 불과합니다.

      중요한 점은 Box가 데이터를 힙에 저장하는 방법을 제공한다는 것이며, 이는 다양한 상황에서 유용할 수 있습니다.

     

    4. 고급 예제 - 간단한 인터프리터 구현

     

    이 사용 예제는 Box의 재귀적인 데이터 구조, 다형성, 그리고 리소스 관리 기능을 보여줍니다.

    이이 언어는 덧셈과 곱셈 두 가지 연산을 지원합니다.

    먼저, 언어에서 표현식을 나타내는 Expr 트레이트를 정의합니다:

    trait Expr {
        fn evaluate(&self) -> i32;
    }



    표현식은 정수 값을 얻기 위해 평가할 수 있는 것을 나타냅니다.

    다음으로, 덧셈과 곱셈 표현식을 나타내는 Add와 Mult 두 개의 구조체를 정의합니다:

    struct Add {
        lhs: Box<dyn Expr>,
        rhs: Box<dyn Expr>,
    }
    
    impl Expr for Add {
        fn evaluate(&self) -> i32 {
            self.lhs.evaluate() + self.rhs.evaluate()
        }
    }
    
    struct Mult {
        lhs: Box<dyn Expr>,
        rhs: Box<dyn Expr>,
    }
    
    impl Expr for Mult {
        fn evaluate(&self) -> i32 {
            self.lhs.evaluate() * self.rhs.evaluate()
        }
    }



    Add와 Mult는 모두 Expr을 구현하는 어떤 것을 박스에 담은 두 개의 하위 표현식을 포함합니다.

    마지막으로, 상수 정수 값을 나타내는 Const 구조체를 정의합니다:

    struct Const {
        value: i32,
    }
    
    impl Expr for Const {
        fn evaluate(&self) -> i32 {
            self.value
        }
    }



    이제 표현식을 나타내고 평가할 수 있습니다:

    let expr: Box<dyn Expr> = Box::new(Add {
        lhs: Box::new(Mult {
            lhs: Box::new(Const { value: 2 }),
            rhs: Box::new(Const { value: 3 }),
        }),
        rhs: Box::new(Const { value: 4 }),
    });
    
    println!("{}", expr.evaluate());  // 출력 결과: "10"


    이 예제는 (2 * 3) + 4 표현식을 생성하고 이를 10으로 평가합니다.

    이 사용 예제는 Box의 여러 기능을 보여줍니다:


    1. Box는 재귀적인 데이터 구조를 가능하게 합니다: Add와 Mult는 다른 Add또는 Mult 인스턴스를 포함할 수 있는 박스를 포함합니다.

    2. Box는 다형성을 가능하게 합니다: Add, Mult, Const는 서로 다른 타입이지만 모두 Expr로 취급하여 Box에 담을 수 있습니다.

    3. Box는 자동 정리 기능을 제공합니다: expr이 스코프를 벗어나면, 중첩된 Add, Mult, Const 인스턴스를 포함한 모든 메모리가 자동으로 해제됩니다.

     

    5. Box::leak

     

    Box::leak 함수는 Box를 "leak"하는 매우 특수한 도구로, 해당하는 메모리가 해제되지 않도록 합니다.

    이는 사실상 제어된 메모리 누수로, 함수의 이름에서 알 수 있듯이 그렇게 명명되었습니다.

    이는 프로그램의 나머지 수명 동안 일부 데이터를 유지하고자 하는 경우에 유용할 수 있습니다.

    Box::leak의 반환된 참조는 프로그램의 나머지 동안 계속 유효한 '정적 참조'의 한 종류로 생각할 수 있습니다.

    명시적으로 할당 해제하지 않는 한 유효한 상태를 유지합니다.

    그러나 메모리 누수는 일반적으로 원하지 않는 동작이며, 특정 상황에서만 사용되어야 합니다.

    다음은 Box::leak가 사용될 수 있는 예제입니다:

    프로그램의 수명 동안 읽힐 예정인 큰 구성 구조체를 초기화하는 함수를 고려해 보겠습니다.

    이 함수는 주로 main 함수의 시작 부분에서 호출될 수 있습니다.

    struct Config {
        // 여기에 일부 구성 필드
    }
    
    impl Config {
        fn new() -> &'static Config {
            let config = Box::new(Config {
                // 필드 초기화
            });
    
            Box::leak(config)
        }
    }
    
    fn main() {
        let config = Config::new();
    
        // 이제 config는 프로그램 전체에서 사용할 수 있습니다.
        // ...
    }



    이 시나리오에서는 Box::leak를 사용하여 Config 객체가 프로그램의 나머지 수명 동안 유지되도록 합니다.

    unsafe 코드를 사용하지 않고도 Config의 단일 전역 인스턴스를 간편하고 안전하게 사용할 수 있는 방법입니다.

    하지만 Box::leak의 이용은 사실상 전역 가변 상태를 생성하는 것이기 때문에

    코드의 추론이 어려워지고 버그가 발생할 가능성이 높아질 수 있습니다. 특히 다중 스레드 환경에서 이러한 방식은 문제가 될 수 있습니다.

    또한, Config 객체의 메모리를 더 이상 필요하지 않은 경우에도 해당 메모리를 해제할 수 없습니다.

    따라서 이러한 사용법은 신중하게 고려해야 합니다.

    Box::from_raw를 사용하여 누출된 Box를 "회수"하여 메모리를 해제할 수도 있습니다:

    // config가 누출된 Box라고 가정해봅시다:
    let config: &'static mut Config = /* ... */;
    
    // Box로 다시 변환하여 회수할 수 있습니다:
    let config_box: Box<Config> = unsafe { Box::from_raw(config) };
    
    // 이제 config_box가 삭제될 때 메모리가 해제됩니다.



    여기서 unsafe 키워드는 Box::from_raw가 unsafe 함수이기 때문에 필요합니다.

    특정 경우에는 config가 Box에서 가져온 유효한 포인터임을 알기 때문에 이를 안전하게 사용할 수 있습니다.

    그러나 일반적으로 임의의 포인터로 Box::from_raw를 호출하는 것은 매우 위험하기 때문에 unsafe로 표시되었습니다.

     

    장단점

     

    Box사용의 장점:

    1. 스택 오버플로우 회피: Box는 데이터를 힙에 할당하므로 큰 데이터 구조나 깊은 재귀에 대한 스택 오버플로우 오류를 방지하는 데 도움이 됩니다. 이를 통해 스택 할당만으로는 처리할 수 없는 큰 데이터를 처리할 수 있습니다.

    2. 효율적인 재귀적 데이터 구조: Box는 트리나 연결 리스트와 같은 재귀적인 데이터 구조를 생성하는 데 자주 사용됩니다. 이러한 데이터 구조는 힙 할당의 형태 없이는 구축하기 어렵거나 불가능할 수 있습니다.

    3. 다형성: Box를 사용하면 트레잇 객체를 사용할 수 있으므로 동적 디스패치가 가능해집니다. 이를 통해 동일한 트레잇을 공유하는 다른 타입의 컬렉션을 가질 수 있으며, 이는 다양한 시나리오에서 유용할 수 있습니다.

    Box 사용의 단점:


    1. 메모리 할당 오버헤드: 힙에 메모리를 할당하는 것은 스택에 할당하는 것보다 느립니다. 이는 운영체제와의 상호작용이 필요하기 때문에 스택 할당보다 훨씬 더 오랜 시간이 걸릴 수 있습니다. 또한, 힙 메모리는 분산되어 있어 캐시 효율성이 떨어질 수 있습니다.

    2. 간접 참조: Box를 사용하면 한 단계의 간접 참조가 추가됩니다. 즉, 데이터를 직접 처리하는 대신 데이터에 대한 포인터를 다루게 됩니다. 이는 추가적인 간접 참조 단계와 캐시 미스의 가능성으로 인해 성능에 약간의 영향을 줄 수 있습니다.

    3. 메모리 단편화: Box를 할당하고 해제하는 과정을 반복하면 시간이 흐름에 따라 힙이 단편화될 수 있습니다. 이는 비효율성과 증가된 메모리 사용을 야기할 수 있습니다.

    일반적으로 Box를 사용할지 여부는 프로그램의 특정 요구사항에 따라 달라집니다. 큰 데이터 구조를 다루는 경우나 재귀적인 데이터 구조나 트레잇 객체를 생성해야 하는 경우 등 힙 할당이 필요한 경우에는 Box가 훌륭한 도구입니다. 그러나 성능이 중요한 요소이고 Box가 제공하는 기능이 필요하지 않은 경우 다른 옵션을 고려하는 것이 좋습니다.

     

    결론

    Rust의 Box 타입은 힙에 할당된 데이터를 관리하는 데 굉장히 유용한 도구입니다.

    다양한 메서드를 통해 Rust 프로그램에서 메모리를 유연하고 강력하게 다룰 수 있습니다.

    그러나 Box가 안전성과 추상화 수준을 제공한다는 점을 강조하면서도,

    프로그래머가 메모리 관리에 대해 생각하는 것을 면제하지는 않는다는 사실을 기억해야 합니다.

    Box와 다른 소유권 타입의 적절한 사용은 효율적이고 안전한 Rust 코드 작성에 중요합니다.

     

     

    Box in std::boxed - Rust

    Consumes and leaks the Box, returning a mutable reference, &'a mut T. Note that the type T must outlive the chosen lifetime 'a. If the type has only static references, or none at all, then this may be chosen to be 'static. This function is mainly useful fo

    doc.rust-lang.org

     

    728x90

    댓글