Недавно читая хабр и смотря на вечные баталии C++ и Rust разработчиков я подумал что-то вроде "А так ли хорошо управление памятью в Rust как о нем говорят?". С этим сегодня мы и попробуем разобраться.
Не будем тянуть, Rust разработчики постоянно говорят о необычный системе "владения" (ownership) которая и управляет памятью в этом языке. Она необычная, пусть работать с ней не сильно сложно, первое время осознавать отличия было достаточно больно. (Для меня как сишника точно). В предпросмотре я упоминал плюсы но в статье их примеров не будет, в них все вроде итак понятно, просто руками выделяешь и освобождаешь память. Я не буду судить что хуже или лучше, как никак это две абсолютно разных технологии которые используются для достижения разных целей.
Что из себя представляет данная система?
Система владения и жизненных циклов в Rust - одно из основных отличий от других языков и главный апостол его memory-safe повадок. Ее суть кроется в трех основных понятиях: ссылках, жизненных циклах и владении. Она разработана для того чтобы писать безопасный для памяти код без сборщика мусор и без ручных malloc() и free()
Правила концепции:
Каждое значение имеет владельца — переменную, которая отвечает за освобождение ресурса.
В каждый момент времени у ресурса может быть только один владелец.
Когда владелец выходит из области видимости, ресурс освобождается.
Эти правила статические и проверяются во время компиляции, в бинарнике их нет. Это мы еще рассмотрим позже. Пока что это не должно быть особо понятно если вы не пользовались подобной системой в прошлом.
❯ Правило первое, владение
fn main() {
let greet = String::from("Привет Хабр!");
println!(greet);
}
Переменная
greet- владелец строкиКогда закрывается фигурная скобка (
greetвыходит из области видимости) память автоматически освобождается. Но это не сборщик мусора, просто вызовdrop(greet)
❯ Правило второе, один владелец на одно время
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 теперь недействителен.
println!("{}", s2);
// println!("{}", s1); -> s1 больше не владелец, ошибка
} // drop(s1);
Ничего сложного, то есть тут переменная передается другому владельцу, а не копируется. К примеру в Python в аналогичном коде вывод был бы два раза hello. Предотвращается двойное освобождение памяти, s1 больше использовать нельзя. Но вообще клонировать тоже можно.
❯ Правило третье, заимствование
fn print_length(s: &String) {
println!("Length: {}", s.len());
}
fn main() {
let s = String::from("hello");
print_length(&s);
println!("{}", s); // s всё ещё действителен
}
Это по сути указатели, просто интегрированные во всю эту систему
&s- это указатель, он памятью не владеетПосле вызова
print_lengthвладелец (s) остается жив.Гарантируется что ссылка не будет жить дольше владельца (Фикс висячих указателей короче)
В изменяемых заимствованиях может существовать только одна
mut &T. (А это фикс гонок данных)
Более низкий уровень
Что же конкретно происходит когда мы создаем переменную сложного типа данных?
fn main() {
let some_text = String::from("Привет Хабр!"); // Не путать String с &str!
}
Раз уж на то пошло давайте посмотрим что происходит в ассемблере
rustc --emit asm string_mem.rs
В сравнении с тем же С где ассемблера вышло на 23 строки в Rust получится примерно 850, я отбросил не важные нам проверки и другое, оставил только самое важное.
_ZN7habr_ex4main17h514a1b0e6cb23afdE:
.cfi_startproc
subq $24, %rsp
.cfi_def_cfa_offset 32
movq %rsp, %rdi
leaq .Lanon.ba62d6a9bc58e2b5279d52a507255ab6.11(%rip), %rsi
movl $22, %edx
callq _ZN76_$LT$alloc..string..String$u20$as$u20$core..convert..From$LT$$RF$str$GT$$GT$4from17hcbb3a0442af0ee98E
movq %rsp, %rdi
callq _ZN4core3ptr42drop_in_place$LT$alloc..string..String$GT$17h2373330cd786f1baE
addq $24, %rsp
.cfi_def_cfa_offset 8
retq
Думаю всем уже должно быть ясно что основные концепции работы с памятью тут не отличаются, все те-же стек и куча, статическая и динамическая память, сложные и простые типы данных.
Вообще строка хранит ссылку на метаданные и само содержание строки в куче, это что-то вроде структуры такой формы
// Псевдокод
String {
ptr: *mut u8, // указатель на данные в куче
len: usize, // длина строки
capacity: usize // ёмкость буфера
}
Но вернемся к ассемблеру, название функции
ZN7habrex4main17h514a1b0e6cb23afdEэто зашифрованное (mangled) название. Манглирование нужно из-за допуска нескольких одинаковых названий функций с разными параметрами, также оно содержит хэш типов и путь к функции. Здесь невооруженным взглядом видно что этоhabr_ex::main(Название_файла::Название_функции)..cfi_startproc- это директива отладки, помогает дебаггеру понимать где сохраняются регистры, как восстанавливать стек и т.п.subq $24, %rspвыделяет 24 байта в стеке (уменьшая указатель стека%rsp). Это место нужно для локальной переменной, о которой надеюсь вы еще помните,some_text..cfi_de_cfa_offset 32- опять стек, нужно для того чтобы "обновить описание" стека для отладчика, теперь CFA =%rsp + 32.-
Следующие же 3 строки требуются для передачи аргументов вызов
String::from()movq %rsp, %rdi leaq .Lanon.ba62d6a9bc58e2b5279d52a507255ab6.11(%rip), %rsi movl $22, %edxМы используем
SysV ABI amd64. Первая строка значит что-то вроде%rdi = %rsp, это мы показываем куда конкретно поместить ту структуру с метаданными и саму строку. (%rsp - Stack Pointer Register. Важно пояснить что сама строка хранится в куче, а мета структура (приводил пример выше) лежит в стеке)leaq .Lanon...(%rip), %rsi- загружает адрес строкового литерала"Привет Хабр!"в%rsi.movl $22, %edx- длина строки в байтах: 22 (в UTF-8, «Привет Хабр!» столько и занимает) -
callq ZN76$LT$alloc.....2af0ee98Eэто вызов функции<alloc::string::String as core::convert::From<&str>>::fromТо есть уже создает сам String в куче и копирует туда данные той строки что мы указали изначально.
Последние же строчки вызывают
drop- это та функция которая вызывается для того чтобы освободить ресурсы если владелец выйдет из области видимости.
Кстати еще существует такая штука как unsafe{}. Она отключает всю безопасность и позволяет управлять памятью как тебе угодно. Сырые указатели (как в Си), некоторые FFI, обращения к union полям, инлайн ассемблер asm!().
Много чего можно быть unsafe, например функция, трейт, конкретный блок кода и т.п. Нужно чаще всего в программировании микроконтроллеров и других подобных задачах. Но особого внимания я думаю там уделять нечему.
❯ Как ownership уходит
Rust имеет LLVM компилятор, то есть исходники на rust превращаются в промежуточный MIR который мы можем поглядеть точно также как и выше посмотрели ассемблер.
Компиляция имеет очень много слоев. Исходный код -> AST (Abstract syntax tree) -> HIR (High Intermediate Representation) -> MIR (Medium IR) -> LLVM IR -> Исполняемый файл.
rustc --emit mir string_mem.rs
Это похожий код просто там уходит ownership и явно указываются инструкции.
// WARNING: This output format is intended for human consumers only
// and is subject to change without notice. Knock yourself out.
// HINT: See also -Z dump-mir for MIR at specific points during compilation.
fn main() -> () {
let mut _0: ();
let _1: std::string::String;
scope 1 {
debug some_text => _1;
}
bb0: {
_1 = <String as From<&str>>::from(const "Привет Хабр!") -> [return: bb1, unwind continue];
}
bb1: {
drop(_1) -> [return: bb2, unwind continue];
}
bb2: {
return;
}
}
В ассемблере есть функции с аналогичными названиями, но эта тема еще на три статьи. А на эту статью я уже все.
Выводы
В общем, на мой взгляд система заслуживает внимания но это определенно не панацея. Как и в конфликтах Rust разработчиков и Плюсовиков, чаще всего там не прав никто. Rust не замена плюсам, скорее аналог. У каждого свои плюсы.
Также хотел бы попросить о обратной связи. Я не сильно опытен в написании статей, так что это важно. Если было интересно прошу поставить плюсик.
Лично на мой взгляд немного подкачал с форматированием ну и чуть чуть контента маловато.
Комментарии (31)

