Модель памяти Rust

Модель памяти Rust довольно сильно отличается как от управляемых языков типа Java или C#, так и от не управляемых языков типа C и C++. Так как Rust является совершенно новым языком программирования и не ограничен какими-либо требованиями совместимости с предшественниками, он реализует наиболее удобную модель памяти для решения следующих целей:
Безопасность. Предотвращение возникновения утечек памяти или ошибок сегментации.
Производительность. Сборщик мусора управляет указателями а не объектами. Нет необходимости замораживать все задачи для очистки памяти, так как каждая из них имеет собственный хип.
Многопоточность. Предотвращение возникновения гонок в памяти, так как каждая из задач имеет собственный хип и передаваемые между задачами данные должны копироваться. Наличие хипа обмена для избежания лишних операций копирования с семантикой владения.

Размещение объектов

Rust предоставляет возможность размещения объектов как на стеке, так и в хипе, причем в случае с хипом объекты должны адресоваться либо при помощи разделяемых либо в уникальных указателей.

Использование стека

Так код let x = Point {x: 1f, y: 1f}; разместит объект типа Point на стеке задачи в которой будет вызван. При копировании подобного объекта будет let y = x; будет скопирован не указатель на объект x, а вся структура типа Point.

Использование разделяемых указателей

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

let x = @Point {x: 1f, y: 1f};

В отличие от стековых объектов, при копировании, копируется исключительно указатель, а не данные.

let y = x; // теперь x и y указывают на один и тот же объект типа Point

Так же необходимо отметить тот факт, что невозможно создать структуру, содержащую указатель на собственный тип (классический пример – односвязный список). Для того что бы компилятор разрешил подобную конструкцию, необходимо обернуть указатель в тип Option.

struct LinkedList<T> {
    data: T,
    nextNode: Option<@LinkedList<T>>
}

Внимание: на данный момент, для управления разделяемыми указателями используется подсчет ссылок, но в будущем планируется переработка данного функционала и использование полноценного сборщика мусора.

Уникальные указатели

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

let x = ~Point {x: 1f, y: 1f};

Уникальные указатели реализуют семантику владения благодаря чему объект может адресовать только один уникальный указатель.

let y = x; // теперь y указывают на созданный ранее объект типа Point.
           // Указатель x деинициализирован.

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

let x = ~Point {x: 1f, y: 1f};
let y = copy x; // теперь y указывают на копию созданного ранее объекта
                //  типа Point. Указатель x не изменился.

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

Временные указатели

Временные указатели – указатели которые могут указывать на любой тип памяти: стек, локальном или хипе обмена, а так же на внутренний член любой структуры данных. На физическом уровне, временные указатели представляют собой типичные Си указатели и, как следствие, не отслеживаются сборщиком мусора и не привносят никаких дополнительных накладных расходов. В то же время, основным отличаем от Си указателей являются дополнительные проверки проводимые на этапе компиляции для гарантии возможности безопасного использования. Для создания временные указателей используется унарный оператор &

let on_the_stack  = &Point {x: 3.0, y: 4.0}; // объект типа Point
// был создан на стеке и временный указатель был сохранен в on_the_stack

Данный код аналогичен следующему:

let on_the_stack  =  Point {x: 3.0, y: 4.0};
let on_the_stack_pointer = &on_the_stack;

Типы отличные от стековых приводятся к временному указателям автоматически, без использования оператора взятия адреса.

let on_the_stack : Point  =  Point {x: 3.0, y: 4.0};
let managed_box  : @Point = @Point {x: 5.0, y: 1.0};
let owned_box    : ~Point = ~Point {x: 7.0, y: 9.0};

fn compute_distance(p1: &Point, p2: &Point) -> float {
    let x_d = p1.x - p2.x;
    let y_d = p1.y - p2.y;
    sqrt(x_d * x_d + y_d * y_d)
}

compute_distance(&on_the_stack, managed_box);
compute_distance(managed_box, owned_box);

Так же временные указатели могут указывать на внутренний элемент структуры данных.

let y = &point.y;

Контроль времени жизни временных указателей довольно объемная и не совсем устоявшаяся тема. При желании с ней можно подробно ознакомится в статье Rust Borrowed Pointers Tutorial и Lifetime Notation.

Разыменование

Для доступа к упакованным значениям необходимо проводить операцию разыменования. При доступе к полям структурированных объектов разыменование производится автоматически.

let managed = @10;
let owned = ~20;
let borrowed = &30;

let sum = *managed + *owned + *borrowed;

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

 
 

Буду очень благодарен за замечания и предложения по корректному переводу понятий:

  • managed box – на данный момент разделяемый указатель;
  • owned box – на данный момент уникальный указатель;
  • exchange heap – на данный момент хип обмена;
  • managed heap – на данный момент локальный хип;
  • ownership semantics – на данный момент семантика владения;
  • borrowed pointer – на данный момент временные указатели.

6 Comments Модель памяти Rust

  1. Bogdan Lytvynovskyi

    “Для создания разделяемых указателей используется унарный оператор ~” – надо поправить на “уникальных указателей”

    Reply
  2. Vitaly

    Спасибо за статью.
    Было бы интересно узнать, в чём преимущества подхода уникальных (захватываемых) указателей. Что произойдёт при попытке работать с деинециализированным (перехваченным) указателем?

    Reply
    1. Alexander Stavonin

      Я планирую еще написать на эту тему чуть позже.
      Ну а сейчас, то можно почитать линки что я привел и поглядеть юнит-тесты в папке с компилятором. К сожалению, на данный момент это лучшая доступная по языку информация.

      Reply

Leave a Reply to Alexander Stavonin Cancel reply