Указатели и память
В Python указателей нет — есть ссылки, и управляет ими сборщик мусора.
В Mojo сборщика нет, а память кто-то должен освобождать. Обычно это
делают List, String и Dict, и трогать указатели напрямую
не приходится. Но иногда приходится.
Три указателя и один вопрос
Заголовок раздела «Три указателя и один вопрос»Выбор между ними сводится к одному вопросу: сколько у значения владельцев.
| Тип | Владельцев | Когда брать |
|---|---|---|
Pointer | ноль, это просто вид на чужое | сослаться на существующее значение |
OwnedPointer | ровно один | значение в куче с единственным хозяином |
ArcPointer | сколько угодно | одно значение читают из нескольких мест |
Pointer — вид на чужое
Заголовок раздела «Pointer — вид на чужое»Ничем не владеет и ничего не освобождает: просто указывает на значение, которое живёт где-то ещё.
from std.memory import Pointer
def main(): var counter = 10 var view = Pointer(to=counter) counter += 5 print(view[])15
Обратите внимание: view показывает текущее значение, а не копию
на момент создания. Компилятор при этом следит, чтобы указатель
не пережил то, на что смотрит, — это те самые origins из главы
Времена жизни.
OwnedPointer — единственный хозяин
Заголовок раздела «OwnedPointer — единственный хозяин»Кладёт значение в кучу и владеет им единолично. Скопировать нельзя:
error: value of type 'OwnedPointer[Buffer]' cannot be implicitly copied, it does not conform to 'ImplicitlyCopyable'
Копия означала бы второго владельца, а весь смысл типа — что владелец один. Освободили бы дважды.
Передайте владение через ^ — или возьмите ArcPointer, если
владельцев действительно нужно несколько.
Это прямой аналог Box из Rust и unique_ptr из C++.
ArcPointer — общее владение
Заголовок раздела «ArcPointer — общее владение»Хранит счётчик ссылок: значение живёт, пока на него смотрит хоть кто-то.
from std.memory import ArcPointer
@fieldwise_initstruct Config(Movable, Deinitable): var retries: Int
def main(): var config = ArcPointer(Config(3)) print("ссылок:", config.count()) var shared = config print("после копии:", config.count()) print("значение:", shared[].retries)ссылок: 1 после копии: 2 значение: 3
Счётчик — это цена. Каждое копирование и каждое уничтожение меняют его,
а для многопоточности это атомарная операция. Берите ArcPointer,
только когда владельцев действительно несколько.
Владеющая рекурсия не собирается
Заголовок раздела «Владеющая рекурсия не собирается»Классический способ построить список или дерево — поле, указывающее на такую же структуру. В Mojo это получается, но не с любым указателем. Владеющие не годятся.
С OwnedPointer ошибка приходит сразу, при разборе:
error: field 'next' has non-'Deinitable' type 'OwnedPointer[Node]'
Чтобы уничтожить Node, нужно уметь уничтожать OwnedPointer[Node],
для чего нужно уметь уничтожать Node. Круг не размыкается.
Сделать поле невладеющим — обычный Pointer, см. ниже.
С ArcPointer любопытнее. Само поле собирается прекрасно:
struct Node(Movable, Deinitable): var value: Int var next: ArcPointer[Node] # это компилируетсяПроверено: size_of[Node]() даёт 16, List[Node] создаётся
и уничтожается. Беда приходит, когда список нужно чем-то закончить.
Нулевого указателя в Mojo нет — «ничего» выражают через Optional,
и вот на нём раскладка ломается:
error: struct has recursive reference to itselfТак что владеющей рекурсии не выходит: без Optional список бесконечен,
с Optional не компилируется.
А невладеющая — выходит
Заголовок раздела «А невладеющая — выходит»Если поле только смотрит на другой узел, а память живёт отдельно,
всё работает. Узлы кладём в один блок, ссылки делаем обычными
Pointer:
comptime Link = Optional[Pointer[Node, MutUntrackedOrigin]]
struct Node(Movable, Deinitable): var value: Int var next: Link
def __init__(out self, value: Int, var next: Link): self.value = value self.next = next^MutUntrackedOrigin — встроенное имя, означающее «изменяемая память,
за временем жизни которой компилятор не следит». Именно такой origin
получает вручную выделенный блок, и именно поэтому круг размыкается:
Pointer ничем не владеет, уничтожать через него нечего.
Проще: индексы вместо указателей
Заголовок раздела «Проще: индексы вместо указателей»Если сложность рекурсии не нужна, узлы складывают в List, а вместо
ссылки хранят номер элемента:
@fieldwise_initstruct Item(Copyable, Movable): var value: Int var next: Int # -1 — конец списка
def main(): var items = List[Item]() items.append(Item(1, 1)) items.append(Item(2, -1))
var i = 0 while i >= 0: print(items[i].value) i = items[i].next1 2
Ручное выделение памяти
Заголовок раздела «Ручное выделение памяти»Если нужен сырой блок, его выделяют через alloc с описанием раскладки:
from std.memory import Layout, dealloc
def main(): var block = alloc(Layout[Int](count=3)) var raw = block.unsafe_ptr() for i in range(3): raw.unsafe_store(i, i * 100) print(raw.unsafe_load(0), raw.unsafe_load(2)) dealloc(block^)0 200
Забыть освобождение не выйдет — компилятор не даст:
error: 'block' abandoned without being explicitly destroyed
Блок памяти — это владеющее значение, и просто выйти из области видимости оно не может. Компилятор называет и причину, и оба выхода:
An `Allocation` owns heap storage and must be consumed before it goes out of scope.Deallocate it with `dealloc(allocation^)`, or call `unsafe_leak()` to take ownershipof the underlying pointer.dealloc(block^) — освободить. Либо unsafe_leak(), если
ответственность за память вы забираете себе.
Это заметно лучше, чем в C: утечка там — молчаливая ошибка времени выполнения, здесь — ошибка сборки.
Всё вместе
Заголовок раздела «Всё вместе»# Три вида указателей и одна задача, которую в Mojo 1.0 приходится# решать обходным путём.
from std.memory import Pointer, OwnedPointer, ArcPointer, Layout, dealloc
@fieldwise_initstruct Config(Movable, Deinitable): """Настройки, которые читают из нескольких мест."""
var retries: Int
@fieldwise_initstruct Buffer(Movable, Deinitable): """Буфер, у которого владелец ровно один."""
var size: Int
# Односвязный список в Mojo 1.0 через указатели не построить:# структура не может ссылаться на себя. Ссылки заменяем индексами.@fieldwise_initstruct Item(Copyable, Movable): var value: Int var next: Int # -1 — конец списка
def main(): print("1. Pointer — ссылка на уже существующее значение") var counter = 10 var view = Pointer(to=counter) counter += 5 print(" значение через указатель:", view[])
print("2. OwnedPointer — владелец ровно один") var buffer = OwnedPointer(Buffer(4096)) print(" размер буфера:", buffer[].size) # var copy = buffer — не соберётся: копировать владельца нельзя
print("3. ArcPointer — владельцев несколько, есть счётчик") var config = ArcPointer(Config(3)) print(" ссылок:", config.count()) var shared = config print(" после копии:", config.count()) print(" попыток из обеих копий:", config[].retries, shared[].retries)
print("4. Сырая память: выделили — обязаны освободить") var block = alloc(Layout[Int](count=3)) var raw = block.unsafe_ptr() for i in range(3): raw.unsafe_store(i, i * 100) print(" содержимое:", raw.unsafe_load(0), raw.unsafe_load(2)) dealloc(block^)
print("5. Список без указателей: ссылки — индексы массива") var items = List[Item]() items.append(Item(1, 1)) items.append(Item(2, 2)) items.append(Item(3, -1)) print(" обход:", end="") var i = 0 while i >= 0: print(" ", items[i].value, end="") i = items[i].next print()1. Pointer — ссылка на уже существующее значение значение через указатель: 15 2. OwnedPointer — владелец ровно один размер буфера: 4096 3. ArcPointer — владельцев несколько, есть счётчик ссылок: 1 после копии: 2 попыток из обеих копий: 3 3 4. Сырая память: выделили — обязаны освободить содержимое: 0 200 5. Список без указателей: ссылки — индексы массива обход: 1 2 3
🎯 Проверь себя
Чем `Pointer` отличается от `OwnedPointer`?
Владением. Pointer ничем не владеет и ничего не освобождает —
это вид на значение, которое живёт где-то ещё. OwnedPointer
кладёт значение в кучу и владеет им единолично, поэтому его нельзя
копировать: копия означала бы второго владельца и двойное
освобождение.
Почему `struct Node` с полем `next: OwnedPointer[Node]` не собирается, а с `Pointer[Node, MutUntrackedOrigin]` — собирается?
Дело во владении. Чтобы уничтожить Node, нужно уничтожить
OwnedPointer[Node], для чего нужно уничтожить Node — круг
не размыкается, и компилятор говорит field 'next' has non-'Deinitable' type. Невладеющий Pointer уничтожать нечего,
поэтому рекурсия проходит. Цена — снятые проверки времени жизни:
в имени MutUntrackedOrigin это сказано прямо.
`ArcPointer[Node]` как поле структуры компилируется. Почему тогда списка на нём не построить?
Потому что список надо чем-то заканчивать, а нулевого указателя
в Mojo нет — «ничего» выражают через Optional. И вот
Optional[ArcPointer[Node]] уже даёт struct has recursive reference to itself. Без Optional список бесконечен, с ним
не компилируется.
Компилятор советует заменить `alloc[Int](4)` на `unsafe_alloc`, но `from std.memory import unsafe_alloc` не работает. В чём дело?
В пути импорта: функция лежит в модуле, а не в пакете —
from std.memory.alloc import unsafe_alloc. Работать она будет,
но вернёт голый указатель без обёртки Allocation, а значит,
и без проверки «не забыли освободить». Для нового кода надёжнее
alloc(Layout[Int](count=4)) с dealloc(block^).
Что произойдёт, если забыть `dealloc`?
Программа не соберётся. Компилятор скажет abandoned without being explicitly destroyed и назовёт оба выхода: dealloc(block^) либо
unsafe_leak(). В отличие от C, утечка здесь — ошибка сборки,
а не молчаливая проблема времени выполнения.
Когда стоит брать `ArcPointer` вместо `OwnedPointer`?
Только когда владельцев действительно несколько и заранее неизвестно, кто закончит последним. Счётчик ссылок не бесплатен: каждое копирование и уничтожение его меняют, а в многопоточном коде это атомарная операция.
Что дальше
Заголовок раздела «Что дальше»Замыкания и лямбды: списки захвата, которые
мы уже видели у vectorize, — и почему без них компилятор отказывается
угадывать.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0