Design: Типизированные продолжения к стекам моделей

Созданный на 29 июл. 2020  ·  68Комментарии  ·  Источник: WebAssembly/design

Мотивация

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

  • легкие («зеленые») нити
  • сопрограммы
  • генераторы
  • асинхронный / ожидание
  • обработчики эффектов
  • вызов / cc

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

Вызовы

  1. Чтобы иметь возможность выражать такие функции, как зеленые потоки и другие, переключение стеков должно быть возможно на любой глубине стека, а не только в нижней части стека («глубокие сопрограммы»).

  2. Чтобы иметь возможность выражать такие функции, как генераторы, должна быть возможность передавать значения (назад и вперед!) При переключении на другой стек.

  3. Чтобы иметь возможность проверять использование стеков, оба стека и значения, передаваемые при переключении, должны быть правильно типизированы.

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

  5. Чтобы несколько из вышеперечисленных функций можно было использовать одновременно, они должны быть выражены _composable_ способом, допускающим произвольное вложение.

  6. Механизм не должен требовать копирования или перемещения кадров стека, поскольку некоторые движки не могут легко сделать это из-за неоднородной природы их стеков.

  7. Не должно быть возможности создавать циклические ссылки на стеки, поскольку для этого потребуется сборщик мусора.

Предложение

Один из известных способов решения вышеуказанных проблем - это рассматривать стеки несколько более абстрактно, используя понятие _дограниченного продолжения_.
Вообще говоря, это представляет собой «остаток» вычисления (продолжение) «до» определенной точки (с разделителями).
Конкретно, эта определенная точка является нижней частью определенного стека, в то время как кадры стека в стеке по существу описывают оставшиеся задачи, которые необходимо выполнить.

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

Как и в случае с асимметричными сопрограммами, переключение между стеками затем происходит с помощью пары инструкций возобновления и приостановки.
Инструкция приостановки «выбрасывает» значение, подобное исключению - мы называем это _событием_ ниже - которое обрабатывает возобновление.
Тег события, как и обычный тег исключения, определяет типы значений, передаваемых от точки приостановки до точки возобновления, а также значения, передаваемые обратно.

Подробности

Первое центральное дополнение - это новая форма возобновляемого исключения, также известного как событие.
Такое событие объявляется с соответствующей новой формой определения исключения.
(в двоичном формате это определение исключения с определенным битом флага):

(event $evt (param tp*) (result tr*))

В отличие от обычного исключения, события могут иметь возвращаемые значения.

Другое центральное дополнение - это новый (эталонный) тип продолжений:

(cont $ft)

где $ft - это индекс типа, обозначающий тип функции [t1*] -> [t2*] .
Интуитивно это описывает приостановленный стек, который возобновляется со значениями типов t1* и в конечном итоге завершается значениями типов t2* .

Создается новый стек с инструкцией

cont.new : [(ref $ft)] -> [(cont $ft)]

который принимает ссылку на функцию для запуска в новом стеке.
Выполнение не начинается до возобновления продолжения.

Инструкция

cont.resume (event $evt $handler)* : [t1* (cont $ft)] -> [t2*]

