Design: Продолжение

Созданный на 7 янв. 2019  ·  38Комментарии  ·  Источник: WebAssembly/design

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

Поиск «продолжений» в системе отслеживания проблем вызывает несколько комментариев @rossberg, в которых упоминаются планы «продолжений с разделителями» / «переключение стека» в более поздней версии WebAssembly: https://github.com/WebAssembly/design/issues/ 796 # issuecomment -403487395, https://github.com/WebAssembly/design/issues/919#issuecomment -349038338, https://github.com/WebAssembly/design/issues/1171#issuecomment -355967209. Кажется, что на веб-сайте нет ни упоминания об этой функции, ни отдельной проблемы в этом репозитории, поэтому я хотел бы спросить - каков здесь статус?

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

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

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

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

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

Был некоторое описание этого здесь , хотя обработка предложение исключение изменилось с тех пор, так что, возможно , это не имеет значения больше. Может быть, @rossberg и @aheejin предоставят дополнительную информацию.

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

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

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

В настоящее время я работаю над альтернативной реализацией Erlang, основанной на предварительной компиляции, а не на интерпретации байт-кода; основной целью является WebAssembly, но в конечном итоге будут поддерживаться и другие цели для поддержки использования в качестве общего компилятора машинного кода для языков BEAM. Для реализации языка и компилятора необходимо выполнить несколько ключевых требований:

  • Правильные хвостовые вызовы, поскольку рекурсия широко используется в языке, нет других средств для выражения итерации.
  • Дешевый нелокальный возврат, Erlang throw обычно используется для упрощения кода, который должен завершиться раньше, или для обеспечения возможности возврата значения из глубины стека вызовов; это не то, что используется только для исключений
  • Псевдо-вытесняющее планирование (реализованное как совместное планирование во время выполнения, но кажется вытесняющим из кода Erlang), которое используется для реализации зеленых потоков (называемых процессами в Erlang)
  • Сборка мусора по процессам; важно для производительности, особенно, чтобы не мешать миру собирать. Во многих случаях процессы Erlang умирают до того, как потребуется выполнить GC этого процесса, а это означает, что большие участки памяти вообще не нужно сканировать.

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

  • Программа конвертируется в форму CPS. В форме CPS, и в этой разновидности, в частности, каждый вызов находится в хвостовой позиции, и вызываемый получает как продолжение возврата, так и продолжение выхода (так называемые продолжения "двойной бочки") в дополнение к любым аргументам.
  • Поскольку ни один вызов никогда не возвращается, стек в конечном итоге взорвется, но трюк, используемый в _Cheney на MTA_, заключается в том, что точка входа принимает адрес указателя стека и использует его для периодической проверки (обычно каждого вызова функции), чтобы определить, В стеке скоро закончится место. Когда это происходит, текущие значения в стеке перемещаются в кучу вместе с текущим продолжением, а longjmp используется для возврата к точке входа (которая действует как трамплин). Это эффективно сбрасывает стек до квадрата с очень небольшими накладными расходами.
  • Переход также дает нам возможность выполнять сборку мусора, планирование и, как следствие сброса стека, гарантирует, что хвостовые вызовы никогда не взорвут стек. Дополнительные точки доходности могут быть добавлены сверх текущего состояния указателя стека, например, подсчет сокращений (как Erlang определяет, когда вытеснять зеленый поток) или определение необходимости крупной сборки мусора.
  • Использование продолжений означает, что мы легко получаем дешевые нелокальные доходы.
  • Использование продолжений означает, что можно легко приостановить один зеленый поток и возобновить другой.

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

Однако, судя по исследованиям, которые я провел до сих пор, использование setjmp / longjmp в WebAssembly через исключения JS приводит к значительному снижению производительности и, по сути, является просто прокладкой совместимости. Это делает любой язык, использующий подход Чейни к ​​компиляции в стиле MTA, излишне медленным. Довольно известный пример такого языка - Chicken Scheme, и я считаю, что Chez Scheme также использует похожий, если не тот же трюк.

