Введение в ARC/ORC в Nim

Nim переходит к более эффективным моделям управления памятью: ARC и ORC. Давайте узнаем, как именно они изменят работу с памятью в нём.



Введение


Всем привет! В этой статье я постараюсь рассказать, что такое ARC и ORC и как они повлияют на производительность или другие части Nim'а. Я не буду глубоко погружаться в аспекты программной части, а постараюсь дать более или менее высокоуровневое объяснение.


Давайте начнём издалека: Nim всегда был языком со сборщиком мусора (GC). Конечно же GC можно выключить, но тогда при работе с большей частью стандартной библиотеки (а она немаленькая) память будет утекать.


Стандартным GC в Nim уже долгое время является refc (отложенный подсчёт ссылок с mark & sweep фазой для сборки циклов), хотя доступны и другие варианты, как markAndSweep, boehm, go, regions.


За последние несколько лет у разработчиков Nim'а появилось несколько разных идей, связанных с деструкторами, собственными ссылками (owned ref) и так далее:



Из симбиоза этих идей получилось то, что в Nim называется ARC


Что такое ARC?


По своей сути ARC это модель управления памятью, основанная на автоматическом подсчёте ссылок (Automatic Reference Counting) с деструкторами и семантикой перемещений (move semantics). Можно заметить то, что ARC в Nim называется так же, как ARC в Swift, но есть одно больше различие — в Nim ARC не использует атомарный подсчёт ссылок.


Подсчёт ссылок остаётся одним из самых популярных алгоритмов для освобождения неиспользуемых ресурсов программы. Счётчик ссылок для любой управляемой (контролируемой runtime) ссылки показывает количество раз, которые ссылка используется в других частях программы. Когда этот счётчик становится нулевым, все данные по этой ссылке освобождаются.


Основным отличием ARC от остальных GC в Nim является то, что ARC полностью детерминированный — компилятор автоматически вставляет деструкторы в программу для удаления переменных (строк, последовательностей, ссылок, и т.д), которые больше не нужны. В этом смысле ARC похож на C++ с его деструкторами (RAII)


Для того, чтобы продемонстрировать этот процесс, мы используем интроспекцию ARC expandArc, которая доступна начиная с Nim 1.4.


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


proc main = 
  let mystr = stdin.readLine()

  case mystr
  of "привет":
    echo "Здравствуйте!"
  of "пока":
    echo "Удачи!"
    quit()
  else:
    discard

main()

И используем эту интроспекцию командой nim c --gc:arc --expandArc:main example.nim.


--expandArc: main

var mystr
try:
  mystr = readLine(stdin)
  case mystr
  of "привет":
    echo ["Здравствуйте!"]
  of "пока":
    echo ["Удачи!"]
    quit(0)
  else:
    discard
finally:
  `=destroy`(mystr)
-- end of expandArc ------------------------

Результат этой интроспекции довольно интересен — Nim завернул тело процедуры main в try: finally выражение (код в finally выполняется всегда, даже если внутри блока try было вызвано исключение) и вставил вызов =destroy для строки mystr, чтобы она уничтожилась после окончания её жизненного цикла.


Благодаря этому мы можем увидеть одну из главных возможностей ARC: управление памятью на основе областей видимости (scope-based MM). Область видимости — это любой отдельный регион кода в программе. Такое управление памятью означает, что компилятор автоматически вставит вызовы деструкторов везде, где это необходимо, после выхода из области видимости. Многие части Nim'а вводят новые области видимости: процедуры, функции, конвертеры, методы, конструкции с block, циклы for и while и так далее.


У ARC к тому же имеются так называемые hooks — специальные процедуры, которые можно привязывать к типам для того, чтобы перезаписать стандартное поведение компилятора при деструкции/перемещении/копировании переменной. Они являются очень полезными при создании нестандартных семантик для своих типов, работы с низкоуровневыми операциями, включающими сырые указателями, или для FFI.


По сравнению с refc ARC обладает немалым количеством преимуществ (включая упомянутые выше):



В целом, ARC для программ на Nim является отличным шагом вперёд, делающим их быстрее, уменьшающим потребление памяти, и давая им предсказуемое поведение.


Для того, чтобы включить ARC для вашей программы, всё, что нужно сделать, это скомпилировать её с ключом --gc:arc, или добавить его в конфигурационный файл вашего проекта (.nims или .cfg).


Проблема с циклами


Но подождите! Разве мы что-то не забыли? ARC проводит подсчёт ссылок, и, как известно, подсчёт ссылок не может освобождать циклы. Цикл — это отношение нескольких объектов, когда они все зависят друг от друга, и эта зависимость замкнута. Возьмём простой пример цикла: 3 объекта (A, B, C), у каждого их которых есть ссылка на другой объект, создают такую зависимость:



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


В Nim'е сборка циклов совершалась mark & sweep фазой refc GC, но лучше использовать ARC как основу для создания чего-то лучшего. Это приводит нас к:


ORC — сборщик циклов для Nim