возобновляет такое продолжение, передав ему ожидаемые аргументы t1* .
Как только вычисление завершается, оно возвращается с t2* .
Если вычисление _suspends_ перед завершением (см. Следующую инструкцию) с одним из перечисленных тегов событий, то выполнение переходит к соответствующей метке $handler .
Это происходит _без раскрутки стека_.
Метка получает аргументы события tp* и продолжение типа (cont $ft') , где $ft' - это тип функции [tr*] -> [t2*] продолжения _next_.

Продолжение является "покадровым", то есть не может быть возобновлено более одного раза.
Попытка возобновить уже возобновленные ловушки продолжения.

Вычисление, выполняющееся в собственном стеке (т. Е. Инициированное как продолжение), может приостановить себя с помощью следующей инструкции:

cont.suspend $evt : [tp*] -> [tr*]

где $evt - событие соответствующего типа [tp*] -> [tr*] .
По сути, это передает управление обратно родительскому стеку.
То есть, как и throw исключение принимает аргументы tp* и передает управление самому внутреннему cont.resume или, скорее, его метке обработчика, соответственно.
Но, в отличие от обычных исключений, выполнение также может быть возобновлено, и здесь возвращаются значения типа tr* .

Как описано выше, самый внутренний активный обработчик, соответствующий тегу события, получает событие tp* и новое продолжение.
Возобновление этого продолжения (с cont.resume ), следовательно, вернет tr* исходному cont.suspend .

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

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

Продолжения - это первоклассные значения, которым разрешено избегать обработчика.
Таким образом, стек можно сохранить в рабочем состоянии, чтобы его можно было возобновить позже.
Время жизни стека заканчивается, когда последнее возобновление завершается регулярно (или за исключением, см. Ниже).
Управление временем жизни продолжений / стеков и evtrefs обычно требует подсчета ссылок в виртуальной машине.
Однако для них невозможно сформировать циклы, поэтому общая сборка мусора не требуется.

Наконец-то,

cont.throw $exn : [te* (cont $ft)] -> [t2*]

прерывает продолжение, вводя (обычное) исключение $exn с аргументами типа te* в точке приостановки.
Это способ явно раскрутить стек, связанный с продолжением.
На практике компилятор должен убедиться, что каждое продолжение используется линейно, т. Е. Либо возобновляется, либо прерывается с помощью throw.
Однако простого способа обеспечить это в системе типов нет, поэтому валидация Wasm не сможет проверить это ограничение.

Пример: простой генератор

Чтобы увидеть, как устроены эти механизмы, рассмотрите возможность кодирования генератора, перечисляющего целые числа i64 от 0 до тех пор, пока не будет выполнено определенное условие.
Потребитель может сообщить об этом условии, вернув генератору логическое значение (i32).
Это можно выразить следующим событием.

(event $enum-yield (param i64) (result i32)) 

Вот собственно генератор:

(func $enum-until
 (param $b i32)
  (local $n i64)

  (local.set $n (i64.const -1))

  (br_if 0 (local.get $b))
  (loop $l

    (local.set $n (i64.add (local.get $n) (i64.const 1)))

    (cont.suspend $enum-yield (local.get $n))

    (br_if $l)

  )

)

Вот один из возможных потребителей, который запускает генератор, пока не будет достигнуто заданное максимальное значение:

(func $run-upto (param $max i64)

  (local $n i64)

  (local $cont (cont (param i32)))
  (local.set $cont (cont.new $enum-until))

  (loop $l

    (block $h (result i64 (cont (param i32)))
      (cont.resume (event $enum-yield $h)
        (i64.ge_u (local.get $n) (local.get $max))

        (local.get $cont)
      )
      (return)
    )

    (local.set $cont)
    (local.set $n)
    ;; ...process $n...
    (br $l)
  )
)

Обратите внимание, как обработчик $h принимает как аргумент i64, созданный генератором, так и следующее продолжение.

Пример: простой планировщик потоков

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

(event $yield)
(event $spawn (param (ref $proc)))

куда

(type $proc (func))

Мы также предполагаем, что существует глобальная переменная, кодирующая очередь потоков в виде списка последовательностей:

(global $queue (list-of (cont $proc)) ...)

Точное представление не имеет значения, поэтому list-of - это просто псевдокод.
Все, что нам нужно, это три очевидные операции с этой очередью, игнорируя их детали:

(func $enqueue (param (cont $proc)) …)
(func $dequeue (result (cont $proc)) …)
(func $queue_empty (result i32) …)

Учитывая эти предпосылки, простой цикл планировщика может быть реализован следующим образом:

(func $scheduler (param $main (ref $proc))
  (cont.new (local.get $main)) (call $enqueue)
  (loop $l
    (if (call $queue_empty) (then (return)))   ;; program is done
    (block $on_yield (result (cont $proc))
      (block $on_spawn (result (ref $proc) (cont $proc))
        (call $dequeue)
        (cont.resume (event $yield $on_yield) (event $spawn $on_spawn))
        (br $l)                                ;; thread terminated
      )
      ;; on $spawn, proc and cont on stack
      (call $enqueue)                          ;; continuation of old thread
      (cont.new) (call $enqueue)               ;; new thread
      (br $l)
    )
    ;; on $yield, cont on stack
    (call $enqueue)
    (br $l)

  )

)

TODO: Еще примеры.

Взаимодействие с обычными исключениями

Регулярные исключения и события не пересекаются.
Обработчик исключений не будет перехватывать события, и наоборот.

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

С другой стороны, получение события не запускает обработчики исключений между точкой приостановки и точкой возобновления.
Они снова становятся активными, когда продолжение возобновляется, и выполнение, таким образом, переключается обратно в их стек.

Обратите внимание, что эту дихотомию можно, в принципе, использовать для реализации исключений с двухфазной раскруткой: первая фаза представлена ​​событием, обработчик которого затем вставляет правильное исключение для раскрутки. Однако такая реализация была бы довольно дорогой, поскольку каждый обработчик исключений должен был бы работать в собственном стеке.
Это все еще полезно в качестве семантического объяснения того, что _ должно_ произойти, и как двухфазная раскрутка будет взаимодействовать с однофазными исключениями.

Самый полезный комментарий

Ах, спасибо за разъяснение @sabine! Ссылка @rossberg выше должна быть полезна для понимания подхода, основанного на оценке контекста. @kayceesrk упоминает машину CEK, что является еще одним подходом. Он делает стек явной частью состояния машины, как и куча. Таким образом, если стек состояния машины выглядит как (cons K K '), то есть стек K' с указателем на другой стек K наверху, то переключатель стека изменяет стек состояния машины, чтобы он выглядел так (cons K ' К). Затем инструкция stack.switch делает нечто подобное, но дополнительно включает исключения для управления нетипизированной связью. Надеюсь, это поможет.

Все 68 Комментарий

Это идентично тому, что вы представили в прошлом @rossberg ? (Если не краткое изложение изменений, может быть полезно.)

@kripken , да, вплоть до переименования и мелких настроек.

Понятно , спасибо

Есть ли еще одно предложение о том, как поддерживать приложения проверки стека, например, двухэтапную обработку исключений, возобновляемые исключения, генераторы в стиле C # / Python, трассировку стека, сборку мусора с линейной памятью, дублирование стека?

cont.throw не принимает evtref , поэтому невозможно ответить на неизвестную приостановку за исключением. Это намеренно?

Новый тип функции $ft' : [tr*] -> [t2*] - это не тот тип, который задает модуль, а тот, который синтезируется из других типов. Разве это не значительно увеличивает объем работы во время проверки, поскольку синтезируемый тип должен быть проверен на совместимость с (более поздним) указанным типом функции?

@RossTate Разве это не поддерживает те, если косвенно, поскольку исключения возобновляются?

@taralx Я считаю, что вы правы, что все эти функции могут быть реализованы в рамках этого предложения. Однако для многих из этих функций выделение целого нового стека было бы излишним. Предложения по проверке стопки, двухфазной размотке и т. Д., По сути, являются частными случаями предлагаемой здесь функции, которую можно реализовать более эффективно.

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

Если оставить в стороне проверку стека, пример с легкими потоками кажется относительно неэффективной реализацией легких потоков. В идеале переключение между потоками включает всего несколько инструкций по переключению контекстов. Но в этом предложении переключение потоков включает обход стека для поиска соответствующего br_on_evt , что не обязательно дешево.

@taralx :

cont.throw не принимает evtref , поэтому невозможно ответить на неизвестную приостановку за исключением. Это намеренно?

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

Новый тип функции $ ft ': [tr ] -> [t2 ] - это не тот тип, который задает модуль, а тот, который синтезируется из других типов. Разве это не значительно увеличивает объем работы во время проверки, поскольку синтезируемый тип должен быть проверен на совместимость с (более поздним) указанным типом функции?

Я так не думаю, поскольку проверка уже должна иметь возможность обрабатывать несколько определений одного и того же типа функции как совместимые. Типы функций являются структурными даже в MVP.

FWIW, есть аналогичный вопрос относительно func.bind и других подобных инструкций. В настоящее время требуется, чтобы целевой тип был незамедлительным, чтобы обойти проблему, но на самом деле было бы достаточно (и дешевле проверить), если бы у него было просто числовое значение, которое указывает количество параметров для связывания и синтеза результирующего типа функции.

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

@RossTate Разве это не поддерживает те, если косвенно, поскольку исключения возобновляются?

Он делает, и, что более важно, делает это компонуемым образом - важный момент, который я забыл включить (добавил). То есть вы можете использовать несколько из этих функций одновременно, даже если они реализуются по отдельности.

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

@RossTate :

Если оставить в стороне проверку стека, пример с легкими потоками кажется относительно неэффективной реализацией легких потоков. В идеале переключение между потоками включает всего несколько инструкций по переключению контекстов. Но в этом предложении переключение потоков включает обход стека для поиска соответствующего br_on_evt , что не обязательно дешево.

Это не так. Обработчики связаны с точками возобновления, а не с ветвями. Следовательно, обработчик присутствует только «внизу» каждого неподвешенного стека, и нет необходимости обходить реальный стек. Более того, в каждом резюме декларируется список обрабатываемых тегов. Таким образом, передача управления обработчику может быть реализована, например, путем косвенного перехода через (локальную для потока) дополнительную таблицу, связывающую каждый тег события с его активным обработчиком.

Он делает, и, что более важно, делает это компонуемым образом - важный момент, который я забыл включить (добавил). То есть вы можете использовать несколько из этих функций одновременно, даже если они реализуются по отдельности.

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

однако есть один особый случай

Давайте рассмотрим первый такой «особый случай», который обсуждался четыре месяца назад : сборка мусора с линейной памятью. Среды выполнения и программы с собственным (линейным) управлением памятью обычно имеют способ проверить стек, чтобы найти и позже обновить корни сборки мусора по запросу. Эти программы теперь хотят компилироваться в WebAssembly и хотят каким-то образом проверять стек по запросу, чтобы найти и обновить эти корни (хотя и более безопасным способом в соответствии с требованиями безопасности WebAssembly).

Ваше предложение состоит в том, чтобы выделять новый стек в каждой точке, в которой кто-то захочет провести такую ​​проверку. Для этих приложений почти каждый кадр стека имеет такой корень и, следовательно, такую ​​потребность. Таким образом, ваше предложение состоит в том, чтобы эти приложения выделяли каждый кадр стека в своем собственном отдельном стеке. @aardappel , как человек, который особенно отстаивал эту функцию, думаете ли вы, что это удовлетворит ваши потребности?

Второй обсуждаемый «особый случай» - трассировка стека. Все наиболее часто используемые языки со сборкой мусора имеют некоторые средства для сбора / изучения трассировки стека (иногда даже лениво с использованием интерактивного обхода стека) во время выполнения даже в производственном режиме (а не только в режиме отладки). Опять же, почти каждый фрейм должен будет предоставлять информацию о трассировке стека, поэтому, опять же, в вашем предложении все эти языки должны размещать каждый фрейм стека в своем собственном отдельном стеке.

Более того, в каждом резюме декларируется список обрабатываемых тегов.

Ах, пропустил это изменение. Но есть ошибка. cont.forward также необходимо указать метку (обработчика) и список событий.

Да, и еще одна (не связанная с этим) ошибка: cont.throw необходимо учитывать, что продолжение может перехватить выброшенное исключение. Итак, у него есть тип вывода, и ему нужно указать метку (обработчик) и список событий.

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

Это предполагает конкретную стратегию реализации и инфраструктуру, по существу, принимая решения, которые часто будут приниматься на уровне приложения (например, должна ли эта переменная с динамической областью видимости быть реализована с использованием обхода стека или использования локального хранилища потока). Если мы собираемся предположить, что всегда есть инфраструктура для локального хранилища потоков, было бы неплохо включить эту функцию непосредственно в WebAssembly, поскольку она полезна не только для продолжений. Тем не менее, это означает, что в приведенных выше примерах каждый вызов и возврат (на языке поверхности) должен обновлять локальное хранилище потока. В общем, каждое приостановление / возобновление, пересылка и выброс должны обновлять локальное хранилище потока, что не требуется для обычных реализаций облегченных потоков.

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

Этот дизайн кажется особенно хорошо подходящим для реализации асимметричных совместных подпрограмм с полным стеком первого класса, так как он отображает относительное соотношение 1: 1 в соответствии с потребностями его реализации.

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

Этот механизм также кажется довольно тяжелым для типичного использования «генераторов», которые, как правило, работают больше как причудливые циклы for, чем что-либо еще. Должна быть какая-то гарантия того, что эту функциональность легко свернуть (без динамического выделения, повторного использования одного и того же стека и т. Д.), Чтобы это было возможно. Если оставить это на усмотрение двигателя, он станет бесполезным.

Это даже больше, если это будет полезно для проверки стека, как упоминает @RossTate . Даже если мы можем указать, что в простых случаях эти инструкции переводятся во что-то близкое к запрету операций, похоже, что это приведет к созданию гораздо более крупных двоичных файлов, чем более простое предложение по проверке стека. Для меня весь смысл наличия чего-то вроде функции проверки стека заключается в том, что она более эффективна, чем теневой стек, поэтому, если есть хотя бы небольшая вероятность, что это не так (например, требуется динамическое распределение), тогда она не будет использоваться. .

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

Еще одно соображение - как проверить (не считая эффективности) разграниченные продолжения. Например, диспетчер облегченных потоков, собирающий мусор, должен собирать (а затем обновлять) корни всех легких потоков. Учитывая конкретный cont представляющий полученный облегченный поток, неясно, как диспетчер потоков может разумно собирать корни cont , используя этот подход.

Я думаю, что по фундаментальным причинам производительности / реализации мы должны ожидать, что нам понадобятся две функции:

  1. возможность для wasm дешево создавать, приостанавливать и возобновлять легкий стек (зеленый поток)
  2. возможность wasm перебирать и проверять (и, возможно, изменять) локальные переменные фреймов в стеке без необходимости значительно изменять представление стека

Я думаю, что это предложение относится к 1, тогда как предложения по итерации стека относятся к 2. Очевидно, что они должны быть спроектированы для компоновки, но, насколько я могу судить из точки зрения perf / impl, я не думаю, что практично предполагать, что одно предложение собирается удовлетворить все наши варианты использования и достичь всех наших целей. Я также думаю, что относительные приоритеты этих функций различны: есть несколько высокоприоритетных вариантов использования для 1, тогда как по крайней мере один движок wasm специально озвучил намерение отложить 2 до тех пор, пока у нас не будет wasm GC.

Да, общий тезис в отзывах, которые мы получили для # 1340, заключался в том, чтобы отдавать приоритет первоклассным стекам над проверкой стека. Моя презентация на прошлой неделе заключалась в том, чтобы оценить, должна ли инспекция стека по-прежнему быть функцией в разработке, и, основываясь на онлайн- и офлайн-отзывах, ответ кажется вероятным «да» (при условии, что он появится после сборки мусора). Это важно знать для разработки первоклассных стеков, потому что так же, как инспекция стека может быть объединена с раскручиванием для поддержки двухфазной обработки исключений, инспекция стека может быть объединена с (ограниченным) отсоединением или перенаправлением для поддержки (одноразового использования) с разделителями. продолжения. Поэтому, если вы хотите «унифицировать представление о механизмах, связанных со стеком», вам необходимо принять во внимание инспекцию стека, чего в этом предложении нет.

@RossTate :

Он делает, и, что более важно, делает это компонуемым образом - важный момент, который я забыл включить (добавил). То есть вы можете использовать несколько из этих функций одновременно, даже если они реализуются по отдельности.

Да, и то же самое верно для любого другого предложения, которое было сделано для прямой поддержки этих функций.

Нет это не так. Это одно из главных нововведений в обработчиках эффектов и причина, по которой они были в восторге от PL, - это то, что они представляют собой композиционный механизм для определения эффектов, включая эффекты управления.

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

Я этого не предлагал. Единственное, что я упомянул, это то, что обработчики эффектов _ могут_ выражать эту форму проверки стека (и они сразу же объясняют, как это будет взаимодействовать с другими функциями управления). Но очевидно, что они будут подходить в качестве реальной стратегии только в том случае, если мы сможем надежно оптимизировать особый случай, о котором я упоминал, что может быть или не иметь место в этой настройке. Если нет, должна быть более явная альтернатива, которая может быть.

Но есть ошибка. cont.forward также необходимо указать метку (обработчика) и список событий.

Почему? Я не думаю, что это так.

Да, и еще одна (не связанная с этим) ошибка: cont.throw необходимо учитывать, что продолжение может перехватить сгенерированное исключение. Итак, у него есть тип вывода

Эх спасибо, ты прав, подписи перепутал. Фиксированный.

и он должен указать метку (обработчик) и список событий.

Мы могли это сделать. Но не ясно, будет ли это так полезно. Идея состоит в том, что cont.throw используется для «уничтожения» стека, когда он больше не должен быть завершен. По крайней мере, с перечисленными вариантами использования он больше не должен возвращаться к обработчику в этой ситуации. Но, может быть, есть какие-то странные варианты использования? Это одна из деталей, которую необходимо выяснить.

Это предполагает конкретную стратегию реализации и инфраструктуру, по сути, принимая решения, которые часто принимаются на уровне приложения.

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

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

Честно говоря, я не понимаю, что вы имеете в виду. AFAICT, это предложение не особо обсуждалось. Были разные дискуссии по поводу разных идей, и иногда я указывал на связь. Поправьте меня, если я ошибаюсь, но ни одна из этих недавних идей не была представлена ​​в качестве замены этой, поэтому ее полезность все еще остается в силе. Достаточно ли также включить некоторые из других (эффективно) - это отдельное обсуждение.

Поэтому, если вы хотите «унифицировать представление о механизмах, связанных со стеком», вам необходимо принять во внимание инспекцию стека, чего в этом предложении нет.

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

@aardappel :

Этот дизайн кажется особенно хорошо подходящим для реализации асимметричных совместных подпрограмм с полным стеком первого класса, так как он отображает относительное соотношение 1: 1 в соответствии с потребностями его реализации.

Ах, это может показаться так на первый взгляд, но это совсем не специфично и не оптимизировано для совместных подпрограмм. Его наследие - (алгебраические) обработчики эффектов, которые являются принципиальным способом выражения и составления почти любого вида состояния или эффекта управления.

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

Ах, возможность вложенного возобновления - вот что делает этот механизм компонуемым. Это то, что позволяет реализовать каждую абстракцию управления отдельно, например, вы можете запустить асинхронную функцию в зеленом потоке и по-прежнему уступать планировщику, не беспокоясь о промежуточных обработчиках. Это значительно упрощает реализацию, а также позволяет использовать абстракции элементов управления в гетерогенной среде с несколькими взаимодействующими языками.

Этот механизм также кажется довольно тяжелым для типичного использования «генераторов», которые, как правило, работают больше как причудливые циклы for, чем что-либо еще. Должна быть какая-то гарантия того, что эту функциональность легко свернуть (без динамического выделения, повторного использования одного и того же стека и т. Д.), Чтобы это было возможно. Если оставить это на усмотрение двигателя, он станет бесполезным.

Он намного менее тяжелый, чем вы думаете. У нас есть основания полагать, что реализация генераторов таким образом может быть такой же или даже более эффективной в среднем, чем обычный подход к построению конечного автомата, который часто включает дополнительный локальный поток управления для повторного входа в предыдущее состояние и требует копирования живых локальных переменных в кучу. на каждом урожае, или размещая их там в первую очередь. Кроме того, он сразу позволяет создавать глубокие генераторы (получаемые из вложенного вызова) без каких-либо дополнительных накладных расходов.

Описанный выше дизайн аналогичен дизайну обработчиков эффектов в Multicore OCaml. На практике накладные расходы на реализацию ряда абстракций от генераторов до облегченных потоков до async / await поверх обработчиков эффектов в Multicore OCaml близки к нулю.

Еще одно соображение - как проверить (не считая эффективности) разграниченные продолжения. Например, диспетчер облегченных потоков, собирающий мусор, должен собирать (а затем обновлять) корни всех легких потоков. Учитывая конкретный cont, представляющий полученный облегченный поток, неясно, как менеджер потоков может разумно собирать корни cont, используя этот подход.

Существует стандартный способ реализации этого, который делает Multicore OCaml, а также Go, GHC Haskell, Fibers в OpenJDK и других языках, использующих облегченные потоки. По сути, продолжения (заблокированные и готовые к запуску горутин в Go, заблокированные легкие потоки в GHC) становятся выделенным объектом в GC, и GC знает, как сканировать стек, связанный с продолжением. Приостановленные продолжения помечаются как обычные объекты. Для любого приостановленного продолжения, доступного из корней GC, GC сканирует стек, связанный с продолжением, так же, как он просматривает текущий стек программы.

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

Это одно из главных нововведений в обработчиках эффектов и причина, по которой они были в восторге от PL, - это то, что они представляют собой композиционный механизм для определения эффектов, включая эффекты управления.

Обработчики эффектов - это языковая функция поверхностного уровня . В зависимости от специфики обработчиков они могут быть скомпилированы разными способами. Хотя продолжения всегда являются жизнеспособным вариантом для любого обработчика, они, как правило, являются наименее эффективным вариантом. Даже в той работе, на которой вы основываете это, есть существенное обсуждение анализа и оптимизации для общих особых случаев идентификации и исключения, но многие из этих оптимизированных форм не выражены в этом предложении. Проблема в этом предложении состоит в том, что оно основано на концепциях высокого уровня, но не предоставляет соответствующие примитивы низкого уровня, с которыми эти концепции часто реализуются.

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

WebAssembly / обработка исключений # 105 была написана для описания этих низкоуровневых примитивов. Вы были явно информированы здесь , что она могла бы выразить алгебраические эффекты, но вы никогда не занимались в этом разговоре. # 1340 было предложением Фазы 0 для изучения этих идей. Но в этом разговоре были высказаны опасения по поводу безопасности, приоритетов, эффективности и сложности. В презентации, которую я сделал на прошлой неделе, были рассмотрены эти проблемы, связанные с проверкой стека, которые нам нужно было измерить, чтобы определить, как лучше всего решить проблемы, связанные с первоклассными стеками.

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

Эта более явная альтернатива уже много обсуждалась. Это проверка стопки, например, №1340 и №1356. Обратите внимание, что ваша инструкция cont.suspend имеет тот же тип, что и call_stack в # 1356. Разница в том, что call_stack не требует answer для выполнения распределения стека только для того, чтобы узнать основную информацию или внести базовые обновления в текущий стек. Но если вы объедините его с примитивами выделения стека и перенаправления стека, answer может выбрать использование продолжения, если это необходимо для поддержки функции поверхностного уровня под рукой, как мы показываем в # 1360. То есть cont.suspect разбивается на группу более мелких примитивных операций и не является самой примитивной операцией.

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

Будет ли это предложение совместимо с изменениями, которые я предложил в WebAssembly / обработка исключений # 123, а именно удалением exnref , введением catch_br и разделением catch и unwind ?

@aheejin Насколько я могу судить, он должен быть совместим с этими изменениями.

У почвы-Initiative / stacks # 10 теперь есть два перевода этого предложения в терминах # 1360, один с использованием проверки стека, а другой с использованием локального хранилища потока (при условии, что такая вещь существует на хосте и доступна для WebAssembly). У каждой стратегии реализации есть свои сильные и слабые стороны, и разные эффекты могут быть лучше выражены с помощью одного или другого (переводы составлены так, что разные эффекты могут фактически делать разные выборы даже в одном приложении). Одна из проблем этого предложения заключается в том, что оно не позволяет приложению выбирать собственную стратегию реализации; вместо этого он запекает высокоуровневую функцию и предоставляет движку решать / угадывать, какую стратегию использовать, подобно тому, как JVM запекает в диспетчере интерфейсных методов.

Пример планировщика зеленых потоков, показанный @rossberg на встрече 2020-08-04, появляется с добавлением обработки исключений для перехвата исключения из cont.resume достаточного для реализации планировщика для Lumen (компилятор / среда выполнения Erlang) .

На x86_64, где у нас есть собственное переключение стека, мы добавляем yield points (здесь закодированы как вызовы @__lumen_builtin_yield() ), когда каждый процесс Erlang (зеленый поток) израсходовал свой временной интервал (закодированный как @CURRENT_REDUCTION_COUNT достигает статического лимита)

init.erl

-module(init).
-export([start/0]).
-import(erlang, [display/1]).

start() ->
  display(atom).

init.llvm.mlir

module <strong i="19">@init</strong> {
  llvm.func @"erlang:display/1"(!llvm.i64) -> !llvm.i64
  llvm.func @__lumen_builtin_yield()
  llvm.mlir.global external local_exec @CURRENT_REDUCTION_COUNT() : !llvm.i32
  llvm.func @lumen_eh_personality(...) -> !llvm.i32
  llvm.func @"init:start/0"() -> !llvm.i64 attributes {personality = @lumen_eh_personality} {
    %0 = llvm.mlir.constant(20 : i32) : !llvm.i32
    %1 = llvm.mlir.addressof <strong i="20">@CURRENT_REDUCTION_COUNT</strong> : !llvm<"i32*">
    %2 = llvm.load %1 : !llvm<"i32*">
    %3 = llvm.icmp "uge" %2, %0 : !llvm.i32
    llvm.cond_br %3, ^bb1, ^bb2
  ^bb1: // pred: ^bb0
    llvm.call @__lumen_builtin_yield() : () -> ()
    llvm.br ^bb2
  ^bb2: // 2 preds: ^bb0, ^bb1
    %4 = llvm.mlir.constant(562949953421378 : i64) : !llvm.i64
    %5 = llvm.mlir.addressof <strong i="21">@CURRENT_REDUCTION_COUNT</strong> : !llvm<"i32*">
    %6 = llvm.mlir.constant(1 : i32) : !llvm.i32
    %7 = llvm.atomicrmw add %5, %6 monotonic  : !llvm.i32
    %8 = llvm.call @"erlang:display/1"(%4) {tail} : (!llvm.i64) -> !llvm.i64
    llvm.return %8 : !llvm.i64
  }
}

В BEAM для Erlang планировщики могут выполнять кражу. В презентации размахивали руками то, как хранятся продолжения. @rossberg Будет ли продолжение украдено / передаваться между веб-воркерами, чтобы планировщик для каждого веб-воркера (и один в основном потоке пользовательского интерфейса) мог украсть работу?

Обновлен OP в соответствии с версией, которую я представил сегодня, убрав отдельный тип evtref и инструкцию br_on_evt. Также удалена инструкция cont.forward, поскольку она может не понадобиться и ее семантика может сбивать с толку.

@kayceesrk Спасибо за разговор! Это было очень ясно и хорошо представлено.

В своем выступлении вы показали некоторый ассемблерный код о том, как вы реализовали алгебраические эффекты в Multicore OCaml. Я чувствую, что цель дизайна WebAssembly для стеков должна состоять в том, чтобы позволить программам WebAssembly по существу писать эти операции на уровне сборки самостоятельно, при условии, что функциональность может быть предоставлена ​​таким же образом компонуемым способом. Исходя из вашего опыта работы с Multicore OCaml, какие основные примитивы для этого?

На предыдущем заседании CG было предложено использовать эти продолжения для эффектов, которые будут иметь обработчик примерно в каждом кадре стека. Представленная вами стратегия реализации включала линейный обход обработчиков эффектов, что предполагает, что частое использование обработчиков эффектов повлияет на производительность. Вы проводили эксперименты в контексте таких эффектов? Если нет, и вы хотели бы попробовать конкретный пример, пропустите исходный код, чтобы каждый вызов функции имел обработчик, указывающий, в каком файле и номере строки находится вызов функции (или был включен в исходном исходном коде), чтобы Обработчики эффектов могут использоваться для реализации трассировки стека (отключение любых оптимизаций, которые необходимы для устранения выделения продолжений). Мне было бы интересно узнать, какова относительная производительность исходной и модифицированной программ.

@rossberg Это были очень

@rossberg Будет ли продолжение украдено / передаваться между веб-воркерами, чтобы планировщик для каждого веб-воркера (и один в основном потоке пользовательского интерфейса) мог украсть работу?

Я предполагаю, что продолжения не будут доступны для разных потоков в первой версии, независимо от того, какое предложение по переключению стека мы выберем.

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

@KronicDeth Я вижу, что Lumen использует LLVM. Использует ли он восходящий бэкэнд LLVM для WebAssembly? Если да, то какую поддержку вы ожидаете от бэкэнда для переключения стека (независимо от того, какое предложение мы используем)? Другими словами, как вы ожидаете использовать механизм переключения стека из LLVM? Как работает коммутация стека Lumen сегодня, если вообще работает?

@KronicDeth Я вижу, что Lumen использует LLVM. Использует ли он восходящий бэкэнд LLVM для WebAssembly?

У нас есть форк LLVM, и мы держим в курсе наших изменений здесь .

Если да, то какую поддержку вы ожидаете от бэкэнда для переключения стека (независимо от того, какое предложение мы используем)? Другими словами, как вы ожидаете использовать механизм переключения стека из LLVM?

Что-то, что может быть расположено в том же месте, где мы называем __lumen_builtin_yield было бы лучше всего, поскольку это означает отсутствие расхождений между кодом переключения x86_64 и WASM. @bitwalker есть ли у вас другие предпочтения для переключения стека в WASM в LLVM IR или LLVM MLIR?

Как работает коммутация стека Lumen сегодня, если вообще работает?

__lumen_builtin_yield вызывает кучу вещей для работы с очередями планировщика ([ 1 ], [ 2 ]), но для фактического обмена это встроенная сборка . Это все для цели x86_64.

Для цели WASM у нас еще нет компилятора, у нас есть среда выполнения, но планировщик и переключение все еще написаны вручную на Rust.

Спасибо за отличные указатели, @KronicDeth!

Если я правильно читаю вашу текущую реализацию __lumen_builtin_yield , похоже, что она работает, поддерживая локальный для потока список процессов (который кажется хранилищем состояний регистров, включая указатели стека и кода), получение следующего готового процесса из этого списка, а затем переключение регистров (включая указатели стека и кода) после некоторой базовой бухгалтерии. Это точное резюме?

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

Изменить: на самом деле, есть небольшая разница. В нашем примере мы переключаемся на стек менеджера, который затем определяет следующий процесс, который нужно запустить, и переключается на этот стек. Ваша реализация находит следующий процесс в стеке производящего потока, вместо того, чтобы иметь средний переключатель к менеджеру. Конечно, наш пример легко изменить, чтобы использовать эту оптимизацию. С другой стороны, здесь всегда будет требоваться два коммутатора стека (и обход стека).

@kayceesrk Я добавил перевод этого дизайна в почвенную инициативу / stacks # 10, используя стратегию реализации Multicore OCaml, которую вы реализовали сегодня. Мне интересно услышать, считаете ли вы, что он точно передает то, что вы описали. Я понятия не имею, насколько свободно вы владеете WebAssembly, особенно в отношении соответствующих молодых / псевдопредложений, поэтому я был бы рад встретиться с вами и пройти с вами перевод.

Помимо этого, я должен отметить одну вещь: ваша стратегия реализации предполагает, что всегда есть возможность быстро определить адрес корня стека на основе адреса его текущего листа. Это вполне правдоподобное предположение; Я просто делаю это явным, поскольку он накладывает ограничение на реализацию. Кроме того, это предположение имеет и другие полезные последствия для пространства проектирования. В частности, он включает вариант проверки стека, который особенно полезен для вашей стратегии реализации, что я использую в почвенной инициативе / стеки № 10.

@rossberg рад видеть это движение. Одна мелочь: не должно быть

Не должно быть возможности создавать циклические ссылки на стеки, поскольку для этого потребуется сборщик мусора.

?

@ b-studios, ой, да, исправлено. Спасибо!

Спасибо, @RossTate.

На предыдущем заседании CG было предложено использовать эти продолжения для эффектов, которые будут иметь обработчик примерно в каждом кадре стека.

Это было бы неразумно. Обработчики эффектов не следует использовать для трассировки стека. Проверка стека и переключение стека являются ортогональными функциями. Я не могу понять, почему одно _ требует_ другого или зачем использовать одно для _симуляции_ другого. Как я уже говорил в своем выступлении, реализация обработчика эффектов не проверяет каждый кадр стека. Мы напрямую загружаем родительский стек за постоянное время. Сравните это с исключениями. Если бы я реализовал исключения, откручивание одного кадра стека за раз для достижения обработчика исключений привело бы к ужасной реализации. Я честно считаю, что проверка стека не имеет ничего общего с переключением стека и должна быть реализована как независимая функция.

Реалистичные примеры обработчиков, которые мы видели на практике, никогда не бывают очень глубокими. Обычно имеется обработчик в нижней части управляющего планировщиком и, возможно, один или два обработчика в верхней части для поддержки генераторов и других инверсий локального управления. Следовательно, линейный поиск в стеке обработчиков не является проблемой. Более того, представленное вчера доработанное предложение удаляет инструкции по тестированию тегов. В отличие от OCaml, который допускает произвольное сопоставление шаблонов полезных данных, тесты тегов могут быть реализованы непосредственно в движках. Механизмы могут просто поддерживать стек текущих тегов и меток для перехода, чтобы найти соответствующий тег и перейти к нему. Я понимаю, что есть некоторые оговорки против пропаганды политики реализации. Но, по нашему опыту, для реальных примеров, когда количество обработчиков невелико, линейный поиск также будет работать. В течение нескольких лет я работал над обработчиками продолжений / эффектов с разделителями, мне еще предстоит увидеть _useful_ пример, в котором в стеке есть много экземпляров одного и того же обработчика.

Я чувствую, что цель дизайна WebAssembly для стеков должна состоять в том, чтобы позволить программам WebAssembly по существу писать эти операции на уровне сборки самостоятельно, при условии, что функциональность может быть предоставлена ​​таким же образом компонуемым способом. Исходя из вашего опыта работы с Multicore OCaml, какие основные примитивы для этого?

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

  1. локальное стековое хранилище (для хранения родительских указателей, таблицы событий и соответствующих меток для перехода),
  2. возможность добавлять вычисления к приостановленному стеку (чтобы тело соответствующего обработчика можно было оценить в родительском стеке, что-то вроде MLton.Thread.prepend [1]) и
  3. механизм переключения между стеками.

Одно из преимуществ этого позволяет функции обработчика, в которой (а) продолжение не прерывается и (б) возобновляется в хвостовой позиции, может оцениваться поверх текущего стека, который выполняет эффект. Если все события / эффекты, связанные с обработчиком, имеют свойства (a) и (b), то мы могли бы избежать выделения отдельного стека для выполнения вычислений. Выгода от возобновления хвоста может перевесить сложность определения того, что (a) и (b) выполняются + сложность реализации этой оптимизации; @daanx может иметь доказательства преимуществ возобновления хвоста.

Тем не менее, у меня есть две оговорки против этого предложения. Во-первых, я не знаю, можем ли мы получить ту же статическую семантику, что и обработчики эффектов (тем не менее, это интересный вопрос). Меня действительно волнует статическая семантика обработчиков, поскольку они наименее удивительны среди всех операторов управления с разделителями. Учитывая, что существует интерес к созданию формальных основ WebAssembly [2], который включает в себя как компиляцию в WebAssembly, так и ее самого, наличие сильной статической семантики для разделенных продолжений дает вам возможность рассуждать о любом количестве вещей, которые вы, возможно, захотите сделать с разделенными продолжениями. . @slindley , @daanx и @matijapretnar знают об этом намного больше, чем я.

Во-вторых, известно, что обработчики эффектов достаточно общие, чтобы описывать все другие операторы управления и полезные высокоуровневые парадигмы (у @slindley и @matijapretnar есть статьи по этому

[1] http://www.mlton.org/MLtonThread
[2] https://www.dagstuhl.de/en/program/calendar/semhp/?semnr=21012

Вы могли бы получить ту же динамическую семантику, введя

  1. локальное стековое хранилище (для хранения родительских указателей, таблицы событий и соответствующих меток для перехода),
  2. возможность добавлять вычисления к приостановленному стеку (чтобы тело соответствующего обработчика можно было оценить в родительском стеке, что-то вроде MLton.Thread.prepend [1]) и
  3. механизм переключения между стеками.

Кроме того, (1) будет зависеть от наличия как первоклассных меток, так и (2) от некоторой формы замыканий. Кроме того, неясно, как сделать любой из этих типов безопасным.

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

@kayceesrk Спасибо за подробный пост! В нем много полезных точек зрения и продуктивных моментов для обсуждения.

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

Согласовано. Однако их конструкции не могут быть полностью независимыми. Обычно при проверке стека требуется пройти по цепочке продолжения. Это вызывает ряд вопросов о том, как эти две функции взаимодействуют, как они могут влиять друг на друга и как они могут способствовать друг другу. Я могу отвлечься от различных примеров, но озабоченность на высоком уровне заключается в том, что это предложение не учло это.

Я должен предварить свой ответ, сказав, что я все еще развиваю вкус к правильному уровню абстракции в WebAssembly.

Я могу полностью понять это, поскольку сам был там (и даже иногда ошибаюсь, несмотря на большую практику). Однако это суть проблемы с этим предложением.

Не думаю, что я видел какие-либо опасения по поводу концепций высокого уровня этого предложения. То есть я не вижу никаких возражений против поддержки алгебраических эффектов или против свойств композиции алгебраических эффектов. Беспокойство вызывает дизайн предложения на нижнем уровне .

Во-вторых, известно, что обработчики эффектов достаточно общие, чтобы описывать все другие операторы управления и полезные высокоуровневые парадигмы (у @slindley и @matijapretnar есть статьи по этому

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

Например, stack.suspend - это две операции, объединенные в одну: найти обработчик и переключить стек. Как мы уже обсуждали, в этом предложении можно выразить трассировку стека, но она не подходит. Однако, если операция «найти обработчик» была выделена из stack.suspend в собственный примитив, то есть проверку стека, то ее можно было бы хорошо реализовать. Вы также упомянули наличие планировщика в корне стека. Этот метод реализации легких потоков известен как батут. Его обратная сторона заключается в том, что он включает в себя два переключателя стека (и обход стека) для передачи управления от одного потока к другому. Если вы отделите часть «переключение стека» от stack.suspend , тогда реализация облегченных потоков с использованием заранее определенного планировщика может позволить производящему потоку просто запустить сам код планировщика, чтобы найти следующий поток, а затем переключиться непосредственно на стек этого потока. Кажется, именно так и реализован Lumen.

Выгода от возобновления хвоста может перевесить сложность определения того, что (a) и (b) выполняются + сложность реализации этой оптимизации; @daanx может иметь доказательства преимуществ возобновления хвоста.

Это еще один пример того, что это предложение находится на неправильном уровне абстракции для WebAssembly. @daanx не нужно убеждать меня в преимуществах этой оптимизации, которая применима ко многим обработчикам; Я уже прочитал соответствующий текст. Но это предложение фактически не поддерживает эту оптимизацию!

В случае WebAssembly ожидается, что движок выполнит довольно простую компиляцию инструкций. Обычно он выполняет некоторые локальные оптимизации, но не предполагает каких-либо крупномасштабных рассуждений. В частности, движок WebAssembly определенно не будет проверять, выполняются ли (a) и (b), а затем выполнять эту оптимизацию. Вместо этого ожидается, что приложение выполнит это рассуждение и сгенерирует инструкции WebAssembly, которые непосредственно выполняют этот оптимизированный код. Проблема в том, что эти оптимизированные инструкции не могут быть выражены в этом дизайне. Поэтому, если вы или @daanx считаете, что эта оптимизация полезна для ваших целей, вам следует знать, что это предложение на самом деле не соответствует вашим собственным целям.

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

Полезно дать приложению, компилируемому в WebAssembly, свободу указывать его реализацию в терминах низкого уровня. Это позволяет приложению включать оптимизацию для конкретного приложения и помогает приложению (или компилятору) разумно прогнозировать компромиссы в стратегиях реализации. Это также избавляет WebAssembly от необходимости предвидеть или добавлять расширения для поддержки различных оптимизаций, которые приложения могли бы выполнять сами, если бы у них был контроль более низкого уровня. Например, дизайн в # 1360 смог поддержать каждую из многих стратегий реализации, которые обсуждались для этого предложения, без изменений . Он даже может поддерживать оптимизацию с возобновлением хвоста, о которой говорилось выше. (Это всего лишь незначительное изменение тематического исследования на основе заголовка в почвенной инициативе / стеках # 10.)

Конечно, слишком большая свобода может быть проблематичной. Но дизайн в # 1360 хорош и обладает свойствами композиционности алгебраических эффектов, но при этом находится на более низком уровне. У него есть эти свойства композиционности, потому что его дизайн фактически начался с этого предложения (а затем прошел через множество итераций понижения при сохранении этих свойств).

проверка стека и обработчики эффектов

Согласовано. Однако их конструкции не могут быть полностью независимыми. Обычно при проверке стека требуется пройти по цепочке продолжения. Это вызывает ряд вопросов о том, как эти две функции взаимодействуют, как они могут влиять друг на друга и как они могут способствовать друг другу. Я могу отвлечься от различных примеров, но озабоченность на высоком уровне заключается в том, что это предложение не учло это.

Это правда, что есть взаимодействие, в котором нужно разобраться, но само взаимодействие, на мой взгляд, довольно простое. Вместо того, чтобы ваш стек вызовов был одним непрерывным стеком кадров, с обработчиками эффектов, он становится стеком стеков. Инспекционный центр штабеля может быть легко проинформирован о новой компоновке штабеля. Вот два свидетельства от Multicore OCaml.

Например, стандартный (восходящий) OCaml использует системный стек для функций OCaml, а стек вызовов содержит как кадры OCaml, так и кадры C. OCaml имеет единственное средство проверки стека, которое просматривает каждый из фреймов OCaml для извлечения корней в этом фрейме и пропускает последовательность C-фреймов, в которых корни не найдены. В Multicore OCaml нам пришлось расширить эту возможность проверки стека, чтобы обрабатывать стеки стеков, появившихся из-за обработчиков эффектов. Все, что нам нужно было сделать, это расширить средство проверки стека, чтобы сказать: «Как только вы дойдете до конца стека во время проверки стека, выберите родительский стек и продолжите проверку родительского стека». Это модульное изменение, которое не влияет на работу проверки стека в любом заданном кадре.

Фактически, у нас есть свидетельства того, что вы можете получить информацию о новом макете стека в Multicore OCaml для универсального средства проверки стека, такого как libunwind, благодаря выразительности DWARF. Наше описание DWARF для стеков описывает, как найти родительский стек и продолжить проверку. Вот пример использования GDB для получения трассировки программы с обработчиками: https://github.com/ocamllabs/ocaml-effects-tutorial#31 -examining-effect-handlers-through-gdb.

Опыт переписывания собственного средства проверки стека OCaml для обработки стеков стеков и описания макета стека в общих настройках, таких как DWARF, заставляет меня поверить, что любое предложение проверки стека может быть осведомлено об обработчиках эффектов с минимальными усилиями.

@kayceesrk Я добавил перевод этого дизайна в почвенную инициативу / stacks # 10, используя стратегию реализации Multicore OCaml, которую вы реализовали сегодня. Мне интересно услышать, считаете ли вы, что он точно передает то, что вы описали. Я понятия не имею, насколько свободно вы владеете WebAssembly, особенно в отношении соответствующих молодых / псевдопредложений, поэтому я был бы рад встретиться с вами и пройти с вами перевод.

Спасибо. Это было бы полезно для меня более детального понимания # 1360. Остальная часть недели для меня немного занята. Я займусь дизайном в начале следующей недели. Я плохо владею WebAssembly, и мне может понадобиться ваша помощь в переваривании кодировки.

Я думал, что @lukewagner хорошо уловил суть # 1359 и # 1360 в https://github.com/WebAssembly/design/issues/1359#issuecomment -666583483. Если я правильно понимаю, это предложение сохраняет инспекцию стека ортогональной переключению стека, тогда как # 1360 делает ее взаимозависимой. Опыт Multicore OCaml показывает, что взаимодействие между проверкой стека и переключением можно описать по модулю без необходимости значительного изменения структуры стека. Более того, для низкоуровневого API, который я здесь набросал https://github.com/WebAssembly/design/issues/1359#issuecomment -669203944, требуются такие функции, как первоклассные метки и замыкания, которые в настоящее время недоступны в WebAssembly. Cегодня.

Мне кажется неразумным ставить высокоприоритетную функцию, такую ​​как переключение стека, в зависимость от многих других функций (проверка стека, локальное состояние стека, первоклассные метки, замыкания), объем которых намного шире, чем переключение стека. Например, я мог бы использовать те же аргументы против обработчиков эффектов, чтобы сказать, что исключения слишком высокоуровневые. Вместо того, чтобы иметь исключения, как они есть сегодня, мы не можем утверждать, что исключения должны быть реализованы с другими функциями, такими как проверка стека (чтобы найти, где обработчик исключений находится в стеке), локальное состояние стека (для отображения стека теги исключений в динамической области для целевых меток) и метки первого класса (для создания таблицы в локальном состоянии стека)?

Я плохо владею WebAssembly, и мне может понадобиться ваша помощь в переваривании кодировки.

Рад помочь. Не стесняйтесь отправить мне электронное письмо, чтобы согласовать время.

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

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

Что касается проблем, различные приложения проверки стека должны иметь возможность выполнять код конкретного приложения во время процесса, и важно, чтобы этот код семантически считался выполняемым на конце стека (цепочки), даже если проверка стека в настоящее время изучает фрейм в стеке выше по цепочке. (Это также важно для компоновки.) Итак, что должно произойти, если этот код приостановит продолжение, и что, если соответствующий обработчик находится где-то в цепочке стека между его листом и текущим проверяемым кадром? Отсоединять продолжение в этом месте было бы неразумно. С помощью высокоуровневого дизайна, подобного этому предложению, мы должны прописать универсальную политику того, что делать в этой ситуации. В низкоуровневом дизайне приложению предоставляется достаточно контроля, чтобы определять свою собственную политику.

Что касается облегчения, в вашем исследовании вы все время упоминаете различные оптимизации, показывающие, как исключить выделение продолжений для определенных общих классов обработчиков. Если вы кодируете инспекцию стека и первоклассные стеки, вы можете использовать инспекцию стека для поиска обработчиков эффектов. В оптимизируемых случаях конкретный обработчик эффекта может быть реализован как простой «ответ» на проверку стека. В неоптимизируемых случаях «ответ» на проверку стека может позаботиться об отсоединении (ранее выделенного / присоединенного) продолжения. (И используя вариант проверки стека eager описанный в почвенной инициативе / stacks # 10, вы можете избежать необходимости обходить стек кадр за кадром на двигателях, которые имеют возможность легко получить доступ к заголовку файла стек с любой точки листа.)

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

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

  • Я не уверен, как работает cont.throw . Используется ли он изнутри продолжения, которое следует остановить, например cont.suspend , или извне, например cont.resume ? Я предполагаю последнее, и если это верно, каков эффект инструкции? Считается ли это throw , поэтому, если мы не хотим распространять исключение до верхнего кадра и сбоя, следует ли нам обернуть его try ~ catch , например
  cont.throw $some_continuation
catch
  do nothing, or do some wrap up
end

?
Будет полезен пример кода с использованием этой инструкции.

  • Теперь, когда в этом предложении нет evtref , мне было интересно, может ли предложение EH быть более похожим на это по синтаксису, что означает, что мы не используем exnref , а как cont.resume , один catch принимает несколько событий и соответствующих меток для перехода. Возможно, это не является предметом данной проблемы, но если у вас есть какие-либо мнения, я буду признателен.

@aheejin , cont.throw является родственником cont.resume , но вместо возобновления со значением он возобновляется с исключением в точке, где произошла предыдущая приостановка. Если в этом стеке нет обработчика исключений, который мог бы перехватить это исключение, оно действительно будет распространяться обратно на cont.throw и далее.

Рассмотреть возможность:

(event $evt)
(exception $exn)

(func $f1
  (cont.suspend $evt)
)

(func $main
  (local $c1 (cont))
  (local.set $c1 (cont.new (ref.func $f1)))
  (try
    (do
      (block $h (result (cont))
        (cont.resume (event $evt $h) (local.get $c1))
        (unreachable)  ;; we never get back here
      )
      (cont.throw $exn)  ;; cont is on stack; this throws
      (unreachable)  ;; we never get back here either
    )
    (catch (nop))  ;; instead, we get here
  )
)

Сравнить с:

(func $f2
  (try
    (do (cont.suspend $evt))
    (catch (return))  ;; swallow exception
  )
)

(func $main
  (local $c1 (cont))
  (local.set $c2 (cont.new (ref.func $f2)))
  (try
    (do
      (block $h (result (cont))
        (con.resume (event $evt $h) (local.get $c2))
        (unreachable)  ;; we never get back here
      )
      (cont.throw $exn)  ;; cont is on stack; continuation catches the exception before returning
      (unreachable)  ;; so we get back here
    )
    (catch (nop))  ;; but never here
  )
)

Теперь, когда у этого предложения нет и evtref, мне было интересно, может ли предложение EH быть более похожим на это по синтаксису, что означает, что мы не используем exnref, и, как и cont.resume, один улов принимает несколько событий и соответствующих меток прыгать. Возможно, это не является предметом данной проблемы, но если у вас есть какие-либо мнения, я буду признателен.

Да, я согласен с тем, что если мы примем это предложение, то будет иметь смысл согласовать его с исключениями. В частности, catch может (необязательно) получить список обработчиков, аналогичный resume . Однако возникает вопрос, как таким образом моделировать комплексные обработчики и rethrow .

@kayceesrk @daanx Я заметил, что обе ваши работы используют глубокую семантику, но в этом предложении используется поверхностная семантика. Я знаю, что глубокая семантика может быть закодирована с помощью поверхностной семантики, но их прямая реализация более эффективна, чем это кодирование. Есть ли причина, по которой вам не нужна прямая поддержка глубокой семантики? (Обратите внимание, что наличие такой поддержки не исключает прямой поддержки неглубокой семантики; хороший низкоуровневый дизайн должен иметь возможность напрямую поддерживать оба из них через более простые примитивы. # 1390, например, может напрямую поддерживать оба, в то же время гарантируя композиционность, например, семантика одного эффекта глубокая, а семантика другого неглубокая, не требуя какой-либо координации между ними.)

@RossTate , предлагаемый механизм более низкоуровневый, чем обычные обработчики эффектов. В частности, создание стека отделено от установки обработчика. Таким образом, мы считаем, что он может эффективно кодировать как поверхностные, так и глубокие обработчики, в отличие от любого из двух других.

Простая низкоуровневая реализация мелких обработчиков состоит в том, чтобы выделить стек для дескриптора / дочернего элемента без кода обработчика в стеке, а затем указать код обработки в текущем / родительском стеке. Это именно то, что делает текущее предложение. Чтобы закодировать глубокие обработчики с мелкими обработчиками, убедитесь, что вы всегда запускаете продолжение с соответствующим обработчиком вокруг него, что, опять же, именно так, как текущее предложение косвенно поддерживает глубокие обработчики. Текущее предложение не особо низкоуровневое; он встроен в специальные мелкие обработчики, но случается так, что это делается в WebAssembly.

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

В отношении обработчиков эффектов высокого уровня я понимаю следующее: @slindley , @dhil и @kayceesrk, пожалуйста, поправьте меня, если я ошибаюсь. Даниил & Сэм бумага для более подробной информации.

  • В своей высокоуровневой форме мелкие и глубокие обработчики имеют разные компромиссы в производительности, и ни один из подходов не превосходит другой: глубокий - это складка, мелкий - случай. Либо один из них лучше справляется с определенным набором абстракций.

  • Выразить глубокие обработчики в терминах мелких обработчиков довольно просто: вам просто нужно вручную повторно ввести обработчик в цикле. Причина, по которой это может быть дорого, в основном состоит в том, что он наивно создает дополнительный стек при каждом возобновлении. - Но это не так в предлагаемом нами низкоуровневом механизме, потому что мы делаем создание стека отдельной операцией, так что один и тот же стек можно использовать повторно. Единственная стоимость - переключение стека и обработчика, что может быть дешевым, как показывают числа

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

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

Единственная стоимость - переключение стека и обработчика, что может быть дешевым, как показывают числа

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

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

Но это не так в предлагаемом нами низкоуровневом механизме, потому что мы делаем создание стека отдельной операцией, так что один и тот же стек можно использовать повторно.

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

Почему не рассматривался такой вариант?

Вы, кажется, делаете много заявлений об альтернативном дизайне. Есть ли у вас какие-либо эмпирические доказательства в пользу любого из них?

Что ж, ни у кого нет никаких эмпирических свидетельств в пользу любого из этих проектов. Но я могу сказать, что # 1360 поддерживает оптимизацию для обработчиков хвостовых эффектов - это означает, что он может выражать более оптимизированный заниженный код, который допускают обработчики хвостовых эффектов - и как таковой он может устранить 2 26 переключателя стека в @kayceesrk. пример для генераторов в зависимости от того, как потребляются сгенерированные значения. В частности, генераторы очень часто используются конструкциями for each , которые кодируются как обработчики эффекта с резюмированием хвоста, и которые, следовательно, # 1360 могут быть реализованы без переключения стека. Однако в менее распространенном случае, когда сгенерированные значения перечисляются посредством последовательных вызовов функций с сохранением состояния, # 1360 может вернуться к менее эффективной реализации с использованием стека. Важно отметить, что функциям, генерирующим значения (скажем, через yield ), не нужно знать, в каком контексте они выполняются.

Хитрость в том, что # 1360 разбивает алгебраические эффекты на компоненты более низкого уровня, а именно проверку стека и создание / переключение стека (без батута). Шаг «выполнить операцию» переводится в (нетерпеливую) проверку стека ( call_stack ), поиск ближайшей исполняемой метки стека ( answer ) для этой операции и ее вызов, предоставляющий ей прямой доступ к отмеченному фрейму стека. В неоптимизированной реализации глубоких обработчиков эта метка стека находится в корне стека, в котором были запущены обработчик и дескриптор, и имеет указатель на родительский стек. В этом случае тело исполняемой метки стека говорит о переключении управления на родительский стек, а затем, когда он переключается обратно с некоторым значением, он «отвечает» на проверку стека любым полученным значением (после сохранения нового родительского стека) . Но в оптимизированной реализации обработчика с изменением хвоста нет необходимости выделять новый стек, вместо этого отмечается текущий стек, и метка просто выполняет код обработчика в том же стеке, а затем «отвечает» тем, что обработчик код возобновил бы продолжение без необходимости выделения стека или переключения стека.

1360 также допускает оптимизацию для более смешанных шаблонов. Например, некоторые обработчики эффектов на некоторых путях потока управления, но не на всех, имеют обратную связь. В этих случаях # 1360 позволяет выделить новый стек на случай, если продолжение ускользнет от обработчика. Но # 1360 также позволяет метке стека в корне выделенного стека выполнять обработчик до того, как будет выполнено какое-либо переключение стека. Если управление переходит вниз по хвостовой ветви, то метка просто «отвечает» тем, что предоставляет обработчик. Только когда управление переходит вниз, в ветвь без возврата к хвосту управление переключается на родительский стек (расширенный для выполнения оставшихся вычислений).

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

Примечание: я понял, что мы можем использовать термин «генератор» по-другому, и, посмотрев документацию по Multicore OCaml, я действительно обнаружил, что это так. Поэтому я отредактировал приведенный выше комментарий, чтобы дать более подробный и подробный ответ, устраняющий разницу в терминологии.

Пример генераторов в сегодняшнем представлении оказался именно тем шаблоном «для каждого», для которого можно исключить все выделение стека и переключение (даже с «глубокими» выходами, т.е. без необходимости использовать автоматическое преобразование). Однако это предложение не может выражать эту оптимизированную стратегию реализации.

Можете ли вы объяснить, почему вы не хотите, чтобы оптимизированная стратегия реализации была выразимой?

Чтобы обсудить более тонкие моменты этого предложения и # 1360, я думаю, было бы полезно, если бы оба они могли быть сформулированы в терминах «глобальной модели мира», включающей понятие приостановленных стеков, текущего исполняемого стека и того, как они связаны друг с другом.

Мне кажется, что оба предложения показывают формальную семантику только с «точки зрения» одного потока. Следствием этого является то, что для наиболее интересных инструкций нам нужно вернуться к неформальному языку, потому что семантика не может быть полностью выражена в однопоточной модели выполнения.

@sabine Семантика обоих предложений

@RossTate, даже если семантика может быть выражена в однопоточном режиме, если следует отметить, что не ожидается, что продолжения могут передаваться между потоками, поскольку он явно указывает, что планировщики кражи работы в разных потоках не смогут захватить продолжение? Об этом упоминалось на собраниях, но, вероятно, это должно быть более явным, чем просто записи собрания. Это предотвращает кражу работы, которую делает Erlang's BEAM, поэтому мы не сможем сделать это в Lumen.

Конечно, CG следует проинформировать о том, что применение обоих предложений по многопоточности по-прежнему ограничено многопоточной моделью WebAssembly и встроенным модулем.

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

Я думаю, что у меня проблемы с использованием семантики большого шага и использованием неформальных объяснений для описания основных частей семантики переключения стека (и очень вероятно, что мне также нужно прочитать некоторые статьи, которые вы все знакомы с участием). Например,

  • здесь, в # 1359, cont.resume (event $evt $handler)* : [t1* (cont $ft)] -> [t2*] у нас есть семантика большого шага в том смысле, что оценка инструкции cont.resume приводит к новому стеку после того, как продолжение было полностью выполнено. Все остальные эффекты cont.resume описаны неформально (хотя, думаю, я понял общую идею).
  • в # 1360 у нас есть stack.switch $event : [t* stackref] -> unreachable где event $event : [t* stackref] , что, я предполагаю, так выглядит переключатель стека с точки зрения текущего потока. Однако в глобальной модели с семантикой малого шага мы должны увидеть стек, на который мы только что переключились?

Я вижу, что оба предложения следуют обычным соглашениям спецификации WebAssembly из https://webassembly.github.io/spec/core/exec/conventions.html , где говорится: «Правила выполнения также предполагают наличие неявного стека, который изменяется путем нажатия или выталкивания значений, меток и фреймов. ".

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

Используя семантику малого шага с явным стеком, мы могли бы математически сформулировать, что происходит при приостановке, возобновлении и более выражаемых примитивах из # 1360.

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

@sabine , семантика продолжений, безусловно, может быть описана семантикой малого шага. Мы действительно записали это для более ранней версии этого предложения, но еще не адаптировали его к текущему воплощению.

Грубо говоря, продолжение / стек представлен контекстом оценки E, и будет новая административная инструкция для представления обработчиков в стеке. Это очень похоже на то, как вы моделируете исключения, при этом продолжение просто фиксирует и реализирует контекст оценки, который вы перебросили, и возобновляет его повторный ввод.

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

Один из стандартных способов явного овеществления стеков - использование CEK-машины. @dhil имеет WIP-расширение семантики Wasm с обработчиками эффектов, которые, как я полагаю, смоделированы как машина CEK.

@rossberg - это доступная где-нибудь более ранняя (устаревшая) версия семантики малого шага?

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

Ах, спасибо за разъяснение @sabine! Ссылка @rossberg выше должна быть полезна для понимания подхода, основанного на оценке контекста. @kayceesrk упоминает машину CEK, что является еще одним подходом. Он делает стек явной частью состояния машины, как и куча. Таким образом, если стек состояния машины выглядит как (cons K K '), то есть стек K' с указателем на другой стек K наверху, то переключатель стека изменяет стек состояния машины, чтобы он выглядел так (cons K ' К). Затем инструкция stack.switch делает нечто подобное, но дополнительно включает исключения для управления нетипизированной связью. Надеюсь, это поможет.

Видел эту статью OOPSLA '20 из ретвита @yaahc @AndrewCMyers о _bidirectional_ обработчиках эффектов. Я пока не очень хорошо разбираюсь в формальной семантике в PL-документах, поэтому, перейдя только к прозе, эти разделы привлекли мой интерес, связанный с предлагаемым WASM использованием обработчика эффектов:

Взаимодействие многоядерных OCAML async / await + exception

Предыдущие кодировки. Возможность кодирования async-await на основе обещаний [Dolan et al. 2017; Leijen 1
2017a, b] говорит о впечатляющей силе алгебраических эффектов, но кодирование существующих языковых конструкций компромиссом в том, как они вмещают исключительные вычисления. Koka [Leijen 2017a, b] поддерживает структурированную асинхронность с помощью алгебраических эффектов, но использует любую монаду для возможных исключительных результатов операции ожидания, но кодирование исключительных результатов в монадические значения - это шаблон, которого алгебраические эффекты в Koka призваны помочь избежать! В отличие от Koka, Multicore OCaml [Dolan et al. 2017] не проверяет алгебраические эффекты статически. Чтобы уведомить пользовательские программы об асинхронно генерируемых исключениях, язык добавляет специальную конструкцию прерывания. Наша цель - обрабатывать асинхронные и ожидающие исключения и асинхронные исключения более унифицированным образом: оба являются статически проверяемыми алгебраическими эффектами.

Часть о многоядерном OCAML кажется актуальной, поскольку группа стеков WASM использовала многоядерный OCAML в качестве примера рабочей реализации, поэтому нам также понадобится специальная конструкция discontinue для объединения вместе async / await и исключения, если мы не поддерживаем что-то вроде обработчиков двунаправленных эффектов, предлагаемых в этой статье?

Использование обычных обработчиков эффектов для имитации двунаправленного

В документ также включены тестовые примеры реализации, в которых, если мы думаем, что обработчики эффектов также должны будут создавать эффекты (например, исключения из async / await , да, они могут быть реализованы с помощью обычных обработчиков эффектов и закрытия, но это будет удар по производительности.

Мы выполнили два ручных перевода примера общения в пинг-понге (раздел 3.3, с ponger, реализованным, как в разделе 3.3.2) на μC ++, причем один из двух использовал обратные вызовы. Эта программа была выбрана потому, что она осуществляет высокочастотную двунаправленную передачу управления. Мы запустили переведенный вручную код с использованием модифицированной реализации μC ++ с отключенной дополнительной проверкой времени выполнения и измерили время работы на процессоре Intel Xeon Gold с тактовой частотой 3,2 ГГц, в среднем за 500 запусков. Перевод, основанный на обратных вызовах, вызвал замедление в 2,1 раза: 42,6 мс против 19,8 мс. Этот результат свидетельствует о том, что двунаправленные обработчики - это первоклассная языковая функция: получение двунаправленности посредством десугарирования в обратные вызовы менее эффективно. (И наоборот, преобразование обратных вызовов в эффективную двунаправленность потребует сложного межпроцедурного анализа.)
8:07

Случайное обращение

Предотвращение случайного обращения. Случайная обработка алгебраических эффектов при наличии полиморфизма эффектов - известная проблема. Туннелирование (обработчики с лексической областью видимости и ограниченным временем жизни) как способ избежать случайной обработки исключений было предложено Zhang et al. [2016]; последующая работа адаптировала его к явному полиморфизму эффектов и доказала параметричность [Zhang and Myers 2019]. Brachthäuser et al. [2018, 2020] реализует туннелированные алгебраические эффекты в библиотеке Scala под названием Effekt, которая кодирует эффекты времени жизни через типы пересечений Scala и типы, зависящие от пути.

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

Документ заставил меня задуматься, как мы обрабатываем обработчики эффектов, не проглатывая неправильные эффекты без всех типов бумажных контуров, что также включает время жизни, как и в Rust.

Это вообще применимо к нам?

Опять же, я обычно не читаю статьи по PL, поэтому я мог совершенно не понимать, как эта статья связана с обработчиками эффектов WASM, но я подумал, что должен поднять этот вопрос, так как он казался по теме.

О, круто! Рад видеть, что Ичжоу продолжает эту работу на своей новой должности. Из этой статьи можно многое усвоить. Чтобы не слишком фрагментировать обсуждение, я сосредоточусь только на втором поднятом вами вопросе.

Интересно, что предоставленные ими измерения больше подходят для проверки стека (# 1356). Причина в том, что обе оценки выполняются для реализации алгебраических эффектов с суммированием хвоста, которая не выполняет переключение стека, и, хотя это предложение (# 1359) не поддерживает такую ​​стратегию реализации, # 1356 поддерживает его answer построить. answer состоит из двух частей: метки стека (что сродни динамической области видимости) и замыкания (над кадром стека), которое, по сути, является обработчиком эффекта с резюмированием хвоста. Один из способов реализации answer - это прямое использование кадра стека в качестве среды его замыкания, то есть замыкания, выделенного стеком. Другой способ - выделить среду закрытия в куче и при необходимости скопировать в среду закрытия и из нее. Их измерения показывают, что затраты на выделение кучи вместо повторного использования кадра стека могут сделать программы с интенсивной проверкой стека в 2,1 раза медленнее. (Для сравнения, это те же авторы, которые обнаружили, что активная трассировка стека делает программы с интенсивным использованием исключений в 6 раз медленнее.)

Далее, вторая важная вещь в этой статье - это использование лексической области видимости. В этом предложении, а также в Stack Inspection используется динамическое определение области видимости - каждый идет вверх по стеку, чтобы найти обработчик для данной эффективной операции. У этого есть свои преимущества - в частности, он хорошо работает в нетипизированных (с точки зрения эффектов) настройках - но также и свои недостатки, такие как случайное обнаружение другого обработчика, чем тот, который задумал программист, как описано в статье. Чтобы реализовать обработчики с лексической областью видимости, вы передаете обработчик эффективной функции как явный аргумент низкого уровня. Ценность, которую вы передаете, в общем случае является закрытием. Но то, что они делают со сроками жизни, гарантирует, что время жизни замыкания никогда не превысит время жизни его кадра стека. Это позволяет им использовать закрытие, выделенное стеком, а не закрытие, выделенное кучей. Этот метод реализации соответствует параметрам более высокого порядка, обсуждаемым в WebAssembly / обработка исключений # 105.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

spidoche picture spidoche  ·  4Комментарии

jfbastien picture jfbastien  ·  6Комментарии

badumt55 picture badumt55  ·  8Комментарии

beriberikix picture beriberikix  ·  7Комментарии

void4 picture void4  ·  5Комментарии