Одна альтернатива - использовать батуты - то есть вместо longjmp когда стек вот-вот взорвется или требуется предел текучести, компилятор генерирует код, который возвращается в батут каждый раз, когда нужно вызвать следующее продолжение. Проблема, конечно же, в том, что для каждого вызываемого продолжения требуется дополнительный вызов функции return +; однако я подозреваю, что сегодня это лучше в WebAssembly по сравнению с текущим хаком setjmp / longjmp . Можно также использовать явное return для раскрутки стека, но я подозреваю, что это даже медленнее, чем решение setjmp / longjmp .

Все это говорит о том, что я считаю, что что-то необходимо, чтобы сделать реализацию языков с похожими требованиями как выполнимой, так и производительной. Возможно, это означает некую родную поддержку продолжения или, возможно, что-то менее надежное, например, поддержку паттерна setjmp , который я описал лучше. Я хочу прояснить, что это не просто проблема, специфичная для моей реализации - набор языков, которые имеют или могут иметь эти требования, имеет некоторые нетривиальные элементы, например, Haskell, любую соответствующую стандарту реализацию Scheme, Erlang (и BEAM языков в целом), и по существу любой язык, который зависит от правильной поддержки хвостовых вызовов, а не только ограниченная версия, ставшая возможной благодаря соглашению о вызовах fastcc.

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

Если у вас есть какие-либо вопросы (или даже предложения!) По поводу вышеизложенного, обязательно дайте мне знать!

@bitwalker Что мне непонятно из описанных вами вариантов:

  • Использование longjmp будет означать, что вы используете фактический стек wasm (который не находится под вашим контролем), что означает, что у вас нет способа определить, сколько вы используете и когда у вас закончится место в стеке. , В данный момент?
  • Если вместо этого вы использовали собственный «теневой стек» в линейной памяти для хранения кадров стека, вы могли бы сбросить его без использования longjmp , предполагая, что вы не используете функции wasm для реализации функций на своем языке, а вместо этого имеете все тела живут внутри огромной конструкции for (;;) switch (..). Это может быть медленнее по другим причинам (сложно выделить регистры?), Но, по крайней мере, не требует помощи от wasm.
  • Использование батута (со стеком wasm) предотвратит использование, в котором фрейм стека должен выжить, например, зеленые потоки?

Кстати, "Чейни на MTA" действительно элегантен .. это почти как копирующий сборщик, но в отличие от полного GC, список фреймов стека копировать тривиально. Также мне нравится, что Бейкер подает эту газету в той же форме с одного и того же URL-адреса более 20 лет :)

@aardappel Конечно, позвольте уточнить :)

Использование longjmp будет означать, что вы используете фактический стек wasm (который не находится под вашим контролем), что означает, что у вас нет способа определить, сколько вы используете и когда у вас закончится место в стеке. , В данный момент?

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

Если вместо этого вы использовали собственный «теневой стек» в линейной памяти для хранения кадров стека, вы могли бы сбросить его без использования longjmp , предполагая, что вы не используете функции wasm для реализации функций на своем языке, а вместо этого имеете все тела живут внутри огромной конструкции for (;;) switch (..). Это может быть медленнее по другим причинам (сложно выделить регистры?), Но, по крайней мере, не требует помощи от wasm.

Вы в значительной степени прибили здесь одну серьезную проблему - она ​​разрушает возможность оптимизации распределения регистров, что может не быть проблемой для WebAssembly (?), Учитывая его семантику стековой машины, но это определенно то, чего я бы избегал, если у меня не было другого выбора . Более серьезная проблема заключается в том, что предотвращается целый ряд оптимизаций в серверной части компилятора (я использую LLVM, но я слышал, что проблема такая же, как и в различных реализациях компиляции на языке C).