ORC является совершенно новым сборщиком циклов, основанным на ARC. Его можно считать полноценным GC, так как он включает в себя фазу локального отслеживания (local tracing phase) в отличие от большинства других отслеживающих GC, которые проводят глобальное отслеживание (global tracing).


Для работы с async в Nim необходимо использовать ORC, потому что асинхронность в Nim'е образует циклы, которые необходимо собрать.


ORC сохраняет большую часть преимуществ ARC кроме детерминированности (частично) — по умолчанию у ORC есть адаптивный лимит для сборки циклов, и hard realtime (тоже частично) — по той же самой причине.


Для включения ORC вам нужно компилировать вашу программу с --gc:orc, но планируется, что в будущем ORC станет стандартным GC в Nim'е


Я заинтересовался! Как мне можно их протестировать?


Ничего сложного — вам всего лишь нужно поставить последнюю версию — Nim 1.4. Возможно эта версия уже доступна в ваших пакетных менеджерах.


Это всё! Спасибо за чтение данной статьи — я надеюсь, что она вам понравилась!


Источники / дополнительная информация:


@Doomet
16.10.2020 09:34 UTC
Первоисточник

Комментарии

@Doomet
16.10.2020 04:35 UTC
0
Для информации — данная статья является переводом моей собственной статьи, опубликованной в nim-lang.org/blog/2020/10/15/introduction-to-arc-orc-in-nim.html, при этом я немного дополнил некоторые моменты.

Об оригинале на английском проводились обсуждения на:
Reddit: reddit.com/r/programming/comments/jbkerv/introduction_to_arcorc_in_nim
Hacker News: news.ycombinator.com/item?id=24786649
@Doomet
16.10.2020 13:02 UTC
0
Обновление — вышел Nim 1.4 с ARC и ORC, которые я описал в статье.
nim-lang.org/blog/2020/10/16/version-140-released.html
@
16.10.2020 14:19 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@Doomet
16.10.2020 14:25 UTC
+1
У него нет никакой расшифровки, ORC просто подразумевает под собой работу с циклами (O обозначает замкнутый цикл)
@nin-jin
16.10.2020 15:37 UTC
-1

ORC выглядит как костыль. Героически боролись со сборщиками мусора со всеми их недостатками и… сделали ещё один сборщик мусора со всеми их недостатками. Чего они не смотрят в сторону Borrowing&Owning?

@xomachine
17.10.2020 09:29 UTC
0
А концепция владения решает проблему замкнутых циклов? Мне казалось, что это её слабое место как раз. Разве можно реализовать в рамках концепции владения, без привлечения подсчёта ссылок какой-нибудь двусвязный список?
17.10.2020 10:06 UTC
-1

Она их предотвращает. Никто не может владеть самим собой.

18.10.2020 07:50 UTC
0
Если я правильно Вас понял, то структуры, подобные двусвязным связным спискам и прочим циклическим графам реализовать в рамках концепции владения не получится. Но как быть если такая структура нужна?
18.10.2020 08:42 UTC
0

Первый элемент владеет вторым, второй — третьим и тд.

18.10.2020 11:19 UTC
+1

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


Ещё почитайте https://news.ycombinator.com/item?id=16443688, коротко говоря — в Rust довольно сложно делать циклические структуры по сравнению с большинством других языков.


Ещё вот — https://rcoh.me/posts/rust-linked-list-basically-impossible/

18.10.2020 12:08 UTC
-1

Последняя ссылка делается невладеющей и всё, какие проблемы?


У Раста свои тараканы. Насколько я понимаю, там нет поддержки наследования владения. Это когда компилятор понимает, что если ты владеешь объектом, который владеет другим объектом, то ты владеешь и вторым тоже. Хотя, кажется RefCell::borrow_mut — это вроде как раз способ ему это объяснить.

18.10.2020 13:44 UTC
+1
У Раста свои тараканы. Насколько я понимаю, там нет поддержки наследования владения. Это когда компилятор понимает, что если ты владеешь объектом, который владеет другим объектом, то ты владеешь и вторым тоже. Хотя, кажется RefCell::borrow_mut — это вроде как раз способ ему это объяснить.

Это не "наследование владения", а скорее транзитивность владения. RefCell::borrow_mut — это вообще про другое. Пожалуйста, не пишите о том, в чём настолько слабо разбираетесь.

18.10.2020 13:50 UTC
-1

Терминологические споры меня не интересуют.
Какая буква в слове "кажется" вам не понятна?
Не указывайте мне что делать и я не скажу, куда вам пойти.

@Doomet
17.10.2020 18:33 UTC
0
Как по мне, для Borrowing & Owning нужно изменять весь язык, что далеко не является лучшим решением :) С ARC/ORC изменений в код вообще вносить не нужно (кроме редких случаев, например порядка финализаторов, которые использовались с refc)
@Siemargl
19.10.2020 21:31 UTC
+1
Обсуждалось, но было отложено на потом как ломающее изменение — на Ним 2.0