Rusty Bytes – изучаем язык Rust
89 subscribers
1 photo
45 links
Канал про разработку на языке Rust: обучающие материалы, ссылки на полезные статьи, подборки интересных библиотек и фреймворков.
Download Telegram
🧠 Управление памятью в Rust.
Часть 3.2. Stack. Принцип работы на примере.

В качестве примера возьмём такой код:

fn add_one(val: i32) -> i32 {
let one = 1;
val + one
}

fn main() {
let unused = true;
let init = 0;
let one = add_one(init);
let two = add_one(one);
}


Запустившись, программа сохраняет в памяти адрес, указывающий на начало стека, куда можно добавлять элементы.

Далее объявляется и инициализируется переменная unused типа boolean размером 1 байт.
Этот байт будет записан по адресу указателя в начало стека, а сам указатель сдвинется на 1 байт вперёд.

Затем создаётся переменная init типа i32 размером 4 байта. И снова эти 4 байта помещаются в стек по адресу, который хранится в указателе на вершину стека, а затем этот указатель сдвигается вперёд на 4 байта.

Следующая переменная – one – инициализуруется результатом работы функции add_one.
Эта функция возвращает i32, т.е. тоже 4 байта, поэтому в стеке будет зарезервировано 4 байта под возвращаемое значение, затем указатель вершины стека сдвинется вперед на 4 байта, и начнётся обработка функции add_one.

На данном этапе программа отдельно сохранит текущий адрес вершины стека. Это начало так называемого стек фрейма (Stack Frame) – кусочка памяти стека, содержащего переменные одной функции. В дальнейшем этот адрес нам понадобится при возврате из функции.

Параметры в функцию передаются посредством копирования значения, под копии тоже нужно место. Поэтому далее в стек будет помещена переменная val и затем переменная one, обе по 4 байта. Указатель на вершину стека сдвинется в сумме на 8 байт.
При этом указатель на начало стек фрейма останется неизменным.

В конце итоговое значение val + one запишется в те 4 байта, которые были зарезервированы для возвращаемого значения (12 байт назад от текущей вершины стека).
А после завершения функции указатель на вершину стека откатится на значение начала стек фрейма. Таким образом мы "освободим" всю память, которая была занята переменными функции add_one (весь стек фрейм).

При вызове add_one второй раз для инициализации переменной two процесс повторится: резерв места под возвращаемое значение, сохранение адреса начала стек фрейма, последовательная "укладка" переменных val и one в стек (и в стек фрейм), запись результата функции в зарезервированное место и откат указателя вершины стека до значения начала стек фрейма.

Благодаря такой реализации мы получили "из коробки" ещё несколько преимуществ:
🟢 После завершения функции вся память, выделенная под переменные этой функции, "освобождается".
Я пишу это слово в кавычках, так как это не совсем то же самое, что освобождение памяти программой. Сама память всё так же зарезервирована под стек нашего приложения. Но при этом эта память становится доступной для записи переменных уже другой функции.
🟢 Благодаря такому процессу освобождения, программа экономно использует выделенное ей место.
Требуемая под стек память – это не сумма требуемой памяти под все переменные всех функций, а сумма требуемой памяти под функции самой длинной цепочки вызовов, начиная с main.

Теперь, рассмотрев принцип работы стека, можно остановиться на некоторых нюансах и переходить к подведению итогов.

Продолжение ⬇️

#rust #rustlang #RustMemoryManagement
🧠 Управление памятью в Rust.
Часть 3.3. Stack. Нюансы.

Для начала хочу отметить два момента:
▶️ Принцип работы стека относится к переменным, а не к их значениям.
То есть для чтения или перезаписи какой-либо переменной, программе не нужно сначала доставать из стека все другие значения, объявленные после неё.
Зная размер всех переменных и порядок их объявления, можно вычислить адрес нужной переменной.
Так для примера из прошлого поста переменную unused можно получить по адресу начала стек фрейма main. А для init адресом будет являться начало стек фрейма + 1 (1 – это размер unused, который был объявлен до init).
Похожим образом происходит запись возвращаемого значения функции add_one в слот, зарезервированный в стеке до переменных val и one.
▶️ Для каждого потока программы создаётся свой стек.
Поэтому работа с каждым стеком происходит в однопоточном режиме без необходимости каких-либо блокировок или синхронизаций.

🔗 Напоследок немного поговорим про ссылки на переменные функций.
Давайте посмотрим на такой пример:

fn print_array(arr: &[i32; 100], reverse: &bool) { ... }

fn main()
let arr: [i32; 100] = [1, 2, 3, ... 100];
let reverse = false;
print_array(&arr, &reverse);
}


Можно ли таким образом передавать ссылки на локальные переменные внутрь других функций?

Да, сделать так можно.
С точки зрения безопасности работы с памятью – всё корректно: принцип работы стека нам гарантирует, что пока работает функция print_array, переменные функции main никуда не денутся.

Более того, для переменной arr такой подход действительно имеет смысл. В случае больших массивов и сам процесс копирования может быть дольше, и его копии будут занимать дополнительное место в стеке.

А вот для переменной reverse в этом случае нет никакого смысла передавать ссылку. Как минимум потому что вместо копирования 1-го байта мы начинаем копировать 8 байт (столько занимают места ссылки на 64-х битных системах).
А ещё можно углубиться в принципы работы процессоров со стеком программ и найти там множество тонкостей, оптимизированных под работу со стеком в его нормальном виде, когда значения переменных лежат в своём стек фрейме, а не ссылаются на какие-то другие области стека.
Поэтому в большинстве случаев для передачи переменных примитивных типов нет нужды пытаться уйти от копирования с помощью передачи ссылок.

Можно ли возвращать ссылку на переменные функции? Например, на большой массив, чтобы избежать копирования.

fn create_array() -> &[i32; 100] {
/** создаём массив и возвращаем на него ссылку */
}

fn main() {
let a = create_array();
}


Нет, так сделать нельзя.
После того, как create_array отработает, стек фрейм этой функции будет освобождён и может быть перезаписан другими значениями. А значит нет гарантии, что мы сможем получить доступ к данным созданного массива.
Rust не допускает таких ситуаций, поэтому будет ошибка компиляции.

О том, как решать подобные задачи, мы поговорим в следующих частях этой серии постов.

А сейчас можно подвести итоги с плюсами и минусами стека:

Продолжение ⬇️

#rust #rustlang #RustMemoryManagement