Использование батута (со стеком wasm) предотвратит использование, в котором фрейм стека должен выжить, например, зеленые потоки?

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

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

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

  2. Любые текущие значения, которые находятся в стеке, перемещаются в кучу (в нашем случае каждый процесс имеет свою собственную кучу, поэтому значения перемещаются туда).
  3. Созданное продолжение сохраняется в структуре процесса (каждый процесс / зеленый поток имеет структуру со всеми его ключевыми метаданными, такими как указатель на его кучу, счетчик сокращений и т. Д.
  4. Выполняется переход, который возвращает управление планировщику.
  5. Планировщик извлекает следующий процесс, который должен быть запланирован, из своей очереди выполнения, получает продолжение для этого процесса и вызывает его с аргументами, хранящимися в этом продолжении.

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

Кстати, "Чейни на MTA" действительно элегантен .. это почти как копирующий сборщик, но в отличие от полного GC, список фреймов стека копировать тривиально. Также мне нравится, что Бейкер подает эту газету в той же форме с одного и того же URL-адреса более 20 лет :)

Я подумал, что использовать стек в молодом поколении - это блестящий ход. И становится лучше! Поскольку мы находимся в форме CPS, вам не нужно копировать кадры стека, единственные живые значения, которые необходимо скопировать, _ должны_ находиться в аргументах текущей функции или ссылаться на них, все, что недоступно из этих аргументов, мертво, и эффектно собранные прыжком :)

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

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

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

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

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

@bitwalker , боюсь, я не понимаю. Как вы думаете, почему должен быть батут? Это, безусловно, должно быть обычным хвостовым вызовом.

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

Боюсь, я не понимаю. Как вы думаете, почему должен быть батут? Это, безусловно, должно быть обычным хвостовым вызовом.

Прошу прощения, если я неправильно истолковал! Я имел в виду этот раздел , где говорится, что семантика выполнения ведет себя как return за которым следует call . Это кажется запутанным способом их объяснения, особенно из-за связи с батутом. Я всегда считал крики хвоста чем-то более похожим на прыжок, и это было моим впечатлением о том, как другие люди тоже смотрят на них. Хотя основной смысл хвостовых вызовов заключается в том, чтобы избежать переполнения стека путем повторного использования кадров стека, я понял, что еще одним ключевым свойством хвостовых вызовов является то, что они избегают большого количества перетасовки стека, которое происходит с вызовом return + или даже просто вызов; больше в случае саморекурсивных вызовов, но еще более широко в случае вызовов функций с аргументами в одной и той же позиции, когда компилятор может избегать перемещения вещей. Думаю, я просто ожидал увидеть это в предложении. Я не говорю вам ничего, чего вы еще не знаете о хвостовых вызовах, я просто пытаюсь объяснить, почему я так интерпретировал предложение :)

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

Спасибо за ссылку! Мне кажется, что это, вероятно, можно было бы использовать для выражения типа потока управления, для которого я использую setjmp / longjmp (как описано выше, например, _Cheney на MTA_), предполагая, что " возобновление "управления во внешнем вызове" перематывает стек вызовов к этой точке. Так ли это?

В любом случае, чем я могу помочь? Решена ли проблема с эталонной реализацией, есть ли еще открытые вопросы, требующие исследования / обсуждения, или просто нужен кто-то, чтобы довести ее до конца?

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

@Danielhillerstrom начал

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

Моя основная проблема связана с этой строкой:

Следовательно, они раскручивают стек операндов, как return

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

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

@Danielhillerstrom начал

Круто, эта работа в настоящее время общедоступна или ее все еще взламывают?

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

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

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

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

Моя основная проблема связана с этой строкой:

Следовательно, они раскручивают стек операндов, как return

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

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

(Команда хвостового вызова в некоторых коммерческих движках, которые останутся неназванными, печально известны тем, что они были до 100 раз медленнее - я уверен, что у нас получится лучше. :))

Об этом уже говорили инженеры по этим двигателям?

Конечно. Все видят необходимость в какой-либо форме переключения стека. Это не значит, что всем не страшно ...

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

Приносим извинения за то, что пропустили это! Это действительно проясняет, почему это сформулировано именно так. Спасибо за ваше терпение :)

@bitwalker

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