Dhwtj
11.10.2025 17:17std::string_view get_name() { std::string s = "John"; return s; // неявное преобразование string -> string_view } int main() { auto name = get_name(); std::cout << name; // UB, крэш или мусор }И
// Rust - не скомпилируется fn get_name() -> &str { let s = String::from("John"); &s // error: cannot return reference to local variable }https://stackoverflow.com/questions/63166194/implicitly-convert-string-to-string-view
Жесть вообще

SilverTrouse
11.10.2025 17:17но при этом если заменить string_view на string& код ровно по такой же причине не скомпилируется ( или выкинет warning который лего сделать ошибкой)

Dhwtj
11.10.2025 17:17Можно и посложнее пример придумать, где просто не заметишь. Вопрос то принципиальный

Jijiki
11.10.2025 17:17https://godbolt.org/z/xxb549TEM у меня так вышло суть в том что лайф тайм внутри функции, кончится когда скобочка закончилась поэтому и будет уб, чтобы лайфтайм был определен он должен быть в зоне видимости, тоесть чтобы не было уб надо передать в функцию что пишем и куда пишем, или в функции создаётся что-то и пишется туда куда указано, дело в том что string_view создаёт еффективное представление на строку (ну чтобы не было копирования данных пишут что еффективно) и при выходе при создании внутри функции, лайфтайм строки! кончится и поэтому там будет пусто вобщем, поэтому надо позаботиться о том чтобы строка! была в области видимости как я понял

rsashka
11.10.2025 17:17Низкоуровневая реализация тут не причем, так как у Rust есть проблемы с доказательством самой модели безопасной работы с памятью.

black_warlock_iv
11.10.2025 17:17Вам там в комментариях много полезного написали, но кажется не в коня корм.

rsashka
11.10.2025 17:17Ага, много слов и никакой конкретики по заданным вопросам. Так же как и ваш комментарий.

qwerty19106
11.10.2025 17:17Это не правда. Вот этот комментарий, и особенно комментарии по всей ветке ниже показывают, что ваша претензия к реализации связного списка не справедливы. На Rust можно написать связный список без unsafe, но нельзя написать оптимальную реализацию в стиле С.
А вот в этом комментарии и нижележащей ветке вам привели ссылки на доказательства корректности модели памяти Rust. Но не доказательства корректности её реализации в компиляторе.

rsashka
11.10.2025 17:17Может быть вы тогда обратите внимание и на ответы к упомянутым вами комментариям?

qwerty19106
11.10.2025 17:17Конечно. Например я обратил внимание, что вы даже не перешли по ссылке оппонента. В ней 11-я статья от 2020 года описывает то, что вам нужно. А вы привели две статьи 2018 года, конечно в них всё было в зачаточном состоянии.

rsashka
11.10.2025 17:17Конечно, я не туда посмотрел или не так понял.
Может быть тогда вы напишите ту самую правильную ссылку с описанием доказательства математической корректности модели управления памятью или на худой конец список проверок и/или ограничений, которые сделаны в компиляторе Rust для её реализации?

qwerty19106
11.10.2025 17:17И вы еще раз подтвердили, что не переходите по ссылкам. Еще раз дублирую:
11-я статья от 2020 года описывает то, что вам нужно

rsashka
11.10.2025 17:17Вы издеваетесь? Мне хочется найти
описанием доказательства математической корректности модели управления памятью или на худой конец список проверок и/или ограничений, которые сделаны в компиляторе Rust для её реализации
А вы мне опять подсовываете частный случай оптимизации при доступе к памяти:
We give formal proofs (mechanized in Coq) showing that this rules out enough programs to enable optimizations that reorder memory accesses around unknown code and function calls, based solely on intraprocedural reasoning.

qwerty19106
11.10.2025 17:17Прямо над вашей цитатой написано следующее:
In this work, we propose Stacked Borrows, an operational semantics for memory accesses in Rust. Stacked Borrows defines an aliasing discipline and declares programs violating it to have undefined behavior, meaning the compiler does not have to consider such programs when performing optimizations.
А под ней:
We also implemented this operational model in an interpreter for Rust and ran large parts of the Rust standard library test suite in the interpreter to validate that the model permits enough real-world unsafe Rust code.
А в самой статье описана реализация интерпретатора Miri, который проверяет код std на соответствие модели Stacked Borrows:
We implemented Stacked Borrows in Miri to be able to test existing bodies of unsafe Rust code and make sure the model we propose is not completely in contradiction with how real Rust code gets written. Moreover, this also served to test a large body of safe code (including code that relies on non-lexical lifetimes), empirically verifying that Stacked Borrows is indeed a dynamic version of the borrow checker and accepts strictly more code than its static counterpart.
Так что либо вы сами издеваетесь, либо мы по разному понимаем термин Aliasing Model.
P.S. Чисто ради интереса, минусы тоже вы лепите?

rsashka
11.10.2025 17:17Причем тут Aliasing Model и цитаты из вашей ссылки?
Я задаю вопросы про базовую концепцию Rust, основанную на единственности владельца, а вы мне приводите работы по
натяжению совы на глобусдоказательству её частичной корректности в некоторых случаях.

Gorthauer87
11.10.2025 17:17Это не проблема Раста, это проблема самой концепции единственного владельца. У неё есть ограничения в виде невозможно выразить не ориентированные и циклические графы.
Ну штош, зато в 95 процентах случаем она работает и улучшает жизнь.
А если прикинуть немного, то для циклических структур не уверен, что вообще возможно формальное доказательство корректности. По циклам же можно бесконечно скакать.

rsashka
11.10.2025 17:17По правде, это не "проблема", а "ограничение" данной концепции. Так будет правильнее, чтобы не создавать негативную коннотацию при использовании штампа "проблема".
Проблема возникает, когда концепцию с подобными ограничениями начинают продвигать как универсальную замену на все случаи жизни, а в случае невозможности реализации конкретного алгоритма, предлагается переделывать архитектуру системы, из-за наличия подобных ограничений в исходной концепции языка.
По хорошему, и это тоже не стоит называть "проблемой". Просто нужно иметь ввиду, что у каждого языка программирвания есть ограничения и не стараться использовать PHP или JavaScript во встраиваемых решениях для микроконтроллеров или, как в данном случае, просто нужно помнить, что в Раст статическая проверка "владения и заимствования" не работает для циклических графов. Только рантайм, аналогичный реализованному в С++ :-)

