В текущей реализации бэкэнда OCaml WebAssembly представления значений в памяти очень похожи на таковые в других более традиционных бэкэндах OCaml. Это отлично работает, если вы можете реализовать свой собственный сборщик мусора, но делать это в WebAssembly довольно неэффективно, поскольку вы в основном дублируете усилия браузера. Нам нужно будет реализовать теневой стек рядом с фактическим стеком, что значительно увеличивает размер загрузки. Также взаимодействие с JavaScript будет далеко не идеальным. Итак, в идеале мы хотим, чтобы сборщик мусора согласовывался с браузером, и позволять браузеру обрабатывать сборщик мусора за нас. Однако, чтобы быть полностью справедливым, собственный сборщик мусора может быть быстрее.

Однако OCaml - не единственный управляемый язык, который должен поддерживаться WebAssembly. Фактически, спецификация WebAssembly GC определяет три основные категории: типизированные функциональные языки, такие как OCaml, объектно-ориентированные языки с номинальными подтипами, такие как Java, и нетипизированные языки, такие как Python. Вместе с требованиями безопасности в Интернете создание широкого решения для сборки мусора - определенно непростая задача. Другими словами: до прибытия сборщика мусора WebAssembly потребуется некоторое время. Однако это также дает возможность дать обратную связь.

В этой статье мы сначала рассмотрим, как управление памятью работает в OCaml, во-вторых, мы снова рассмотрим бэкэнд WebAssembly и, наконец, посмотрим, как бэкэнд WebAssembly, скорее всего, перейдет на WebAssembly GC.

Обратите внимание, что мы не собираемся обсуждать детали сборки мусора WebAssembly - только те части, которые имеют отношение к бэкэнду WebAssembly.

Управление памятью OCaml

Ценности

В OCaml куча сегментирована на блоки, где блок состоит из заголовка и значений. Заголовок описывает, каким должен быть блок, сколько значений он содержит и каков статус GC этого блока. Значения могут быть либо числами с плавающей запятой, либо целым числом. Значения помечены - они могут быть либо фактическим int, либо указателем на основе наименее значимого бита (последнего бита) значения.

Простой пример блока памяти для 32-битной архитектуры:

При переводе на WebAssembly следует отметить два важных момента:

  • int умещается в 32-битном формате и поэтому распаковывается. Если бы int был помещен в коробку, ему также потребовался бы указатель на другой блок памяти с заголовком.
  • указатель также умещается в 32-битном формате, и когда два указателя равны друг другу, это называется ссылочным равенством.

В идеале мы хотим сохранить эти свойства при переходе к управляемому решению.

Ассигнования

Всякий раз, когда выделяется часть памяти, она присоединяется либо к глобальным корням сборщика мусора, либо к локальным корням сборщика мусора. Все, что находится на верхнем уровне вашей программы, можно считать глобальным корнем сборщика мусора. Корни локального сборщика мусора - это соединения, которые стек вызовов имеет с выделенной частью памяти. Глобальные корни GC видны в CMM, локальные корни GC - нет. Важной частью здесь является то, что среда выполнения OCaml должна знать, какая память используется, а что можно освободить.

Вывоз мусора

Сборщик мусора использует ранее упомянутые корни GC для обхода графа памяти. Это происходит в двух разных фазах, фазе отметки и фазе развертки. Во время фазы метки вся память, которая все еще используется, помечается с помощью флага состояния GC, а во время фазы развертки память проверяется на наличие блоков, которые можно использовать повторно. Обратите внимание, что сборщик мусора OCaml является одним из лучших сборщиков мусора и часто лучше, чем рукописное решение для управления памятью.

Указатели

В CMM функции могут передавать указатели на другие функции. Когда необходимо получить фактические данные, за указателем загружается память. Обратите внимание, что указатели могут передавать несколько функций до того, как они будут фактически использованы. Имея только указатели на границах функций, трудно понять, каким должен быть соответствующий блок памяти до фактического использования. Также часто бывает, что не все значения блока памяти используются в одной и той же функции.

Бэкэнд WebAssembly

В настоящее время в бэкэнде WebAssembly память выделяется в доступной линейной памяти, и мы делаем вид, что у нас ее бесконечное количество. Ints и float преобразуются в int32 и float32 соответственно. Указатели также переводятся в int32. В целом это должно выглядеть очень похоже на то, что делают другие серверные ВМ.

Переход к WebAssembly GC

Контур

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

Ожидается, что в следующем году в Firefox и Chrome появится ступенька к GC (и нескольким другим спецификациям): ссылочные типы, которые представят anyref. Anyref можно считать заменой указателям. Как и в случае с указателями, неясно, что стоит за anyref. Anyref также позволяет импортировать что угодно из JavaScript в WebAssembly. Это не обязательно должен быть один из числовых типов.

Однако, чтобы перейти к чему-то более конкретному, вам нужно будет иметь возможность понижать значение anyref до другого типа. Это часть WebAssembly GC и все еще дорабатывается, но, грубо говоря, вам нужно будет указать типы, до которых может быть приведена ссылка на anyref. Типы, к которым будет преобразована anyref, будут структурами с i31ref и ссылками на другие структуры. Структуру можно рассматривать как замену блока памяти, а i31ref можно рассматривать как wasm-версию распакованного OCaml int. Однако обратите внимание, что сам i31ref концептуально является ссылкой, а не int.

Наш предыдущий пример блока памяти будет преобразован в эту структуру:

Обратите внимание, что больше нет заголовка блока, а также исчезли биты тега. Скорее всего, движок браузера добавит вам этот заголовок блока в качестве детали реализации.

Реализация

Первым прагматичным шагом на пути к WebAssembly GC является переход к ссылочным типам и реализация всех операций выделения GC и доступа к ним в JavaScript, вызываемых WebAssembly вместо будущих инструкций WebAssembly GC. Типы ссылок уже доступны в Firefox каждую ночь для экспериментов.

Это примерно набор того, что нужно как минимум сделать:

  • добавить тип anyref
  • использовать структурные особенности спецификации WebAssembly GC MVP вместо использования линейной памяти
  • переместить блоки данных также как-то в управляемую версию
  • удалить помеченные целые числа и связанные с ними битовые операции
  • игнорировать Cblockheader
  • и… заставить все это работать.

Чтобы увидеть примеры преобразования кода в WebAssembly GC, я создал репозиторий: https://github.com/SanderSpies/ocaml-wasm-gc-experimenting. Работа над этим все еще продолжается, и в ней немногое, но в следующем месяце будет добавлено больше примеров.

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

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