За исключением того, что вы не можете получить адрес чего-либо в стеке wasm по замыслу. В тот момент, когда вы берете адрес локальной переменной (в LLVM или, например, C ++), сгенерированный код wasm изменяет локальную переменную с wasm local на что-то в теневом стеке. Таким образом, вы не можете измерить стек wasm таким образом. И вы уже сказали, что будете недовольны тем, что все будет в теневом стеке.

Вы все еще можете эмулировать это ... вы можете для каждой функции wasm статически вычислить общий размер всех ее локальных переменных + args + max temps и использовать это как консервативное приближение используемого размера стека, а затем выполнить очистку стека (используя longjmp или исключения), как только он превысит некоторую консервативную границу, по вашим оценкам, будет нормальное количество wasm-стека для использования на любом движке. Но wasm не указывает стек min / max, который должна предоставлять реализация, так что это очень сомнительно. Вы даже можете попытаться определить, что предоставляет реализация, намеренно исчерпав пространство стека для различных рекурсивных функций, подсчитав количество вызовов, поймав ловушку и оценив консервативное пространство стека, ограниченное этим;)

Опять же, я думаю, что в целом вам будет лучше со стеком теней :)

Откровенно говоря, модулю wasm должно быть разрешено хотя бы намекать на объем стека, который он хочет предоставить движку, потому что без этого вы никогда бы не смогли даже использовать рекурсивные алгоритмы (которые обрабатывают пользовательские данные, например, парсер ) в wasm, _если стек может быть сколь угодно маленьким_. Или серверным модулям всегда нужно было заставлять рекурсивные алгоритмы использовать теневой стек. Какой в ​​этом смысл @titzer @binji ?

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

А, хорошо, я представлял, что вы имели в виду трамплин для каждого вызова / callcc, но вы имеете в виду, что в идеале он используется только для yield и когда у вас заканчивается место в стеке?

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

Ага, столько "креативных" идей, которые все еще актуальны!

@aardappel :

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

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

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

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

@rossberg
1) Хорошее замечание, это проблема. Тем не менее, по-прежнему полезно иметь возможность указывать его для большинства случаев, когда «тяжелый» модуль (который полагается на рекурсию), который был запущен как первый / основной модуль, указывает такое ограничение, а любые вспомогательные / дочерние модули не У меня нет особых требований. Или, если изменить размер стека легко, это может быть максимум всех задействованных модулей ... хотя я предполагаю, что будут механизмы, которые не смогут изменять размер, если они напрямую используют стек процессов хоста, так что все это спорный вопрос.

2) Да, это сильно зависит от реализации, но это уже тот случай, если я пишу нативную программу. Если я знаю, что Linux предоставит мне 8 МБ, я затем смотрю на свою рекурсивную функцию, чтобы оценить, насколько глубоко я могу пойти и разумно ли это для данных наихудшего случая, которые я думаю, что я буду обрабатывать ... прежде чем принять решение превратить их в не- рекурсивный алгоритм или нет. Это уже очень неточная наука. Я не понимаю, почему для wasm все должно быть иначе. Возможность сказать «Я ожидаю, что там будет порядка 1 МБ стека» даст мне гораздо лучшие гарантии, чем необходимость писать свой код, предполагая, что некоторые движки ограничат меня до 16 КБ стека (и, таким образом, придется удалить любую рекурсию из моего кода). То же самое для варианта использования

За исключением того, что вы не можете получить адрес чего-либо в стеке wasm по замыслу. В тот момент, когда вы берете адрес локальной переменной (в LLVM или, например, C ++), сгенерированный код wasm изменяет локальную переменную с wasm local на что-то в теневом стеке. Таким образом, вы не можете измерить стек wasm таким образом. И вы уже сказали, что будете недовольны тем, что все будет в теневом стеке.

Что ж, возможно, нужен пример, чтобы проиллюстрировать то, что я имею в виду. Следующее - тривиальное приложение C, которое использует рекурсию для увеличения счетчика до некоторого произвольного предела. Он использует батут, но только когда стек собирается взорваться, и он определяет, когда вернуться на батут, проверяя, сколько места в стеке, по его мнению, осталось:

#include <emscripten.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <stddef.h>

#define STACK_BUFFER 16384

// Checks whether the stack is overflowing
static bool stack_check(void);

// The address of the top of the stack when we first saw it
// This approximates a pointer to the bottom of the stack, but
// may not necessarily _be_ the exact bottom. This is set by
// the entry point of the application
static void *stack_initial = NULL;
// Pointer to the end of the stack
static uintptr_t stack_max = (uintptr_t)NULL;
// The current stack pointer
static uintptr_t stack_ptr_val = (uintptr_t)NULL;
// The amount of stack remaining
static ptrdiff_t stack_left = 0;

// Maximum stack size of 248k
// This limit is arbitrary, but is what works for me under Node.js when compiled with emscripten
static size_t stack_limit = 1024 * 248 * 1;

static bool stack_check(void) {
  // Get the address of the top of the stack
  stack_ptr_val = (uintptr_t)__builtin_frame_address(0); // could also use alloca(0)

  // The check fails if we have no stack remaining
  // Subtraction is used because the stack grows downwards
  return (stack_ptr_val - stack_max - STACK_BUFFER) <= 0;
}

#define MAX_DEPTH 50000

static int depth;
static int trampoline;

int a(int val);
int b(int val);

int a(int val) {
    // Perform stack check and only recurse if we have stack
    if (stack_check()) {
        trampoline = 1;
        return val;
    } else {
        depth += 1;
        return b(val); 
    }
}

int b(int val) {
    int val2;
    if (depth < MAX_DEPTH) {
        // Keeping recursing, but again only if we have stack
        val2 = val + 1;
        if (stack_check()) {
            trampoline = 1;
            return val2;
        }
        return a(val2);
    } else {
        return val;
    }
}

int main() {
    stack_initial = __builtin_frame_address(0); // could also use alloca(0)
    stack_max = (uintptr_t)stack_initial - stack_limit;

    double ts1, ts2 = 0;
    ts1 = emscripten_get_now();

    depth = 0;
    trampoline = 0;

    int val = 0;

    while (true) {
        val = a(val);
        // This flag helps us distinguish between a return to reset the stack vs
        // a return because we're done. Not how you'd probably do this generally,
        // but this is just to demonstrate the basic mechanism
        if (trampoline == 1) {
            trampoline = 0;
            continue;
        }
        break;
    }
    ts2 = emscripten_get_now();

    printf("executed in %fms\n", ts2-ts1);

    return 0;
}

Чтобы убедиться в этом, запустите emcc -s WASM=1 demo.c -o demo.js и запустите node demo.js . Моя установка локального узла, похоже, имеет максимальный размер стека 984 КБ, но, как вы можете видеть, мне пришлось установить максимальный размер стека в моей демонстрации на 248 КБ, так как любое большее значение приведет к переполнению стека, и, к сожалению, этого не происходит. чтобы получить доступ к точному пределу стека, нужно угадать или установить константу.

Пожалуйста, простите за ужасный код, но я быстро набросал его. Я надеюсь, что основная идея должна быть ясна - если кто-то решит вернуться к трамплину только тогда, когда стек заканчивается, это _is_ возможно в WebAssembly сегодня, хотя и с некоторыми ограничениями. До сих пор в моем разнообразном профилировании схожих сценариев это, по сути, связь между описанным выше подходом и обычным батутом, но я подозреваю, что более реалистичные сценарии покажут, что чем реже нужно возвращаться к батуту, тем лучше. Однако с таким маленьким стеком для работы выгода, к сожалению, довольно небольшая - я ожидал, что буду работать со стеком не менее 2 МБ, если не больше, 248 КБ просто смешно. Что касается setjmp / longjmp , он даже близко не подходит из-за того, как это реализовано сегодня.