kotan-11
11.10.2025 17:17Еще ни разу не видел ни одной статьи, которая рассказывает, как ссылочная модель Раста строит иерархии объектов в хипе. Как предотвращаются утечки памяти и дедлоки, как гарантируется иммутабельность шареных объектов. Я тут попытался реализовать на Расте вот этот бенчмарк DOM-структур данных, и пока застрял на невозможности иметь одновременно объект с единственным владельцем, на который можно ссылаться weak-указателями из других объектов. Кроме того для двухпроходной операции копирования с сохранением топологии графа мне нужен map(оригинальный объект->копия). И указатели в этой мапе должны потом присваиваться типизированным указателям -
Weak<RefCell<Card>>иWeak<RefCell<dyn CardItem>>. Хотя я даже не уверен, что мне нужны именно эти указатели. Не мобли бы вы подсказать, как в Расте реализуются структуры данных в хипе. Не обязательно в комментарии. Это могло бы стать хорошей темой для отдельной статьи.
Namilsky Автор
11.10.2025 17:17И правда интересная идея для статьи, но пока что я сам не смотрел как реализуется иерархия объектов в куче. В ближайшее время поищу и постараюсь ответить

Jijiki
11.10.2025 17:17концепция BVH(Bounding_volume_hierarchy) ищите в инете описания, но если найдёте про аабб - представляйте флай обжект тоесть коробку в которой данные, на вашем сленге Trait<T> чото такое или Box или просто структура с метаданными или та структура которая определяет связь или выборку
можно реализовать как дерево индексов, но у вас нету рейкаста, возможно надо будет подумать как выбирать иерархии или боксы
тоесть у вас владельцы в последовательности, в DOM индексация для ускорения(тут как раз не владельцы а указатели)
в теории ссылочная модель раста такая же как в С++, вам просто нужна последовательность и алгоритм по которому вы сортируете обьекты из последовательности(критерий при вставке из последовательности, чтобы дерево было еффективно)
Скрытый текст
struct Object3D { glm::vec3 position; glm::vec3 prevpos; glm::vec3 velocity; float spped; glm::vec3 size; // cube glm::vec3 radius; // sphere bool isSphere; bool collided = false; // visualisation bool stayObj = false; glm::vec3 NormalWall; //(1, 0, 0) std::string name; };//типо флай обджекта наверно данные для рендера в другом месте ... struct BVHNode { AABB box; BVHNode *left = nullptr; BVHNode *right = nullptr; std::vector<Object3D *> objects; // листовой узел bool isLeaf() const { return !objects.empty(); } ~BVHNode() { delete left; delete right; } }; BVHNode *buildBVH(std::vector<Object3D *> &objs, int depth = 0); ... BVHNode *bvhRoot; std::vector<Object3D> objects;//реальные обьекты ... glm::vec3 v = glm::vec3{0, 0, 0}; createOneBigCubeCoords(objects, v);//спрятана генерация обьектов просто std::vector<Object3D *> objPtrs;//я себе это как индексация представляю for (auto &o : objects) objPtrs.push_back(&o); bvhRoot = buildBVH(objPtrs);//будет обход по механизму дерева уже всё тех же обьектов ... но это дерево пока еще для коллизий, в теории так же можно сделать и для рендераутечек не было покачто

rivo
11.10.2025 17:17Насколько знаю, никак, модель ссылок работает с иерархической областью видимости (scope). Когда используются графы или циклические зависимостм, применяют индексы и указатели. Остальное гарантируется системой типов. Если захотите неправильно их использовать, у вас это получиться.

Gordon01
11.10.2025 17:17Кстати еще существует такая штука как
unsafe{}. Она отключает всю безопасностьНе отключает.
и позволяет управлять памятью как тебе угодно.
Не позволяет.
Rust имеет LLVM компилятор, то есть исходники на rust превращаются в промежуточный MIR который мы можем поглядеть точно также как и выше посмотрели ассемблер
Вы перепутали LLVM IR и MIR.
В общем, на мой взгляд система заслуживает внимания но это определенно не панацея.
Есть ощущение что перед выводом пропущен огромный кусок текста из которого сделан этот вывод
Rust не замена плюсам, скорее аналог. У каждого свои плюсы.
Замена.
Также хотел бы попросить о обратной связи. Я не сильно опытен в написании статей, так что это важно.
Не очень понятно зачем на хабре перепечатка первой главы растбука который уже есть на русском.
Часть про описание ассемблера может написать любая ЛЛМ.
Так что не очень понятно в чем ценность. Написать на хабре что плюсы точно-точно живы? Увы нет, они мертвы.

i-netay
11.10.2025 17:17Статья понравилась, разбор на низком уровне смысла того, о чём люди спорят, не вникая в подробности и не читая asm, лучше всего такие споры разрешает. Хочется видеть больше разборов, как реализуется безопасность чего-то и какой ценой, какие бывают другие варианты сделать то же.
Однако поправил бы, что unsafe не снимает все проверки безопасности. Код внутри блока unsafe компилируется по почти всем тем же правилам, разрешается очень немногое дополнительно: адресная арифметика, разыменование сырых указателей, небезопасная работа с union, ffi, потоконебезопасность, послабления на mut&, передачу аргументов с гонкой данных и ещё там пара штуковин. Есть явный исчерпывающий список, что становится можно, это не всё подряд.

domix32
11.10.2025 17:17Как-то тема borrow checker так и осталась нетронутой. Думал щас про SAT или хотя бы расскраску памяти как в Asan/Lsan расскажут .
Lewigh
Это не вполне корректное утверждение.
unsafe{}ничего не отключает а скорей наоборот включает внутри области возможность работать с небезопасными операциями: разыменование сырого указателя, вызов небезопасных функций, модификация статической переменной, доступ к полям union. Безопасность работает так же как и прежде, просто внутри области видимости программист берет на себя отвественность за возможное возникновение неопределенного поведения для вышеуказанных операций.Namilsky Автор
Спасибо за уточнение
kotan-11
"Безопасность работает так же как и прежде" - как безопасность может работать как и прежде, если можно свободно обращаться по любым указателям без никаких проверок, а доступ к полям union это самый настоящий reinterpret_cast? Поясните пожалуйста.
Lewigh
Я имел ввиду что никакие существующие правила и проверки не отключаются а работают также как и прежде и unsafe к примеру не позволяет брать несколько мутабельных ссылок, но разумеется работа с сырыми указателями в области unsafe может привести к UB.