четверг, 9 января 2020 г.

Управление памятью в Rust. Часть 1.

Основные принципы устройства памяти и работы с ней рассмотрены в статье Память в C++. Сегодня поговорим как работает с памятью Rust в контексте его парадигмы владения ресурсами.

Мы знаем, что память делится на статическую, стек и кучу, но до сих пор мы не говорили, где физически размещаются данные и как это происходит. Автоматические переменные и функции размещаются в стеке, это должно происходить максимально быстро и эффективно, поэтому организовано с помощью стека, отсюда и название. В стеке переменные и функции размещаются в своих областях по областям видимости и это значит, что изначально при переходе в другую область видимости, например при присвоении или передаче параметров в функцию и возврате из нее, всегда происходит копирование с дублированием памяти, из старой переменной (аргумент) в новую (параметр). Дальше мы можем либо не обращать внимания на старую переменную (как в с++) – копирование владения, либо в целях безопасности очистить (как в Rust) – передача владения. С точки зрения производительности очистка нагружает систему, поэтому для базовых типов ее специально не производят, и она происходит при уничтожении переменных (выход из области видимости). И тут важно понять, что и при копировании владения и при передаче, происходит копирование данных из одной части стека в другую, только либо без очистки, либо с очисткой. Даже при работе со ссылками происходит тоже самое с адресами памяти, которые являются значением наших переменных-ссылок, что конечно же эффективнее, чем передавать сами значения, на которые ссылаемся, и удобство в данном случае компенсирует затраты.

У размещения в стеке есть свои преимущества – скорость, удобство, но есть и свои недостатки, данные должны быть фиксированной длинны (требование для стека), и операции с тяжелыми данными (от 4 Kb) снижают эффективность, на которую мы надеялись. В этом случае нам поможет динамическое выделение памяти, когда мы сами управляем памятью вручную. И для этого служит область памяти под названием куча. В ней мы можем разместить наши тяжелые данные и менять их размер при необходимости, плюс мы можем получать к ним доступ в нужные нам моменты, без необходимости постоянно их куда-нибудь перекидывать. Главное требование – не создавать их больше чем это действительно необходимо и не забыть их очистить, чтобы не было утечек памяти и условий для возникновения ошибки при неправильном обращении к этим участкам памяти. В си++ для этого используются указатели, сырые с возможность вручную выделять (new) и освобождать (delete) память, и умные, которые сами следят за выделением и очисткой памяти (unique_ptr и shared_ptr). В Rust для этого служат умные указатели Box и Rc, которые отличаются тем, что с помощью Box можно менять данные, на которые он указывает, а с помощью Rc нет.

Первый из них Box, это аналог unique_ptr, который является единственным владельцем ресурса и следит за его очисткой. Для Box есть одна особенность, если он указывает на трейт или на Any, с помощью dyn (смотрите статью про Объекты в Rust), то кастование-вниз(downcast) этого указателя в тип имплементирующий трейт или Any, перемещает владение ресурсом.

Второй из них это Rc, аналог shared_ptr, который является совладельцем ресурса и тоже следит за очисткой ресурса, когда больше уже не остается совладельцев, с помощью подсчета ссылок. У Rc тоже есть особенность, наличие живых совладельцев ресурса, заставляет жить и сам ресурс, с другой стороны, при уничтожении всех совладельцев (Rc-указателей) очистится и ресурс, данные будут уничтожены и память очищена. Если мы хотим быть совладельцем ресурса, но в тоже время не влиять на его жизнеспособность, то можем ослабить (weak) указатель. Но тогда мы должны учитывать возможность, что ресурс может быть еще жив или уже мертв по причине смерти остальных сильных совладельцев. Ослабить указатель можно с помощью Rc::downgrade. Проверить жив ли еще ресурс можно с помощью upgrade(), которая возвращает Options<Some(Rc), None>.

Тут стоит обратить внимание на один момент, казалось бы, что такие структуры данных, как строки и векторы, которые могут содержать тяжелые данные, нужно отдельно размещать в куче, но этого специально делать не нужно, т.к. они и так реализуют механизм размещения своих элементов в куче, в отличие от массивов. А хранить в куче указатели на ресурсы в куче, это вообще не эффективно. Поэтому использование массивов для хранения указателей на ресурсы в куче лучше, т. к. не нужно производить лишнее разыменовывание элементов. И вообще, куча — это плохо, она медленная.

use std::rc::Rc;

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {

    //Box
    let mut v_box: Vec<Box<Point>> = Vec::new();

    v_box.push(Box::new(Point { x: 0, y: 0 }));
    v_box.push(Box::new(Point { x: 1, y: 1 }));
    *v_box[0] = Point { x: 99, y: 99 }; //Ok!

    println!("v_box.len:{} v_box:{:?}", v_box.len(), v_box);

    //Rc
    let rc = Rc::new(Point { x: 2, y: 2 });

    let rc1 = Rc::clone(&rc);
    let rc2 = Rc::clone(&rc);
    //*rc2 = Point { x: 3, y: 3 }; //Error!
    let rc3 = Rc::clone(&rc);
    //let rc4:Rc<Point> = rc.clone(); // weak: Some(Rc)

    //Weak
    let weak = Rc::downgrade(&rc);
    {
        let v_rc = vec![rc, rc1, rc2, rc3];
        println!("v_rc.len:{} v_rc:{:?}", v_rc.len(), v_rc);

    } //weak: None

    let wr = weak.upgrade();
    println!("weak: {:?}", wr);
}

Перегуд В.

Комментариев нет:

Отправить комментарий