Как сказал @rossberg , я не думаю, что размер стека для каждого модуля имеет смысл, он должен быть глобально настраиваемым, что, по сути, имеет место сегодня, за исключением того, что вы настраиваете размер глобального стека для самого движка. Насколько я понимаю, это также может означать, что он вообще не настраивается, и это нормально - фактический размер не является основной проблемой, хотя текущая ситуация не идеальна. Разрешение стеку увеличиваться до произвольного размера за счет роста в кучу тоже кажется жизнеспособным вариантом, но, вероятно, не идеальным, поскольку на самом деле это не то, чего можно было бы ожидать. Я бы предпочел просто увидеть более щадящий размер стека (как я уже сказал выше, не менее 2 МБ).

С точки зрения спецификации, я не думаю, что можно _ требовать_ определенного размера стека, но я думаю, что можно добавить или уточнить несколько вещей:

  • Рекомендация относительно минимального размера стека. Некоторым платформам с жесткими ограничениями, вероятно, потребуется снизить скорость, но если есть рекомендация для чего-то вроде 2 МБ, то платформы, у которых нет ограничений, могут выиграть от большего пространства стека для работы, но спецификация не _вложила_ минимум
  • Наиболее важным является способ получить текущий предел стека, применимый к исполняемому коду WebAssembly (например, значение getrlimit(RLIMIT_STACK, ..) будет ограничением, полезным для кода, скомпилированного в WebAssembly). Это обеспечивает языковые реализации средствами для обработки стеков разного размера без необходимости добавлять кучу условных констант для каждого движка или, что еще хуже, одну чрезвычайно низкую константу для наименьшего общего знаменателя.

Для моего собственного проекта я собираюсь провести больше профилирования, но, исходя из того, что я вижу, я, вероятно, просто буду использовать простой батут при нацеливании на WebAssembly, пока не будет setjmp / longjmp или что-то вроде возобновляемых исключений становится жизнеспособным; хвостовые вызовы решают мой вариант использования; или размеры стека в основных двигателях становятся достаточно большими, чтобы можно было использовать _Cheney на MTA_. Инструментов в моем распоряжении просто недостаточно, чтобы заставить его работать хорошо сегодня.

Я также должен уточнить, я скомпилировал приведенный выше пример с помощью emcc , но он настроен на использование моей собственной установки LLVM (в этой демонстрации используется мастер LLVM, поэтому 9, и построенный в течение последних нескольких дней - никаких изменений с моей стороны), поэтому он компилируется с помощью бэкэнда LLVM, _not_ asm.js - на всякий случай, если ожидается, что это изменит ситуацию.

@bitwalker
Вы уверены, что приведенный выше код делает то, что вы думаете? Afaik, __builtin_frame_addressalloca ) вернет адреса в теневом стеке, тогда как ваша рекурсия происходит в стеке wasm, поэтому это должны быть несвязанные значения. Поскольку в этом коде вы на самом деле ничего не храните в теневом стеке, он может даже не расти, поэтому __builtin_frame_address может возвращать ... одно и то же значение каждый раз?

@aardappel Я провел много тестов перед публикацией;). Чтобы проверить значения, возвращаемые на каждом шаге, я сбросил все значения, относящиеся к стеку, через printf . Я также провел эксперимент как с включенной проверкой стека, так и без нее. Без него код даже близко не приближается к 50 КБ (значение MAX_DEPTH) и прерывается, что, конечно, ожидается. При его включении программа достигает 50 км, при этом ударяя по батуте только ~ 6 раз, завершая успешно. Если вы измените максимальный объем стека на величину, превышающую 248 КБ, скажем 256 КБ, стек переполняется, что указывает на то, что код возвращается к трамплину правильно, когда мы достигаем определенного предела стека. Значение, создаваемое __builtin_frame_address (которое понижается до внутреннего @llvm.frameaddress ), становится меньше на правильную величину каждый раз, когда выполняется проверка стека (на основе приблизительной оценки на основе просмотра распределений в LLVM IR), поэтому, похоже, он дает то значение, которое я ожидал.

Я пробовал реализовать с getrlimit(2) , но возвращаемое им значение явно отключено от стека (упомянутого вами теневого стека) и может относиться к реальному стеку - я не уверен, но это не так. Бесполезно для проверки стека. Так что есть некоторые инструменты, которые нельзя использовать.

@bitwalker Я думаю, это может быть потому, что вы компилируете без оптимизации. Я взял ваш код и скомпилировал его с помощью webassembly.studio, который по умолчанию компилируется с -O3 . Это полностью оптимизирует цикл и все вызовы функций. Если я добавлю фиктивный вызов, он превратит все это в цикл. https://webassembly.studio/?f=0aj0cn61ob8

@binji Да, я компилирую без оптимизаций, и это

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

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

Ваш код использует неопределенное поведение в C. Это работает на обычном оборудовании, но не на Wasm.

Ваш код использует неопределенное поведение в C. Это работает на обычном оборудовании, но не на Wasm.

Чтобы быть ясным, я запускаю этот код в движке WebAssembly, в частности в v8, а не в x86_64.

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

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

Функции Wasm используют системный стек, как и большинство других кодов. Движок реализует защиту стека, но в остальном он использует системный стек, как и любой другой компилятор и среда выполнения. И да, переполнение может быть, но такие переполнения ловятся. Теневой стек - это то, что было введено компилятором C, и он содержит только локальные переменные, которые должны быть в памяти, адресуемой из C. Ясно, что механизм не может хранить там адреса возврата или другие внутренние указатели, не создавая серьезных проблем с безопасностью.

И да, переполнение может быть, но такие переполнения ловятся.

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

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

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

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

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

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

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

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

Тем более, что они привнесут значительную степень наблюдаемого недетерминизма.

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

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

Это касается одной проблемы (стек слишком мал и, следовательно, слишком легко переполняется), но не имеет отношения к проблеме, которую я пытаюсь поднять.

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

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

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

Сами по себе ограничения не являются запретительными и не являются проблемой. Ограничения ресурсов и связанный с ними риск существуют и за пределами сети, но там есть инструменты для их устранения - в контексте ограничений стека есть setjmp / longjmp , проверка стека и мусора. сбор с помощью прыжка / возврата, когда он собирается переполниться, или с помощью батутов. По сути, можно гарантировать, что если вы собираетесь превысить лимит, с ним можно будет аккуратно обработать (или просто разрешить превышение лимита и прекратить / паниковать). Проблема с Wasm, для которой я пытаюсь найти решение, заключается в том, что эти инструменты (за исключением трамплинов) удалены (полностью или практически, как в случае с SJLJ) и не имеют замены. Для языков, вся реализация которых основана на этих инструментах, их отсутствие, безусловно, является серьезной проблемой. Я вовсе не говорю, что Wasm в корне ошибочен или что-то в этом роде, но я думаю, что здесь чего-то не хватает в спецификации, в частности, некоторых средств понимания кода текущих ограничений выполняется под (например, размер стека) и где вы относитесь к эти ограничения (например, что-то похожее на адрес, который дает вам представление о вашей позиции относительно максимального размера стека).

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

@bitwalker :

Тем более, что они привнесут значительную степень наблюдаемого недетерминизма.

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

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

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

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

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

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

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

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

Конечно, но теперь, когда Wasm открывает возможность этих инструментов, разве спецификация не должна стремиться идентифицировать и адресовать?

Итак, самый важный вопрос для меня: каков дальнейший путь для этих языков / реализаций?

А пока используйте теневой стек, очень похожий на то, что делает ваш код C в неоптимизированной сборке, и как я уже упоминал в своем первом ответе.

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

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

Количество языков, модель исполнения которых отличается от C-подобной модели, бесконечно, вряд ли мы будем поддерживать их все напрямую. Фактически, мы даже не полностью поддерживаем C напрямую :) Вызовы хвоста и возобновляемые исключения помогут некоторым, но я предсказываю, что популярным вариантом останется «прокатка собственных фреймов стека».

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

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

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

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

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

Я не удивлен, что они недоступны _ прямо сейчас_, но я удивлен и разочарован, услышав такой ответ, особенно от вас, который, кажется, говорит, что, поскольку что-то не было доступно в Интернете исторически, тогда это не заслуживает внимания. WebAssembly уже используется за пределами Интернета, и такие варианты использования даже упоминаются в спецификации / материалах для самого WebAssembly! Не говоря уже о том, что WebAssembly уже предлагает инструменты, которые ранее были недоступны; Хвост зовет, будучи лишь одним из них.

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

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

А пока используйте теневой стек, очень похожий на то, что делает ваш код C в неоптимизированной сборке, и как я уже упоминал в своем первом ответе.

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

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

Как вы предлагаете реализовать собственный стек? Компиляция в собственный код означает использование собственного стека, в данном случае это означает теневой стек Wasm, над которым мы _не_ полностью контролируем. Компиляция программы в гигантскую диспетчеризацию цикла / переключателя - ужасное решение по ряду причин. Единственная текущая альтернатива - это трамплин для каждого вызова, но, как упоминалось ранее, помимо затрат на производительность, он по-прежнему не учитывает любой язык, который хочет выполнять сборку мусора с использованием _Cheney в сборщике поколений MTA style_ или использовать стек для выделения.

Количество языков, модель исполнения которых отличается от C-подобной модели, бесконечно, вряд ли мы будем поддерживать их все напрямую. Фактически, мы даже не поддерживаем полностью C напрямую :)

Из того, что я видел, практически каждый важный функциональный язык (Haskell, Lisp, ML, Prolog) имеет по крайней мере один компилятор / реализацию, генерирующую C, а модели, которые эти языки представляют, охватывают практически все пространство языков функционального программирования. Например, Erlang очень похож на ML в своем основном представлении. Все эти языки значительно отличаются от C семантически, но, тем не менее, все они могут быть переведены на что-то, что может вписаться в модель C. Я не думаю, что мы говорим о проблеме поддержки всех возможных деталей реализации, но наличие примитивов для запроса использования стека - это сквозная проблема, которая затрагивает все типы языков, включая C.

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

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

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

@bitwalker

Возможно, мне стоит открыть новую проблему специально для примитивов стека, которые, по моему мнению, необходимы?

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

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

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

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

Сделаю

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

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

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

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

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

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

Учитывая, что такой язык, как язык пирета @shriram, основан на глубоком понимании недостатков и точек успеха ведущих языков (включая javascript), его комментарий имеет вес https://github.com/WebAssembly/design/issues/ 919 # issuecomment -348000242. Javascript - это низкая планка, главное, что он получил, это то, что он повсеместен из-за участия в распространении контента по сетевым системам точка-точка. Монополии на распространение данных возникают исключительно как возникающее поведение при распространении данных через систему связи точка-точка. Язык, разработанный за 10 дней, прошел на финише тяжелого узла TCP / IP, предназначенного для показа рекламы, он не особенный и не достиг своего текущего статуса благодаря своим достоинствам. У нас есть возможность исправить ошибки, и мы не нуждаемся в таком количестве фреймворков, чтобы наполовину переопределить продолжения. ТШО - большой шаг в правильном направлении.

Продолжения позволяют использовать соответствующие параллельные языки функционального программирования, у которых есть реакторы, передающие данные в несколько точек (а не только на серверы GAFAM!). Особенно, когда данные становятся адресуемыми напрямую, и нам не нужно получать индексированные данные, обращаясь к узлам Google. Наличие однопоточной среды выполнения для Google дает преимущества, поскольку она хорошо работает, направляя рекламу по IP-каналу клиент / сервер. Правильные продолжения дают нам лучшие варианты. Браузер может быть намного больше, чем умышленно выведенная из строя машина для показа рекламы, которой он является сегодня.

Немного поздно для этого разговора, о котором я не знал, но @bitwalker может счесть полезным взглянуть (а может, и больше!) На Stopify [сайт: https://www.stopify.org ], [документ: https://cs.brown.edu/~sk/Publications/Papers/Published/bnpkg-stopify/ ], [репо: https://github.com/plasma-umass/Stopify ].

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

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

CC @arjunguha @jpolitz @rachitnigam

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

Закрытие в пользу №1359

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

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

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

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

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

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

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