Design: Пожалуйста, поддержите произвольные ярлыки и Gotos.

Созданный на 8 сент. 2016  ·  159Комментарии  ·  Источник: WebAssembly/design

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

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

Мои жалобы:

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

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

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

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

Кроме того, настойчивость конструктов более высокого уровня приводит к некоторым
патологические случаи. Например, устройство Даффа заканчивается ужасной паутиной.
вывод сборки, как видно из The Wasm Explorer .
Однако обратное неверно: все, что можно выразить
в веб-ассемблере может быть тривиально преобразовано в эквивалент в некотором
неструктурированный, основанный на goto формат.

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

Полифиллинг:

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

Если мы предположим синтаксис, подобный LLVM (или QBE), для веб-сборки, то некоторый код
это выглядит так:

int f(int x) {
    if (x == 42)
        return 123;
    else
        return 666;
}

может скомпилироваться в:

 func @f(%x : i32) {
    %1 = test %x 42
jmp %1 iftrue iffalse

 L0:
    %r =i 123
jmp LRet
 L1:
    %r =i 666
jmp LRet
 Lret:
    ret %r
 }

Это может быть полифиллировано в Javascript, который выглядит так:

function f(x) {
    var __label = L0;
    var __ret;

    while (__label != LRet) {
        switch (__label) {
        case L0:
            var _v1 = (x == 42)
            if (_v1) {__lablel = L1;} else {label = L2;}
            break;
        case L1:
            __ret = 123
            __label = LRet
            break;
        case L2;
            __ret = 666
            __label = LRet
            break;
        default:
            assert(false);
            break;
    }
}

Это некрасиво? Ага. Это имеет значение? Надеюсь, если веб-сборка взлетит,
не долго.

А если нет:

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

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

control flow

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

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

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

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

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

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

Блоки, которые можно упорядочить в DAG, могут быть выражены в блоках и ветвях wasm, как в вашем примере. Switch-loop — это стиль, который используется, когда это необходимо, и, возможно, потребители могли бы использовать несколько потоков перехода, чтобы помочь здесь. Возможно, взгляните на binaryen, который может сделать большую часть работы для вашего бэкэнда компилятора.

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

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

структура действительно помогает здесь для общих шаблонов кода

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

Блоки, которые можно упорядочить в DAG, могут быть выражены в блоках и ветвях wasm, как в вашем примере.

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

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

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

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

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

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

Что касается CPS, я не думаю, что нужна явная поддержка — он популярен в кругах FP, потому что его довольно легко преобразовать в ассемблер напрямую, и он дает аналогичные преимущества SSA с точки зрения рассуждений (http:// mlton.org/pipermail/mlton/2003-January/023054.html); Опять же, я не эксперт в этом, но, насколько я помню, продолжение вызова сводится к метке, нескольким движениям и переходу.

@oridb «есть быстрые и хорошо известные преобразования в SSA из плоских списков инструкций»

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

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

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

Вы бы сохранили стек значений или использовали дизайн на основе регистров?

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

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

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

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

Спасибо за ваши вопросы и опасения.

Стоит отметить, что многие разработчики и разработчики
WebAssembly имеют опыт работы с высокопроизводительными промышленными JIT, не только
для JavaScript (V8, SpiderMonkey, Chakra и JavaScriptCore), но и в
LLVM и другие компиляторы. Я лично реализовал два JIT для Java
байт-код, и я могу засвидетельствовать, что стековая машина с неограниченным количеством переходов
вносит некоторую сложность в расшифровку, проверку и построение
компилятор ИР. На самом деле существует множество шаблонов, которые можно выразить в Java.
байт-код, который вызовет высокопроизводительные JIT, включая как C1, так и C2 в
HotSpot, чтобы просто сдаться и перевести код только на выполнение в
устный переводчик. Напротив, построение IR компилятора из чего-то вроде
Я тоже делал AST из JavaScript или другого языка. То
дополнительная структура AST делает часть этой работы намного проще.

Конструкция конструкций потока управления WebAssembly упрощает потребителей за счет
обеспечивает быструю и простую проверку, легкое преобразование в форму SSA за один проход
(даже граф IR), эффективные однопроходные JIT и (с постзаказом и
стековая машина) относительно простая интерпретация на месте. Структурированный
управление делает неприводимые графы потока управления невозможными, что устраняет
целый класс неприятных угловых случаев для декодеров и компиляторов. Это также
прекрасно закладывает основу для обработки исключений в байт-коде WASM, для которого V8
уже разрабатывает прототип совместно с производством
выполнение.

У нас было много внутренних дискуссий между членами об этом очень
тему, так как для байт-кода это одна вещь, которая больше всего отличается от
другие цели машинного уровня. Тем не менее, это ничем не отличается от таргетинга
исходный язык, такой как JavaScript (что делают многие компиляторы в наши дни) и
требует лишь незначительной реорганизации блоков для достижения структуры. Там
известны алгоритмы для этого и инструменты. Мы хотели бы предоставить некоторые
лучшее руководство для тех производителей, которые начинают с произвольного CFG для
сообщайте об этом лучше. Для языков, ориентированных на WASM, непосредственно из AST
(что на самом деле то, что V8 делает сейчас для кода asm.js - напрямую
перевод JavaScript AST в байт-код WASM), нет никакой реструктуризации
шаг необходимый. Мы ожидаем, что это относится ко многим языковым инструментам.
по всему спектру, у которых нет сложных IR внутри.

В четверг, 8 сентября 2016 г., в 9:53, Ори Бернштейн, [email protected]
написал:

Вы бы сохранили стек значений или использовали дизайн на основе регистров?

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

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

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


Вы получаете это, потому что подписаны на эту тему.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-245521009 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/ALnq1Iz1nn4--NL32R9ev0JPKfEnDyvqks5qn77cgaJpZM4J3ofA
.

Спасибо @titzer , у меня возникло подозрение, что у структуры Wasm есть цель, выходящая за рамки простого сходства с asm.js. Однако мне интересно: байт-код Java (и CIL) не моделирует CFG или стек значений напрямую, они должны быть выведены JIT. Но в Wasm (особенно если добавляются подписи блоков) JIT может легко выяснить, что происходит со стеком значений и потоком управления, поэтому мне интересно, если бы CFG (или конкретно нередуцированный поток управления) были смоделированы явно, как циклы и блоки, может ли это избежать большинства неприятных угловых случаев, о которых вы думаете?

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

@oridb

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

Я согласен, что goto очень полезны для многих компиляторов. Вот почему такие инструменты, как Binaryen, позволяют создавать произвольные CFG с помощью gotos и могут очень быстро и эффективно преобразовать их в WebAssembly.

Это может помочь думать о WebAssembly как о вещи, оптимизированной для использования браузерами (как указал @titzer ). Большинству компиляторов, вероятно, не следует генерировать WebAssembly напрямую, а лучше использовать такой инструмент, как Binaryen, чтобы они могли генерировать goto, получать кучу бесплатных оптимизаций и не думать о низкоуровневых деталях двоичного формата WebAssembly (вместо этого вы испускаете IR, используя простой API).

Что касается полифилинга с помощью шаблона while-switch, о котором вы упоминаете: в emscripten мы начали с этого до того, как разработали метод «relooper» для воссоздания циклов. Паттерн while-switch в среднем примерно в 4 раза медленнее (но в некоторых случаях значительно меньше или больше, например, небольшие циклы более чувствительны). Я согласен с вами, что теоретически оптимизация с переходом на потоки может ускорить это, но производительность будет менее предсказуемой, поскольку некоторые виртуальные машины будут делать это лучше, чем другие. Он также значительно больше с точки зрения размера кода.

Это может помочь думать о WebAssembly как о вещи, оптимизированной для использования браузерами (как указал @titzer ). Большинству компиляторов, вероятно, следует не генерировать WebAssembly напрямую, а использовать такой инструмент, как Binaryen...

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

Но если вы оптимизировали использование веб-браузера, почему бы просто не определить веб-сборку как SSA, что, как мне кажется, больше соответствует тому, что я ожидал, и требует меньше усилий для «преобразования» в SSA?

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

Что касается представлений SSA, они, как правило, имеют большой размер кода. SSA отлично подходит для оптимизации кода, но не для его компактной сериализации.

@oridb См. комментарий @titzer «Дизайн конструкций потока управления WebAssembly упрощает потребителей, обеспечивая быструю, простую проверку , легкое преобразование за

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

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

Цель wasm — снять бремя оптимизации с потребителя среды выполнения. Существует сильное сопротивление добавлению сложности в компилятор времени выполнения, поскольку это увеличивает поверхность атаки. Так много проблем проектирования состоит в том, чтобы спросить, что можно сделать, чтобы упростить компилятор времени выполнения без ущерба для производительности, и много споров!

Что ж, возможно, сейчас уже слишком поздно, но я хотел бы подвергнуть сомнению идею о том, что алгоритм relooper или его варианты могут давать «достаточно хорошие» результаты во всех случаях. Очевидно, что в большинстве случаев они могут это сделать, так как большая часть исходного кода изначально не содержит непреодолимого потока управления, оптимизации обычно не делают вещи слишком сложными, а если и делают, например, как часть слияния повторяющихся блоков, их, вероятно, можно научить не к. А как быть с патологическими случаями? Например, что, если у вас есть сопрограмма, которую компилятор преобразовал в обычную функцию со структурой, подобной этой псевдо-C:

void transformed_coroutine(struct autogenerated_context_struct *ctx) {
    int arg1, arg2; // function args
    int var1, var2, var3, …; // all vars used by the function
    switch (ctx->current_label) { // restore state
    case 0:
        // initial state, load function args caller supplied and proceed to start
        arg1 = ctx->arg1;
        arg2 = ctx->arg2;
        break;
    case 1: 
        // restore all vars which are live at label 1, then jump there
        var2 = ctx->var2; 
        var3 = ctx->var3;
        goto resume_1;
    [more cases…]
    }

    [main body goes here...]
    [somewhere deep in nested control flow:]
        // originally a yield/await/etc.
        ctx->var2 = var2;
        ctx->var3 = var3;
        ctx->current_label = 1;
        return;
        resume_1:
        // continue on
}

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

Я не думаю, что есть хороший способ повторить что-то подобное, если «нормальный» поток управления достаточно сложен. (Может быть неправильно.) Либо вы дублируете массивные части функции, потенциально нуждаясь в отдельной копии для каждой точки выхода, либо вы превращаете все это в гигантский переключатель, который, согласно @kripken, в 4 раза медленнее, чем повторный цикл в типичном коде ( что само по себе, вероятно, несколько медленнее, чем вообще не нуждающееся в relooper).

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

Может быть, делать такое преобразование — плохая идея для начала, так как впоследствии ничто не доминирует над чем-либо, поэтому оптимизации на основе SSA мало что могут сделать… может быть, это лучше сделать на уровне сборки, может быть, вместо этого wasm должен получить нативную поддержку сопрограмм? Но компилятор может выполнить большинство оптимизаций до выполнения преобразования, и кажется, что, по крайней мере, разработчики сопрограмм LLVM не видели острой необходимости откладывать преобразование до генерации кода. С другой стороны, поскольку точная семантика, которую люди хотят получить от сопрограмм, довольно разнообразна (например, дублирование приостановленных сопрограмм, возможность проверять «стековые кадры» для GC), когда дело доходит до разработки переносимого байт-кода (а не компилятор), правильнее поддерживать уже преобразованный код более гибко, чем заставлять виртуальную машину выполнять преобразование.

Во всяком случае, сопрограммы — это только один пример. Другой пример, который я могу придумать, — это реализация виртуальной машины внутри виртуальной машины. В то время как более распространенной особенностью JIT являются боковые выходы , которые не требуют goto, есть ситуации, которые требуют боковых входов - опять же, требующих перехода в середину циклов и тому подобного. Другим вариантом могут быть оптимизированные интерпретаторы: интерпретаторы, ориентированные на wasm, не могут действительно соответствовать интерпретаторам, ориентированным на нативный код, который как минимум может повысить производительность с помощью вычисляемых переходов и может углубляться в сборку для большего… предсказатель ветвления, предоставляя каждому случаю свою собственную инструкцию перехода, так что вы могли бы воспроизвести часть эффекта, имея отдельный переключатель после каждого обработчика кода операции, где все случаи были бы просто переходами. Или, по крайней мере, иметь если или два, чтобы проверить конкретные инструкции, которые обычно следуют за текущей. Есть некоторые частные случаи этого шаблона, которые можно представить с помощью структурированного потока управления, но не общий случай. И так далее…

Конечно, есть какой-то способ разрешить произвольный поток управления, не заставляя виртуальную машину выполнять много работы. Идея соломенного человечка может быть сломана: у вас может быть схема, в которой разрешены переходы к дочерним областям, но только в том случае, если количество областей, которые вы должны ввести, меньше предела, определенного целевым блоком. Ограничение по умолчанию равно 0 (без переходов из родительских областей), что сохраняет текущую семантику, а ограничение блока не может быть больше, чем ограничение родительского блока + 1 (легко проверить). И виртуальная машина изменит свою эвристику доминирования с «X доминирует над Y, если он является родителем Y» на «X доминирует над Y, если он является родителем Y с расстоянием, превышающим предел прыжка дочернего элемента Y». (Это консервативное приближение, не гарантирующее представление точного набора доминаторов, но то же верно и для существующей эвристики — внутренний блок может доминировать над нижней половиной внешнего блока.) Поскольку только код с неприводимым потоком управления потребуется указать ограничение, в общем случае это не увеличит размер кода.

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

FWIW: Wasm имеет особый дизайн, который объясняется всего несколькими очень важными словами, «за исключением того, что ограничение вложенности делает невозможным ветвление в середину цикла снаружи цикла».

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

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

Можно расширить wasm до SSA-кодирования (добавить pick , разрешить блокам возвращать несколько значений и иметь элементы цикла, извлекающие значения), поэтому, что интересно, ограничения, требуемые для эффективного декодирования SSA, могут не понадобиться (потому что он уже может быть закодирован SSA)! Это приводит к функциональному языку (который для эффективности может иметь кодировку стиля стека).

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

(func f1 (arg1)
  (let ((c1 10)) ; Some values up the stack.
    (blocks ((b1 (a1 a2 a3)
                   ... (br b3)
               (br b2 (+ a1 a2 a3 arg1 c1)))
             (b2 (a1)
                 ... (br b1 ...))
             (b3 ()
                 ...))
   .. regular structured wasm ..
   (br b2 ...)
   ....
   (br b3)
    ...
   ))

Но будут ли веб-браузеры когда-либо обрабатывать это эффективно внутри?

Сможет ли кто-то, имеющий опыт работы со стековой машиной, распознать шаблон кода и сопоставить его с кодировкой стека?

Здесь есть интересное обсуждение неприводимых циклов http://bboissin.appspot.com/static/upload/bboissin-thesis-2010-09-22.pdf.

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

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

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

Для справки , я решительно поддерживаю @oridb и @comex в этом вопросе.
Я думаю, что это серьезная проблема, которую следует решить, пока не стало слишком поздно.

Учитывая природу WebAssembly, любые ошибки, которые вы совершаете сейчас , скорее всего, останутся с вами на десятилетия вперед (посмотрите на Javascript!). Вот почему этот вопрос так важен; избегайте поддержки goto сейчас по какой бы то ни было причине (например, для облегчения оптимизации, которая --- откровенно говоря --- влияние конкретной реализации на общую вещь, и, честно говоря, я думаю, что это лениво), и вы в конечном итоге получите проблемы в долгосрочной перспективе.

Я уже вижу будущие (или текущие, но в будущем) реализации WebAssembly, пытающиеся в особом случае распознавать обычные шаблоны while/switch для реализации меток, чтобы правильно их обрабатывать. Это взлом.

WebAssembly — это чистый лист, поэтому сейчас самое время избегать грязных хаков (точнее, требований к ним).

@darkuranium :

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

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

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

Учитывая природу WebAssembly, любые ошибки, которые вы совершаете сейчас , скорее всего, останутся с вами на десятилетия вперед (посмотрите на Javascript!). Вот почему этот вопрос так важен; избегайте поддержки goto сейчас по какой бы то ни было причине (например, для облегчения оптимизации, которая --- откровенно говоря --- влияние конкретной реализации на общую вещь, и, честно говоря, я думаю, что это лениво), и вы в конечном итоге получите проблемы в долгосрочной перспективе.

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

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

В противном случае я думаю, что goto — отличная возможность для будущего . Лично я, вероятно, сначала занялся бы другими, такими как генерация кода JIT. Это мой личный интерес после GC и потоков.

Привет. Я сейчас пишу перевод с webassembly на IR и обратно на webassembly, и я обсуждал эту тему с людьми.

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

if (x) goto inside_loop;
// banana
while(y) {
    // things
    inside_loop:
    // do things
}

Компиляторы EBB будут производить следующее:

entry:
    cjump x, inside_loop
    // banana
    jump loop

loop:
    cjump y, exit
    // things
    jump inside_loop

inside_loop:
    // do things
    jump loop
exit:
    return

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

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

<inside_loop, if(x)>
    // banana
<loop °>
<exit if(y)>
    // things
</inside_loop, if(x)>
    // do things
</loop ↑>
</exit>

Затем вам нужно построить стек из них. Какой из них идет ко дну? Это либо «внутренняя петля», либо «петля». Мы не можем этого сделать, поэтому нам нужно вырезать стек и копировать вещи:

if
    // do things
else
    // banana
end
loop
  br out
    // things
    // do things
end

Теперь мы можем перевести это на веб-сборку. Извините, я еще не знаком с тем, как строятся эти петли.

Это не особая проблема, если мы думаем о старом программном обеспечении. Вполне вероятно, что новое ПО переведено на веб-сборку. Но проблема в том, как работают наши компиляторы. Они выполняли поток управления с базовыми блоками в течение _десятилетий_ и предполагают, что все идет.

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

Но, например, следующее будет работать так же хорошо:

    cjump x, label(1)
    // banana
0: label
    cjump y, label(2)
    // things
1: label
    // do things
    jump label(0)
2: label
    // exit as usual, picking the values from the top of the stack.

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

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

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

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

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

Решение о том, как справиться с непреодолимым потоком управления в WebAssembly, объяснено в документе «Emscripten: компилятор LLVM-to-JavaScript». Relooper реорганизует программу примерно так:

_b_ = bool(x)
_b_ == 0 if
  // banana
end
block loop
  _b_ if
    // do things
    _b_ = 0
  else
    y br_if 2
    // things
    _b_ = 1
  end
  br 0
end end

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

Люди, компилирующие из веб-сборки, вероятно, приспособятся к обработке и разделению свернутого потока управления.

Так:

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

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

    • Однако, когда дело доходит до спецификации goto, это значительно упрощает работу! Инструкции блоковые уже за bikeshedding, и это не имеет большое значение ожидать производств компиляторов таргетинг wasm выразить приводимый поток управления , используя их - алгоритм не так уж трудно. Основная проблема заключается в том, что небольшая часть потока управления не может быть выражена с их помощью без снижения производительности. Если мы решим эту проблему, добавив новую инструкцию goto, нам не придется так сильно беспокоиться об эффективности кодирования, как если бы мы полностью изменили дизайн. Код, использующий goto, конечно, должен быть достаточно компактным, но он не должен конкурировать с другими конструкциями по компактности; это только для неприводимого потока управления и должно использоваться редко.

  • Сводимость не особенно полезна.

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

    • Я проверил текущие реализации WebAssembly в JavaScriptCore, V8 и SpiderMonkey, и все они, похоже, следуют этому шаблону. (V8 более сложен — некое представление «моря узлов», а не базовых блоков — но также отбрасывает вложенную структуру.)

    • Исключение : анализ циклов может быть полезен, и все три эти реализации передают в IR информацию о том, какие базовые блоки являются началом циклов. (Сравните с LLVM, который, как «тяжеловесный» бэкенд, разработанный для компиляции AOT, отбрасывает его и пересчитывает в бэкенде. Это более надежно, так как он может найти вещи, которые не выглядят как циклы в исходном коде, но делают после кучи оптимизаций, но медленнее.)

    • Анализ циклов работает с «естественными циклами», которые запрещают ответвления в середине цикла, которые не проходят через заголовок цикла.

    • WebAssembly должен по-прежнему гарантировать, что блоки loop являются естественными циклами.

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

    • Непреодолимый поток управления усложняет компиляцию WebAssembly в JavaScript (полифиллинг), поскольку компилятору придется запускать сам алгоритм повторного цикла.

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

    • По сравнению с этим немного усложнить компилятор не так уж и сложно.

    • Поэтому я не думаю, что есть веская причина не добавлять какую-либо поддержку неприводимого потока управления.

  • Основная информация, необходимая для построения представления SSA (которое по замыслу должно быть возможно за один проход), — это дерево доминаторов .

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

    • block :



      • В ББ, начинающем блок, доминирует предыдущий ББ.*


      • В ББ, следующем за соответствующим end , доминирует ББ, начинающий блок, но не ББ до end (потому что он будет пропущен, если было br вне блока). ).



    • loop :



      • В ББ, начинающем блок, доминирует предыдущий ББ.


      • BB после end доминирует над BB до end (поскольку вы не можете перейти к инструкции после end кроме как выполнив end ).



    • if :



      • На стороне if, на стороне else и на BB после end доминирует BB до if .



    • br , return , unreachable :



      • (BB сразу после br , return или unreachable недоступен.)



    • br_if , br_table :



      • ББ до br_if / br_table доминирует над ББ после него.



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

    • Пример ложноотрицательного результата:

      ```

      блокировать $внешний

      петля

      бр $внешний ;; так как это безоговорочно ломается, то тайно доминирует над концом ББ

      конец

      конец

    • Но это нормально, AFAIK.



      • Ложные срабатывания были бы плохими, потому что, например, если говорят, что базовый блок A доминирует над базовым блоком B, машинный код для B может использовать регистр, установленный в A (если ничто между ними не перезаписывает этот регистр). Если A на самом деле не доминирует над B, регистр может содержать мусорное значение.


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



    • В любом случае, подумайте о том, как должна работать инструкция goto в терминах дерева доминаторов. Предположим, что А доминирует над В, которое доминирует над С.

    • Мы не можем перейти от A к C, потому что это пропустит B (нарушение предположения о доминировании). Другими словами, мы не можем перейти к не непосредственным потомкам. (А со стороны бинарного производителя, если они рассчитали истинное дерево доминаторов, такого скачка никогда не будет.)

    • Мы могли бы безопасно перейти от A к B, но переход к непосредственному потомку не так уж и полезен. Это в основном эквивалентно оператору if или switch, который мы уже можем сделать (используя инструкцию if если есть только двоичный тест, или br_table если их несколько).

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

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

  • Исходя из этого, вот предложение соломенного человека:

    • Одна новая инструкция блочного типа:
    • labels resulttype N instr * end
    • Должно быть ровно N непосредственных дочерних инструкций, где «непосредственный дочерний элемент» означает либо инструкцию блочного типа ( loop , block или labels ) и все до соответствующего end или одна неблочная инструкция (которая не должна влиять на стек).
    • Вместо создания одной метки, как в других инструкциях блочного типа, labels создает N+1 меток: N указывает на N дочерних элементов, а одна указывает на конец блока labels . В каждом из дочерних элементов индексы меток от 0 до N-1 относятся к дочерним элементам по порядку, а индекс метки N относится к концу.

    Другими словами, если у вас есть
    loop ;; outer labels 3 block ;; child 0 br X end nop ;; child 1 nop ;; child 2 end end

    В зависимости от X br относится к:

    | Х | Цель |
    | ---------- | ------ |
    | 0 | конец block |
    | 1 | ребенок 0 (начало block ) |
    | 2 | ребенок 1 (нет) |
    | 3 | ребенок 2 (нет) |
    | 4 | конец labels |
    | 5 | начало внешнего цикла |

    • Выполнение начинается с первого потомка.

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

    • Ветвление к одному из дочерних элементов раскручивает стек операндов до его глубины в начале labels .

    • То же самое происходит и с ветвлением в конец, но если тип результата не пуст , ветвление в конец извлекает операнд и помещает его после раскручивания, аналогично block .

    • Доминирование: базовый блок перед инструкцией labels доминирует над каждым из потомков, а также BB после окончания labels . Дети не доминируют друг над другом или в конце.

    • Примечания к дизайну:

    • N указывается заранее, чтобы код можно было проверить за один проход. Было бы странно добраться до конца блока labels , чтобы узнать количество дочерних элементов, прежде чем узнать цели индексов в нем.

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

Было бы действительно здорово, если бы можно было прыгнуть в цикл, не так ли? IIUC, если бы этот случай был учтен, то неприятная комбинация loop+br_table никогда бы не понадобилась...

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

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

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

Не совсем: по крайней мере, в SM граф IR не является полностью общим графом; мы предполагаем определенные инварианты графа, которые следуют из генерации из структурированного источника (JS или wasm), и часто упрощаем и/или оптимизируем алгоритмы. Поддержка полностью общей CFG потребует либо аудита/изменения многих проходов в конвейере, чтобы не принимать эти инварианты (либо путем их обобщения, либо пессимизации их в случае неприводимости), либо предварительное дублирование с разделением узлов, чтобы сделать граф приводимым. Это, конечно, выполнимо, но неправда, что это просто вопрос того, что wasm является искусственным узким местом.

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

Когда в прошлом мы обсуждали обратно совместимые пути для расширения wasm с произвольной поддержкой goto, один большой вопрос заключается в том, какой вариант использования здесь: «упростить производителей, не запуская алгоритм типа relooper», или это «разрешить более эффективный codegen для фактически неприводимого потока управления»? Если это только первое, то я думаю, что нам, вероятно, понадобится какая-то схема встраивания произвольных меток/gotos (которая одновременно совместима с предыдущими версиями, а также компонуется с будущей блочной структурой try/catch); это просто вопрос взвешивания затрат/выгод и вопросов, упомянутых выше.

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

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

Когда в прошлом мы обсуждали обратно совместимые пути для расширения wasm с произвольной поддержкой goto, один большой вопрос заключается в том, какой вариант использования здесь: «упростить производителей, не запуская алгоритм типа relooper», или это «разрешить более эффективный codegen для фактически неприводимого потока управления»?

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

Мне действительно нужно собрать больше данных о том, насколько распространен на практике неприводимый поток управления…

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

Я открыт для убеждения, что моя вера неверна.

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

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

С другой стороны, мое предложение могло бы уже послужить достойным примитивом для интерпретаторов. Если вы комбинируете labels с br_table , вы получаете возможность указывать таблицу переходов непосредственно на произвольные точки в функции, что не сильно отличается от вычисляемого перехода. (В отличие от переключателя C, который, по крайней мере, изначально направляет поток управления к точкам внутри блока переключателя; если все случаи являются переходами, компилятор должен иметь возможность оптимизировать дополнительный переход, но он также может объединять несколько «избыточных» переключать операторы в один, разрушая преимущество наличия отдельного перехода после каждого обработчика инструкций.) Я не уверен, в чем проблема с распределением регистров, хотя...

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

>

Структура вложенного цикла, то, что гарантирует сводимость, такова:
почти выброшен в начале. [...] Я проверил текущий
Реализации WebAssembly в JavaScriptCore, V8 и SpiderMonkey, а также
все они, похоже, следуют этому образцу.

Не совсем: по крайней мере, в SM граф IR не является полностью общим графом; мы
предполагают определенные инварианты графа, которые следуют из генерируемого из
структурированный исходный код (JS или wasm) и часто упрощают и/или оптимизируют
алгоритмы.

То же самое в V8. На самом деле это одна из моих основных проблем с SSA в обоих случаях.
соответствующая литература и реализации, которые они почти никогда не определяют
что представляет собой «правильно сформированный» CFG, но имеют тенденцию неявно предполагать различные
в любом случае недокументированные ограничения, обычно обеспечиваемые построением
языковой интерфейс. Бьюсь об заклад, что многие/большинство оптимизаций в существующих компиляторах
не сможет работать с действительно произвольными CFG.

Как говорит @lukewagner , основной вариант использования непреодолимого контроля, вероятно,
«поточный код» для оптимизированных интерпретаторов. Трудно сказать, насколько они актуальны
для домена Wasm, и является ли его отсутствие на самом деле самым большим
узкое место.

Обсудив непреодолимый поток управления с рядом людей
исследуя IR компилятора, «самым чистым» решением, вероятно, было бы добавить
понятие взаимно рекурсивных блоков. Это случилось бы, чтобы соответствовать Васму
Структура управления довольно хорошо.

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

По памяти и, вероятно, неправильно, SPEC2006 имеет один неустранимый цикл в 401.bzip2 и все. На практике встречается довольно редко.

Clang будет выдавать только одну инструкцию indirectbr в функциях, использующих вычисляемый переход. Это приводит к превращению многопоточных интерпретаторов в естественные циклы с блоком indirectbr в качестве заголовка цикла. После выхода из LLVM IR единственный indirectbr дублируется в генераторе кода, чтобы восстановить исходную путаницу.

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

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

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

Лучший,
-Бен

В четверг, 20 апреля 2017 г., в 5:20, Якоб Стоклунд Олесен <
уведомления@github.com> написал:

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

По памяти и, вероятно, неправильно, SPEC2006 имеет один непреодолимый цикл в
401.bzip2 и все. На практике встречается довольно редко.

Clang будет выдавать только одну инструкцию косвенного br в функциях, использующих
вычисленный переход. Это приводит к превращению многопоточных интерпретаторов в
естественные циклы с блоком косвенных br в качестве заголовка цикла. После ухода
LLVM IR, единственный косвенный br дублируется хвостом в генераторе кода
восстановить первоначальный клубок.


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

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

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

Лучший,
-Бен

В понедельник, 1 мая 2017 г., в 12:48 Бен Л. Титцер [email protected] написал:

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

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

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

Лучший,
-Бен

В четверг, 20 апреля 2017 г., в 5:20, Якоб Стоклунд Олесен <
уведомления@github.com> написал:

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

По памяти и, вероятно, неправильно, SPEC2006 имеет один неустранимый цикл.
в 401.bzip2 и все. На практике встречается довольно редко.

Clang будет выдавать только одну инструкцию косвенного br в функциях, использующих
вычисленный переход. Это приводит к превращению многопоточных интерпретаторов в
естественные циклы с блоком косвенных br в качестве заголовка цикла. После ухода
LLVM IR, единственный косвенный br дублируется хвостом в генераторе кода
восстановить первоначальный клубок.


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

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

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

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

Эх... Я хотел ответить раньше, но жизнь помешала.

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

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

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

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

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

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

a:
  control = 1;
  goto x;
b:
  control = 2;
  goto x;
...
x:
  // use control

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

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

Что касается представления, что лучше: явное (инструкция labels или подобное) или неявное (оптимизация реального цикла+переключатели по определенному шаблону)?

Преимущества неявного:

  • Сохраняет спецификацию компактной.

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

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

Недостатки неявного:

  • Это похоже на взлом. Правда, как говорит @titzer , на самом деле это не

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

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

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

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

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

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

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

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

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

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

@comex Еще одно соображение заключается в том, сколько должны работать двигатели wasm. Например, выше вы упомянули Ion's AliasAnalysis, однако другая сторона истории заключается в том, что анализ псевдонимов не так важен для кода WebAssembly, по крайней мере, сейчас, когда большинство программ используют линейную память.

Алгоритм живучести Ion BacktrackingAllocator.cpp потребует некоторой доработки, но это не будет чрезмерно. Большая часть Ion уже обрабатывает различные формы непреодолимого потока управления, поскольку OSR может создавать несколько входов в циклы.

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

Кстати, в этом выпуске также стоит упомянуть алгоритм построения SSA «на лету» Брауна и др. , который представляет собой простой и быстрый алгоритм построения SSA «на лету» и поддерживает непреодолимый поток управления.

Меня интересует использование WebAssembly в качестве бэкенда qemu на iOS, где WebKit (и динамический компоновщик, но с проверкой подписи кода) — единственная программа, которой разрешено помечать память как исполняемую. Codegen Qemu предполагает, что операторы goto будут частью любого процессора, для которого он должен генерировать код, что делает серверную часть WebAssembly практически невозможной без добавления goto.

@tbodt -

@eholk Похоже, это будет намного медленнее, чем прямой перевод машинного кода в wasm.

@tbodt Использование Binaryen добавляет дополнительный IR на пути, да, но он не должен быть намного медленнее, я думаю, он оптимизирован для скорости компиляции. Кроме того, у него могут быть преимущества, отличные от обработки gotos и т. д., поскольку вы можете дополнительно запустить оптимизатор Binaryen, который может делать то, чего не делает оптимизатор qemu (вещи, специфичные для wasm).

На самом деле мне было бы очень интересно сотрудничать с вами в этом, если хотите :) Я думаю, что перенос Qemu на wasm был бы очень полезен.

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

@kripken Я был бы заинтересован в сотрудничестве, где лучше всего с тобой пообщаться?

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

Я не уверен, что кто-то еще пробовал это, поэтому, вероятно, будет много шероховатостей.

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

Еще одной проблемой будет выделение места в таблице по умолчанию. Как вы сопоставляете адрес с индексом таблицы?

Другой вариант — перегенерировать функцию wasm для каждого нового базового блока. Это означает количество повторных компиляций, равное количеству используемых блоков, но я готов поспорить, что это единственный способ заставить код работать быстро после его компиляции (особенно внутренние циклы), и он не должен быть полным. перекомпилировать, мы можем повторно использовать Binaryen IR для каждого существующего блока, добавить IR для нового блока и просто запустить relooper для всех из них.

(Но, может быть, мы можем заставить qemu компилировать всю функцию заранее, а не лениво?)

@tbodt за совместную работу с Binaryen, один из вариантов — создать репо с вашей работой (и использовать там задачи и т. д.), другой — открыть конкретную задачу в Binaryen для qemu.

Мы не можем заставить qemu компилировать всю функцию за раз, потому что в qemu нет понятия «функции».

Что касается перекомпиляции всего кеша блоков, то это может занять много времени. Я выясню, как использовать встроенный профилировщик qemu, а затем открою вопрос о бинарном.

Побочный вопрос. На мой взгляд, язык, ориентированный на WebAssembly, должен обеспечивать эффективную взаимно рекурсивную функцию. Для описания их полезности я бы предложил вам прочитать: http://sharp-gamedev.blogspot.com/2011/08/forgotten-control-flow-construct.html

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

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

@davidgrenier , все функции в модуле Wasm взаимно рекурсивны. Можете ли вы уточнить, что вы считаете неэффективным в них? Вы имеете в виду только отсутствие хвостовых звонков или что-то еще?

Приходят общие хвостовые звонки. Хвостовая рекурсия (взаимная или иная) будет частным случаем этого.

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

Goto очень полезен для генерации кода из диаграмм в визуальном программировании. Возможно, сейчас визуальное программирование не очень популярно, но в будущем оно может привлечь больше людей, и я думаю, что wasm должен быть к этому готов. Подробнее о генерации кода из диаграмм и перехода: http://drakon-editor.sourceforge.net/generation.html

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

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

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

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

Просто чтобы уточнить, для этого будет недостаточно обычного goto, для вашего варианта использования требуется

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

Похоже, у вас есть нормальный поток управления в каждой функции, но вам также нужна возможность переходить от входа в функцию к некоторым другим местам в «середине» при возобновлении горутины - сколько таких мест? Если это каждый базовый блок, то репетитор будет вынужден создавать цикл верхнего уровня, через который проходит каждая инструкция, но если их всего несколько, это не должно быть проблемой. (На самом деле это то, что происходит с поддержкой setjmp в emscripten — мы просто создаем дополнительные необходимые пути между базовыми блоками LLVM и позволяем повторному циклу обрабатывать их в обычном режиме.)

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

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

Можно ли будет вызвать функцию в стиле CPS или реализовать call/cc в WASM?

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

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

  • Неструктурированный поток управления делает возможными неприводимые графы потока управления
  • Устранение * любой «быстрой, простой проверки, простого преобразования в форму SSA за один проход»
  • Открытие компилятора JIT для нелинейной производительности
  • Люди, просматривающие веб-страницы, не должны страдать от задержек, если исходный компилятор языка может выполнить предварительную работу.

_*хотя могут быть альтернативы, такие как алгоритм построения SSA "на лету" Брауна и др., который обрабатывает непреодолимый поток управления_

Если мы все еще застряли там, вызовы _and_ tail продвигаются вперед, возможно, стоит попросить языковые компиляторы по-прежнему переводить в gotos, но в качестве последнего шага перед выводом WebAssembly разбить «блоки меток» на функции и преобразовать gotos в хвостовые вызовы.

Согласно статье 1977 года дизайнера схемы Гая Стила Lambda: The Ultimate GOTO , преобразование должно быть возможным, а производительность хвостовых вызовов должна быть в состоянии близко соответствовать gotos.

Мысли?

Если мы все еще застряли там, вызовы _and_ tail продвигаются вперед, возможно, стоит попросить языковые компиляторы по-прежнему переводить в gotos, но в качестве последнего шага перед выводом WebAssembly разбить «блоки меток» на функции и преобразовать gotos в хвостовые вызовы.

По сути, это то, что в любом случае сделал бы каждый компилятор, никто из тех, кого я знаю, не выступает за неуправляемые переходы, которые вызывают так много проблем в JVM, только за граф типизированных EBB. LLVM, GCC, Cranelift и все остальные имеют (возможно, неприводимую) SSA-форму CFG в качестве своего внутреннего представления, и компиляторы от Wasm до нативного имеют такое же внутреннее представление, поэтому мы хотим сохранить как можно больше этой информации и восстановить как можно меньше этой информации. Локальные значения с потерями, так как они больше не являются SSA, а поток управления Wasm с потерями, так как это больше не произвольный CFG. AFAIK, если бы Wasm был регистровой машиной SSA с бесконечным регистром и встроенной мелкозернистой информацией о живучести регистра, вероятно, был бы лучшим для codegen, но размер кода раздулся бы, стековая машина с потоком управления, смоделированным на произвольной CFG, вероятно, является лучшим промежуточным звеном . Я могу ошибаться насчет размера кода с помощью регистровой машины, но его можно эффективно закодировать.

Дело в неприводимом потоке управления заключается в том, что если он неприводим во внешнем интерфейсе, он по-прежнему неприводим в wasm, преобразование relooper/stackifier не делает поток управления приводимым, оно просто преобразует неприводимость в зависимость от значений времени выполнения. Это дает бэкенду меньше информации, и поэтому он может создавать худший код. Единственный способ создать хороший код для неприводимых CFG прямо сейчас — это обнаружить шаблоны, испускаемые relooper и stackifier, и преобразовать их обратно в неприводимый CFG. Если вы не разрабатываете V8, который, насколько я знаю, поддерживает только сокращаемый поток управления, поддержка несокращаемого потока управления является чистой победой — она значительно упрощает как интерфейсы, так и серверы (интерфейсы могут просто генерировать код в том же формате, в котором они хранятся внутри, бэкенды не не нужно обнаруживать шаблоны), при этом производя лучший результат в случае, когда поток управления является неприводимым, и вывод, который так же хорош или лучше в обычном случае, когда поток управления является приводимым.

Кроме того, это позволит GCC и Go начать производство WebAssembly.

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

Разве v8 не может просто включить relooper, чтобы принимать входные CFG? Похоже, что большие куски экосистемы заблокированы из-за деталей реализации v8.

Просто для справки: я заметил, что операторы switch в c++ очень медленны в wasm. Когда я профилировал код, мне приходилось преобразовывать его в другие формы, которые работали гораздо быстрее для обработки изображений. И это никогда не было проблемой на других архитектурах. Я действительно хотел бы goto по соображениям производительности.

@graph , не могли бы вы предоставить более подробную информацию о том, как «операторы переключения работают медленно»? Всегда ищите возможность улучшить производительность... (Если вы не хотите увязнуть в этой теме, напишите мне напрямую, [email protected].)

Я опубликую здесь, так как это относится ко всем браузерам. Простые операторы, подобные этому, при компиляции с emscripten были быстрее, когда я конвертировал их в операторы if.

for(y = ....) {
    for(x = ....) {
        switch(type){
        case IS_RGBA:....
         ....
        case IS_BGRA
        ....
        case IS_RGB
        ....
....

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

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

@graph Полный тест был бы очень полезен здесь. В общем, переключатель в C может превратиться в очень быструю таблицу переходов в wasm, но есть крайние случаи, которые пока плохо работают, и нам может понадобиться исправить их либо в LLVM, либо в браузерах.

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

@graph , если emscripten создаст br_table, то jit иногда будет генерировать таблицу переходов, а иногда (если он думает, что это будет быстрее) он будет искать ключевое пространство линейно или с помощью встроенного двоичного поиска. То, что он делает, часто зависит от размера коммутатора. Конечно, возможно, что политика выбора не оптимальна ... Я согласен с @kripken , исполняемый код был бы очень полезен здесь, если у вас есть чем поделиться.

(Не знаю насчет v8 или jsc, но Firefox в настоящее время не распознает цепочку «если-то-иначе» как возможный переключатель, поэтому обычно не рекомендуется использовать переключатели с открытым кодом для длинных цепочек «если-то-иначе». точка безубыточности, вероятно, находится не более чем в двух или трех сравнениях.)

@lars-t-hansen @kripken @graph вполне может быть, что br_table в настоящее время просто очень не оптимизирован, как показывает этот обмен: https://twitter.com/battagline/status/1168310096515883008

@aardappel , это любопытно, тесты, которые я провел вчера, не показали этого, в Firefox в моей системе точка безубыточности была около 5 случаев, насколько я помню, и после этого br_table стал победителем. microbenchmark, конечно, и с некоторой попыткой равномерного распределения ключей поиска. если гнездо «если» смещено в сторону наиболее вероятных ключей, так что требуется не более пары тестов, то гнездо «если» победит.

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

@ lars-t-hansen Да, мы не знаем его контрольный пример, возможно, он имел необычное значение. В любом случае, похоже, что у Chrome больше работы, чем у Firefox.

Я в отпуске, поэтому и не отвечаю. Спасибо за понимание.

@kripken @


Main.cpp

#include <stdio.h>

#include <chrono>
#include <random>

class Chronometer {
public:
    Chronometer() {

    }

    void start() {
        mStart = std::chrono::steady_clock::now();
    }

    double seconds() {
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
    }

private:
    std::chrono::steady_clock::time_point mStart;
};

int main() {
    printf("Starting tests!\n");
    Chronometer timer;
    // we want to prevent optimizations based on known size as most applications
    // do not know the size in advance.
    std::random_device rd;  //Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
    std::uniform_int_distribution<> dis(100000000, 1000000000);
    std::uniform_int_distribution<> opKind(0, 3);
    int maxFrames = dis(gen);
    int switchSelect = 0;
    constexpr int SW1 = 1;
    constexpr int SW2 = 8;
    constexpr int SW3 = 32;
    constexpr int SW4 = 38;

    switch(opKind(gen)) {
    case 0:
        switchSelect = SW1;
        break;
    case 1:
        switchSelect = SW2; break;
    case 2:
        switchSelect = SW3; break;
    case 4:
        switchSelect = SW4; break;
    }
    printf("timing with SW = %d\n", switchSelect);
    timer.start();
    int accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        switch(switchSelect) {
        case SW1:
            accumulator = accumulator*3 + i; break;
        case SW2:
            accumulator = (accumulator < 3)*i; break;
        case SW3:
            accumulator = (accumulator&0xFF)*i + accumulator; break;
        case SW4:
            accumulator = (accumulator*accumulator) - accumulator + i; break;
        }
    }
    printf("switch time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);
    timer.start();
    accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        if(switchSelect == SW1)
            accumulator = accumulator*3 + i;
        else if(switchSelect == SW2)
            accumulator = (accumulator < 3)*i;
        else if(switchSelect == SW3)
            accumulator = (accumulator&0xFF)*i + accumulator;
        else if(switchSelect == SW4)
            accumulator = (accumulator*accumulator) - accumulator + i;
    }
    printf("if-else time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);

    return 0;
}

В зависимости от значения switchSelect. if-else превосходит. Пример вывода:

Starting tests!
timing with SW = 32
switch time = 2.049000 seconds
accumulated value: 0
if-else time = 0.401000 seconds
accumulated value: 0

Как видите, для switchSelect = 32 if-else намного быстрее. Для других случаев if-else немного быстрее. Для случая switchSelect = 1 & 0 оператор switch работает быстрее.

Test in Firefox 69.0.3 (64-bit)
compiled using: emcc -O3 -std=c++17 main.cpp -o main.html
emcc version: emcc (Emscripten gcc/clang-like replacement) 1.39.0 (commit e047fe4c1ecfae6ba471ca43f2f630b79516706b)

Используется последняя стабильная версия emscripen от 20 октября 2019 года. Свежая установка ./emcc activate latest .

Я заметил выше, что есть опечатка, но это не должно влиять на то, что if-else быстрее в случае SW3, поскольку они выполняют одни и те же инструкции.

опять же с выходом за точку безубыточности 5: Интересно, что для switchSelect=32 для этого случая по скорости аналогично if-else. Как видите, для 1003 if-else немного быстрее. Переключатель должен победить в этом случае.

Starting tests!
timing with SW = 1003
switch time = 2.253000 seconds
accumulated value: 1903939380
if-else time = 2.197000 seconds
accumulated value: 1903939380


main.cpp

#include <stdio.h>

#include <chrono>
#include <random>

class Chronometer {
public:
    Chronometer() {

    }

    void start() {
        mStart = std::chrono::steady_clock::now();
    }

    double seconds() {
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
    }

private:
    std::chrono::steady_clock::time_point mStart;
};

int main() {
    printf("Starting tests!\n");
    Chronometer timer;
    // we want to prevent optimizations based on known size as most applications
    // do not know the size in advance.
    std::random_device rd;  //Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
    std::uniform_int_distribution<> dis(100000000, 1000000000);
    std::uniform_int_distribution<> opKind(0, 8);
    int maxFrames = dis(gen);
    int switchSelect = 0;
    constexpr int SW1 = 1;
    constexpr int SW2 = 8;
    constexpr int SW3 = 32;
    constexpr int SW4 = 38;
    constexpr int SW5 = 64;
    constexpr int SW6 = 67;
    constexpr int SW7 = 1003;
    constexpr int SW8 = 256;

    switch(opKind(gen)) {
    case 0:
        switchSelect = SW1;
        break;
    case 1:
        switchSelect = SW2; break;
    case 2:
        switchSelect = SW3; break;
    case 3:
        switchSelect = SW4; break;
    case 4:
        switchSelect = SW5; break;
    case 5:
        switchSelect = SW6; break;
    case 6:
        switchSelect = SW7; break;
    case 7:
        switchSelect = SW8; break;
    }
    printf("timing with SW = %d\n", switchSelect);
    timer.start();
    int accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        switch(switchSelect) {
        case SW1:
            accumulator = accumulator*3 + i; break;
        case SW2:
            accumulator = (accumulator < 3)*i; break;
        case SW3:
            accumulator = (accumulator&0xFF)*i + accumulator; break;
        case SW4:
            accumulator = (accumulator*accumulator) - accumulator + i; break;
        case SW5:
            accumulator = (accumulator << 3) - accumulator + i; break;
        case SW6:
            accumulator = (i - accumulator) & 0xFF; break;
        case SW7:
            accumulator = i*i + accumulator; break;
        }
    }
    printf("switch time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);
    timer.start();
    accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        if(switchSelect == SW1)
            accumulator = accumulator*3 + i;
        else if(switchSelect == SW2)
            accumulator = (accumulator < 3)*i;
        else if(switchSelect == SW3)
            accumulator = (accumulator&0xFF)*i + accumulator;
        else if(switchSelect == SW4)
            accumulator = (accumulator*accumulator) - accumulator + i;
        else if(switchSelect == SW5)
            accumulator = (accumulator << 3) - accumulator + i;
        else if(switchSelect == SW6)
            accumulator = (i - accumulator) & 0xFF;
        else if(switchSelect == SW7)
            accumulator = i*i + accumulator;

    }
    printf("if-else time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);

    return 0;
}


Спасибо, ребята, за внимание к этим тестовым случаям.

Это очень разреженный switch , который LLVM в любом случае должен преобразовать в эквивалент набора if-then, но, по-видимому, он делает это менее эффективно, чем ручное if-then. Пробовали ли вы запускать wasm2wat, чтобы увидеть, чем эти два цикла отличаются в коде?

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

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

Переключатель в узких петлях использовался, потому что это был более чистый код, а не производительность. Но для wasm влияние на производительность было слишком велико, поэтому он был преобразован в более уродливые операторы if. Для обработки изображений во многих моих случаях использования, если я хочу большей производительности от переключателя, я бы переместил переключатель за пределы цикла и просто имел копии цикла для каждого случая. Обычно переключатель просто переключается между какой-либо формой пиксельного формата, цветового формата, кодирования и т. д. И во многих случаях константы вычисляются с помощью определений или перечислений, а не линейно. Теперь я вижу, что моя проблема не связана с дизайном goto. У меня просто было неполное понимание того, что происходит с моими операторами switch. Я надеюсь, что мои заметки будут полезны разработчикам браузеров, читающим это, чтобы оптимизировать wasm для обработки изображений в этих случаях. Спасибо.

Я никогда не думал, что гото может быть таким жарким спором 😮 . Я в лодке у каждого языка должен быть goto 😁 . Еще одна причина добавить goto — это упрощает компиляцию компилятором в wasm. Я почти уверен, что это где-то упоминалось выше. Теперь мне не на что жаловаться 😞 .

Там есть дальнейший прогресс?

из-за жарких дебатов я бы предположил, что какой-то браузер добавит поддержку goto как нестандартного расширения байт-кода. Тогда, возможно, GCC сможет войти в игру как поддерживающая нестандартную версию. Что я не думаю, что в целом хорошо, но позволит увеличить конкуренцию компиляторов. Это учтено?

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

@graph для меня твое предложение звучит как "давайте все
Это так не работает. Текущая структура WebAssembly дает МНОГИЕ преимущества (к сожалению, не очевидные). Попробуйте глубже погрузиться в философию wasm.

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

Понятно, что wasm в его нынешнем состоянии имеет некоторые серьезные упущения. Люди работают над заполнением пробелов (например, упомянутый @binji ), но я не думаю, что «глобальную структуру wasm» нужно переделывать. Просто мое скромное мнение.

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

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

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

Мне просто нравится процесс здесь. Сторона A объясняет, почему это необходимо в конкретных приложениях. Сторона B говорит, что они делают это неправильно, но не предлагает поддержки для этого приложения. Сторона А объясняет, почему ни один из прагматических аргументов Б не выдерживает никакой критики. Сторона Б не хочет иметь с этим дело, потому что считает, что сторона А поступает неправильно. Сторона А пытается достичь цели. Сторона Б говорит, что это неправильная цель, называя ее ленивой или грубой. Более глубокие философские смыслы теряются на стороне А. Прагматические теряются на стороне Б, поскольку они претендуют на некое более высокое моральное основание. Сторона А видит в этом аморальную механистическую операцию. В конечном счете, сторона B, как правило, сохраняет контроль над спецификацией, к лучшему или к худшему, и они сделали невероятно много с их относительной чистотой.

Честно говоря, я просто сунул сюда свой нос, потому что несколько лет назад я пытался сделать порт TinyCC для WASM, чтобы я мог запустить среду разработки на ESP8266, ориентированную на ESP8266. У меня всего ~4 МБ памяти, поэтому о повторном зацикливании и переключении на AST, а также о многих других изменениях не может быть и речи. (Примечание: почему relooper похож на relooper? Это оооочень ужасно, и никто не переписал эту херню на C!?) Даже если бы это было возможно на данный момент, я не знаю, стал бы я писать цель TinyCC. на WASM, поскольку мне это уже не так интересно.

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

Может ли кто-нибудь, ответственный за V8, подтвердить в этой ветке, что текущая реализация V8 не влияет на противодействие непреодолимому потоку управления?

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

Что ж, как ни прискорбно, текущие реализации, существующие до сих пор, настолько важны, что будущее (предположительно более длинное, чем прошлое) не так важно. Я пытался объяснить это в № 1202, что согласованность важнее, чем несколько реализаций, но, похоже, я заблуждаюсь. Удачи в объяснении того, что некоторые решения по разработке где-то в каком-то проекте не являются универсальной истиной или должны по умолчанию считаться правильными.

Этот поток - одна канарейка в угольной шахте W3C. Хотя я с большим уважением отношусь ко многим представителям W3C, решение доверить JavaScript Ecma International, а не W3C, было принято не без предрассудков.

Как и @cnlohr , я надеялся на порт TCC wasm, и не зря;

«Wasm разработан как переносимая цель компиляции для языков программирования, позволяющая развертывать в Интернете клиентские и серверные приложения». - webassembly.org

Конечно, любой может рассуждать, почему goto — это [ВСТАВЬТЕ ЖАРгон], но как насчет того, чтобы мы предпочитали стандарты мнениям. Мы все можем согласиться с тем, что POSIX C является хорошей базовой целью, особенно с учетом того, что сегодняшние языки либо созданы из C, либо сравниваются с ним, а заголовок домашней страницы WASM рекламирует себя как переносимую цель компиляции для языков. Конечно, некоторые функции, такие как потоки и simd, будут намечены. Но полное игнорирование чего-то столь же фундаментального, как goto , даже не придание ему приличия дорожной карты, не соответствует заявленной цели WASM и такой позиции органа по стандартизации, которая дала зеленый свет <marquee> находится за гранью.

В соответствии со стандартом кодирования SEI CERT C Rec. «Рассмотрите возможность использования цепочки goto при выходе из функции с ошибкой при использовании и освобождении ресурсов» ;

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

Затем в рекомендации предлагается пример предпочтительного решения POSIX C с использованием goto . Скептики укажут на примечание о том, что goto по-прежнему считается вредным . Интересно, что это мнение не воплощено ни в одном из этих конкретных стандартов кодирования, просто примечание. Что подводит нас к канарейке, «считающейся вредной».

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

Функции не нулевой стоимости, даже в C. Если кто-то предлагает замену gotos & labels, canihaz пожалуйста! Если кто-то скажет, что мне это не нужно, откуда они это узнают? Когда дело доходит до производительности, goto может дать нам немного больше, чем трудно возразить инженерам, что нам не нужны производительные, простые в использовании функции, которые существовали с самого начала языка.

Без плана по поддержке goto WASM является игрушечной целью компиляции, и это нормально, возможно, именно так W3C видит Интернет. Я надеюсь, что WASM как стандарт выйдет за пределы 32-битного адресного пространства и вступит в гонку компиляции. Я надеюсь, что инженерный дискурс сможет уйти от "это невозможно...", чтобы позволить ускорить расширение GCC C, такое как метки как значения, потому что WASM должен быть УДИВИТЕЛЬНЫМ. На мой взгляд, TCC гораздо более впечатляющий и полезный на данный момент, без лишней болтовни, без хипстерской целевой страницы и блестящего логотипа.

@d4tocchini :

В соответствии со стандартом кодирования SEI CERT C Rec. «Рассмотрите возможность использования цепочки goto при выходе из функции с ошибкой при использовании и освобождении ресурсов» ;

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

Затем в рекомендации предлагается пример предпочтительного решения POSIX C с использованием goto . Скептики укажут на примечание о том, что goto по-прежнему считается вредным . Интересно, что это мнение не воплощено ни в одном из тех конкретных стандартов кодирования, просто примечание. Что подводит нас к канарейке, «считающейся вредной».

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

@rossberg , хорошее замечание по поводу помеченных разрывов в этом примере, но я не согласен с вашим качественным предположением, что C должен «отступить». goto — более богатая конструкция, чем помеченные разрывы. Если C должен быть включен в число переносимых целей компиляции, а C не поддерживает помеченные разрывы, это скорее немая точка. Java пометила перерывы/продолжения, в то время как Python отклонил предложенную функцию , и, учитывая, что и Sun JVM, и CPython по умолчанию написаны на C, не согласитесь ли вы, что C как поддерживаемый язык должен быть выше в списке приоритетов?

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

Есть ли язык, который нельзя написать на C? C как язык должен информировать о функциях WASM. Если POSIX C невозможен с сегодняшним WASM, тогда есть правильный план действий.

Не совсем по теме спора, но чтобы не затенять, что тут и там в аргументации таятся случайные ошибки:

Python имеет помеченные перерывы

Можете ли вы уточнить? (Ака: Python не имеет помеченных разрывов.)

@pfalcon , да, мой плохой, я отредактировал свой комментарий, чтобы уточнить, что Python предложил пометить перерывы / продолжение, и отклонил его.

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

1) Обратите внимание, сколько всего этого присутствует в musl libc, а не непосредственно в emscripten. (Вторым наиболее часто используемым является тесты/третьи_партии)
2) Конструкции исходного кода не совпадают с инструкциями байт-кода.
3) Emscripten не находится на том же уровне абстракции, что и стандарт wasm, поэтому его не следует пересматривать на этой основе.

В частности, сегодня может быть полезно переписать gotos из libc, потому что тогда у нас будет больше контроля над результирующим cfg, чем если бы мы доверяли relooper/cfgstackify. У нас нет, потому что это нетривиальный объем работы, чтобы получить сильно отличающийся код от исходного musl.

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

такая позиция органа по стандартизации, который дал зеленый свет <marquee> выходит за рамки приличия.

Это особенно глупое заявление.

1) Нам, всему Интернету, осталось более десяти лет до того, как мы примем это решение.
2) We-the-wasm-CG — полностью (почти?) отдельная группа людей из этого тега, и, вероятно, их тоже раздражают очевидные прошлые ошибки.

без лишней болтовни, без хипстерской целевой страницы и блестящего логотипа.

Это можно было бы перефразировать на «Я расстроен», не сталкиваясь с проблемами тона.

Как показывает эта ветка, эти разговоры и так достаточно сложны.

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

Я думаю, что эта ветка уже давно перестала быть продуктивной - она ​​​​работает уже более четырех лет, и похоже, что здесь были использованы все возможные аргументы за и против произвольных goto s; следует также отметить, что ни один из этих аргументов не является особенно новым;)

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

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

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

@penzn Почему предложение функклетов застряло? Он существует с октября 2018 года и все еще находится в фазе 0.

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

@J0eCool

  1. Обратите внимание, как много этого присутствует в musl libc, а не непосредственно в emscripten. (Вторым наиболее часто используемым является тесты/третьи_партии)

Да, намек был на то, как часто он используется в C в целом.

  1. Конструкции исходного кода отличаются от инструкций байт-кода.

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

  1. Emscripten не находится на том же уровне абстракции, что и стандарт wasm, поэтому его не следует пересматривать на этой основе.

Дело в том, что вы найдете goto в большинстве крупных проектов C, даже в наборе инструментов WebAssembly в целом. Целевой переносимый компилятор для языков в целом, который недостаточно выразителен для своих собственных компиляторов, не совсем соответствует характеру нашего предприятия.

В частности, сегодня может быть полезно переписать gotos из libc, потому что тогда у нас будет больше контроля над результирующим cfg, чем доверие к relooper/cfgstackify.

Это круговое. Многие выше подняли серьезные оставшиеся без ответа вопросы относительно безошибочности такого требования.

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

Можно удалить gotos, как вы сказали, это нетривиальный объем работы ! Вы предлагаете всем остальным дико расходиться по путям кода, потому что gotos не должны поддерживаться?

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

Проблеск надежды! Я был бы доволен, если бы поддержка goto/label была воспринята серьезно с пунктом дорожной карты + официальным приглашением сдвинуть дело с мертвой точки, даже если годы спустя.

Это особенно глупое заявление.

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

«Может ли кто-нибудь, ответственный за V8, подтвердить в этой ветке, что текущая реализация V8 не влияет на противодействие непреодолимому потоку управления?

Я спрашиваю, потому что это то, что беспокоит меня больше всего. [...]

Если вы, ребята, прислушиваетесь к чему-либо полезному, примите близко к сердцу отзыв @neelance о Go 1.11. С этим трудно поспорить. Конечно, мы все можем выполнить нетривиальную чистку goto, но даже в этом случае мы получаем серьезный удар по производительности, который можно исправить только с помощью инструкции goto.

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

Вот еще одна реальная проблема, вызванная отсутствием goto/funclets: https://github.com/golang/go/issues/42979.

Для этой программы компилятор Go в настоящее время генерирует двоичный файл wasm с 18 000 вложенных block s. Сам двоичный файл wasm имеет размер 2,7 МБ, но когда я запускаю его через wasm2wat я получаю файл .wat размером 4,7 ГБ. 🤯

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

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

Я также нахожу странным, как разработчики WebAssembly кажутся столь яростными против этого, несмотря на то, что почти все со стороны компилятора говорят им, что это необходимо. Помните: WebAssembly — это стандарт, а не манифест. И дело в том, что большинство современных компиляторов используют некоторые формы SSA + базовые блоки внутри (или что-то почти эквивалентное, с теми же свойствами), которые не имеют концепции явных циклов[2]. Даже JIT используют что-то подобное, настолько это распространено.
Абсолютное требование повторного цикла без аварийного люка «просто используйте goto», насколько мне известно[3], беспрецедентно за пределами трансляторов с языка на язык --- и даже в этом случае, только переводчики с языка на язык, которые целевые языки без goto. В частности, я никогда не слышал, чтобы это нужно было делать для любого типа IR или байт-кода, кроме WebAssembly.

Возможно, пришло время переименовать WebAssembly в WebEmscripten (Вебскриптен?).

Как сказал @d4tocchini , если бы не монопольный статус WebAssembly (необходимый из-за ситуации со стандартизацией), он, вероятно, уже был бы разветвлен во что-то, что может разумно поддерживать то, что разработчики компилятора уже знают, что он должен поддерживать.
И нет, «просто используйте emscripten» не является допустимым контраргументом, потому что он делает стандарт зависимым от одного поставщика компилятора. Надеюсь, мне не нужно объяснять вам, почему это плохо.

РЕДАКТИРОВАТЬ: я забыл добавить одну вещь:
Вы до сих пор не уточнили, является ли вопрос техническим, философским или политическим. Я подозреваю последнее, но был бы рад оказаться неправым (потому что технические и философские вопросы решаются гораздо легче, чем политические).

Вот еще одна реальная проблема, вызванная отсутствием goto/funclets: golang/go#42979.

Для этой программы компилятор Go в настоящее время генерирует двоичный файл wasm с 18 000 вложенных block s. Сам двоичный файл wasm имеет размер 2,7 МБ, но когда я запускаю его через wasm2wat я получаю файл .wat размером 4,7 ГБ. 🤯

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

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

И нет, "просто используйте emscripten" не является действительным контраргументом.

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

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

@neelance в модифицированном примере, где код разделен между несколькими функциями, показано, что накладные расходы памяти (время выполнения/компиляция) намного ниже. Генерируется ли в этом случае меньше блоков, или просто отдельные функции означают, что движок GC может быть более гранулированным?

@conrad-watt Память использует даже не код Go, а хост WebAssembly: когда я создаю экземпляр двоичного файла wasm с помощью Chrome 86, мой процессор достигает 100% в течение 2 минут, а использование памяти на вкладке достигает пика в 11,3 ГБ. Это происходит до того, как будет выполнен двоичный код wasm / код Go. Проблема заключается в форме бинарного файла wasm.

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

Чтобы попытаться устранить неоднозначность моего предыдущего вопроса - если разделенная версия кода скомпилируется в Wasm с меньшим количеством блоков (из-за некоторой причуды повторного цикла), это будет одним из объяснений уменьшения накладных расходов на память и будет хорошей мотивацией для добавления более общего управление потоком в Wasm.

В качестве альтернативы, может случиться так, что разделенный код приводит к (примерно) одинаковому общему количеству блоков, но поскольку каждая функция компилируется отдельно JIT, метаданные/IR, используемые для компиляции каждой функции, могут быть более охотно собраны движком Wasm. . Аналогичная проблема возникла в V8 несколько лет назад при разборе/компиляции больших функций asm.js. В этом случае введение более общего потока управления в Wasm не решит проблему.

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

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

В этом случае введение более общего потока управления в Wasm не решит проблему.

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

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

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

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

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

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

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

Наоборот: первое присваивание вложено так глубоко, а не последнее. Вложенные block s и одиночный br_table вверху — это то, как традиционный оператор switch выражается в wasm. Это таблица прыжков, о которой я упоминал. Нет 3000 вложенных циклов.

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

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

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

Конечно, но проблема с производительностью здесь будет ортогональна тому, как поток управления между этими базовыми блоками выражается в исходном языке (т.е. не мотивация для более общего потока управления в Wasm). Чтобы убедиться, что V8 здесь особенно плох, можно проверить, демонстрируют ли FireFox/SpiderMonkey или Lucet/Cranelift одинаковые накладные расходы на компиляцию.

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

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

Я вижу вашу точку зрения.

Я по-прежнему считаю, что представление базовых блоков не с помощью инструкций перехода, а с помощью переменной перехода и огромной таблицы переходов/вложенных блоков представляет собой довольно сложное выражение простой концепции базовых блоков. Это приводит к снижению производительности и риску возникновения проблем со сложностью, подобных той, что мы видели здесь. Я считаю, что простые системы лучше и надежнее, чем сложные системы. Я до сих пор не видел аргументов, которые убедили бы меня в том, что более простая система — плохой выбор. Я только слышал, что V8 будет трудно реализовать произвольный поток управления, и мой открытый вопрос, чтобы сказать мне, что это утверждение неверно (https://github.com/WebAssembly/design/issues/796#issuecomment-623431527), не имеет значения. ответили еще.

@неланс

Chrome даже может запустить код до завершения интенсивного процесса.

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

В более общем плане: как вы думаете, смогут ли планы горутин в Go? Это лучшая ссылка, которую я могу найти, но сейчас она довольно активна, с встречами раз в две недели и несколькими сильными вариантами использования, которые мотивируют работу. Если Go может использовать сопрограммы wasm, чтобы избежать шаблона большого переключателя, то я думаю, что произвольные gotos не понадобятся.

Компилятор Go не использует алгоритм relooper, потому что он по своей сути несовместим с концепцией переключения горутин.

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

Я был бы очень рад поэкспериментировать с этим на Go, если вам интересно! Очевидно, это будет не так хорошо, как встроенная поддержка переключения стека в wasm, но это может быть лучше, чем уже существующий шаблон большого переключения. И было бы проще позже перейти на встроенную поддержку переключения стека. Конкретно, как этот эксперимент мог бы работать, так это заставить Go генерировать код с нормальной структурой, вообще не беспокоясь о переключении стека, и просто генерировать вызов специальной функции maybe_switch_goroutine в соответствующих точках. Преобразование Asyncify в основном позаботится обо всем остальном.

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

@kripken Спасибо за очень полезный пост.

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

Вот бинарный файл wasm , который можно запустить с помощью wasm_exec.html .

Как вы думаете, смогут ли планы переключения стека wasm решить проблемы с реализацией горутин в Go?

Да, на первый взгляд кажется, что это поможет.

Однако у нас есть хорошие результаты при использовании структурированного потока управления wasm + Asyncify.

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

Я считаю, что алгоритм стека LLVM проще/лучше, если вы хотите это реализовать: https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2

Я подал заявку на проект Go: https://github.com/golang/go/issues/43033.

@neelance , приятно видеть, что предложение @kripken немного помогает с golang + wasm. Учитывая, что эта проблема связана с переходом/метками, а не переключением стека, и учитывая, что Asyncify вводит новые сборки deps/специального корпуса с Asyncify до тех пор, пока не будет выпущено переключение стека и т. д., вы бы охарактеризовали это как решение или менее чем оптимальное смягчение? Как это соотносится с предполагаемыми преимуществами, если бы были доступны инструкции по переходу?

Если аргумент Линуса Торвальдса о «хорошем вкусе» для связанных списков основан на элегантности удаления единственного оператора перехода в специальном регистре, трудно рассматривать этот вид гимнастики со специальным регистром как победу или даже шаг в правильном направлении. Лично использовал gotos для асинхронного API в C, чтобы поговорить о переключении стека до того, как инструкции goto вызовут всевозможные запахи.

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

@neelance @darkuranium @d4tocchini не все участники Wasm считают, что отсутствие goto - это правильно, на самом деле, я бы лично оценил это как ошибку дизайна Wasm №1. Я абсолютно за его добавление (либо в виде функлетов, либо напрямую).

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

  1. Присоединяйтесь к Wasm CG.
  2. Кто-то инвестирует время, чтобы стать чемпионом предложения goto. Я рекомендую начинать с существующим funclets предложений, как это уже хорошо продумано @sunfishcode быть «ненавязчивым» для нынешних двигателей и инструментов , которые полагаются на блочной структуру, поэтому она имеет больше шансов на успех , чем сырой перейти к.
  3. Помогите ему пройти через 4 стадии предложения. Это включает в себя создание хороших дизайнов для любых возражений, инициирование обсуждений с целью осчастливить достаточное количество людей, чтобы вы получили большинство голосов при продвижении по этапам.

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

@aardappel Насколько мне известно, @sunfishcode пытался протолкнуть предложение функклетов и потерпел неудачу. Почему для меня было бы иначе?

@neelance Я не думаю, что у @sunfishcode было много времени, чтобы протолкнуть предложение дальше его первоначального создания, поэтому оно «застопорилось», а не «провалилось». Как я пытался указать, для того, чтобы предложение прошло весь путь по конвейеру, от лидера требуется непрерывная работа.

@неланс

Спасибо за пробник! Я могу подтвердить ту же проблему локально. Я подал https://bugs.chromium.org/p/v8/issues/detail?id=11237

Нам нужно будет реализовать relooper в Go [..] Один небольшой недостаток заключается в том, что он добавляет зависимость к binaryen для создания двоичных файлов wasm.

Кстати, если это поможет, мы можем сделать библиотечную сборку бинарника в виде одного файла C. Может проще интегрировать?

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

@таралкс

Я считаю, что алгоритм стека LLVM проще/лучше,

Обратите внимание, что эта ссылка относится не к восходящему LLVM, а к компилятору Cheerp (который является ответвлением LLVM). Их Stackifier имеет похожее имя на LLVM, но другое.

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

@kripken Спасибо за регистрацию проблемы.

Кстати, если это поможет, мы можем сделать библиотечную сборку бинарника в виде одного файла C. Может проще интегрировать?

Навряд ли. Сам компилятор Go был преобразован в чистый Go некоторое время назад, и, на самом деле, он не использует никаких других зависимостей C. Я не думаю, что это будет исключением.

Вот текущее состояние предложения по функлетам: Следующим шагом в процессе является призыв к голосованию CG для перехода к этапу 1.

Я сам в настоящее время сосредоточен на других областях WebAssembly, и у меня нет пропускной способности, чтобы продвигать функлеты; если кто-то заинтересован в том, чтобы взять на себя роль чемпиона для фанклетов, я был бы рад передать ее.

Навряд ли. Сам компилятор Go был преобразован в чистый Go некоторое время назад, и, на самом деле, он не использует никаких других зависимостей C. Я не думаю, что это будет исключением.

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

@Вюрих

Я думаю, что это может быть лучшим случаем для добавления gotos в wasm, но кому-то нужно будет собрать убедительные данные из реального кода, демонстрирующие такие серьезные обрывы производительности. Сам таких данных не видел. Работа по анализу дефицита производительности wasm, такая как «Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code» (2019) , также не подтверждает, что поток управления является важным фактором (они отмечают большее количество инструкций ветвления, но это не так). из-за структурированного потока управления - скорее из-за проверок безопасности).

@kripken Есть ли у вас какие-либо предложения о том, как можно собирать такие данные? Как показать, что дефицит производительности связан со структурированным потоком управления?

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

Я несколько удивлен, что у нас еще нет конструкции switch case, но функлеты включают ее.

@неланс

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

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

Другой вариант — скомпилировать wasm во что-то, что, по вашему мнению, может оптимально обрабатывать поток управления, то есть «отменить» структурирование. LLVM должен уметь это делать, поэтому запуск wasm на виртуальной машине, которая использует LLVM (например, WAVM или wasmer), или через WasmBoxC, может быть интересным. Возможно, вы могли бы отключить оптимизацию CFG в LLVM и посмотреть, насколько это важно.

@таралкс

Интересно, я что-то упустил о времени компиляции или использовании памяти? На самом деле структурированный поток управления должен быть лучше — например, из него очень просто перейти к форме SSA по сравнению с общей CFG. На самом деле это было одной из причин, по которой wasm изначально выбрал структурированный поток управления. Это также очень тщательно измеряется, потому что это влияет на время загрузки в Интернете.

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

Я имел в виду производительность компиляции в эмбеддере, но кажется, что это рассматривается как ошибка , а не обязательно проблема чисто производительности?

@таралкс

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

@kripken

На самом деле структурированный поток управления должен быть лучше — например, из него очень просто перейти к форме SSA по сравнению с общей CFG. На самом деле это было одной из причин, по которой wasm изначально выбрал структурированный поток управления. Это также очень тщательно измеряется, потому что это влияет на время загрузки в Интернете.

Очень конкретный вопрос, на всякий случай: знаете ли вы какой-либо компилятор Wasm, который действительно делает это - "очень простой" переход от "структурированного потока управления" к форме SSA. Потому что, на первый взгляд, поток управления Wasm не так (полностью/в конечном итоге) структурирован. Формально структурированный контроль — это тот, где нет break s, continue s, return s (грубо говоря, модель программирования Scheme, без магии типа call/cc). При их наличии такой поток управления грубо можно назвать «полуструктурированным».

Существует хорошо известный алгоритм SSA для полностью структурированного потока управления: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4503 . Вот что он говорит о полуструктурированном потоке управления:

Для структурированных операторов мы показали, как генерировать как форму SSA, так и дерево доминаторов за один проход во время синтаксического анализа. В следующем разделе мы покажем, что наш метод даже можно расширить до определенного класса неструктурированных операторов (LOOP/EXIT и RETURN), которые могут вызывать выходы из управляющих структур в произвольных точках. Однако, поскольку такие выходы являются своего рода (дисциплинированным) переходом, неудивительно, что с ними гораздо сложнее работать, чем со структурированными операторами.

OTOH, есть еще один известный алгоритм, https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf, который, возможно, тоже однопроходный, но не имеет проблем не только с неструктурированным управлением поток, но даже с неприводимым потоком управления (хотя и не дает для него оптимального результата).

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

И общая тема вопроса состоит в том, чтобы установить (хотя я бы сказал «сохранить») окончательную причину, по которой Wasm отказался от произвольной поддержки goto. Поскольку, наблюдая за этой веткой в ​​​​течение многих лет, я построил ментальную модель, которая заключается в том, что это делается для того, чтобы избежать столкновения с неустранимыми CFG. И действительно, пропасть лежит между приводимыми и неприводимыми CFG, при этом многие алгоритмы оптимизации (намного) проще для приводимых CFG, и это то, что закодировали многие оптимизаторы. (Полу)структурированный поток управления в Wasm — это просто дешевый способ гарантировать сводимость.

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

Спасибо.

@kripken Я сейчас немного сбит с толку и хочу учиться. Я смотрю на ситуацию и в настоящее время вижу следующее:


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

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

Если КФГ неприводима, то возможны два варианта:

  • Компилятор делает его приводимым, например, путем введения таблицы переходов. На этом шаге теряется информация. Трудно восстановить исходный CFG без анализа, специфичного для компилятора, сгенерировавшего двоичный файл. Из-за этой потери информации любой сгенерированный машинный код будет несколько медленнее, чем код, сгенерированный из исходной CFG. Мы можем сгенерировать этот машинный код с помощью однопроходного алгоритма, но это будет стоить потери информации. [1]

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

[1] Я знаю, что на самом деле это не потеря информации, если есть какой-то способ отменить операцию, но я не могу описать это лучше.


Где ошибка в моем мышлении?

@pfalcon

Знаете ли вы какой-либо компилятор Wasm, который на самом деле делает это - "очень простой" переход от "структурированного потока управления" к форме SSA.

Насчет ВМ: напрямую не знаю. Но IIRC в свое время @titzer и @lukewagner сказали, что это удобно реализовать таким образом - возможно, один из них может уточнить. Я не уверен, была ли здесь нередуцируемость всей проблемой или нет. И я не уверен, реализовали ли они упомянутые вами алгоритмы или нет.

О других вещах, помимо виртуальных машин: оптимизатор Binaryen определенно выигрывает от структурированного потока управления wasm, и не только от его возможности сокращения. Различные оптимизации проще, потому что мы всегда знаем, где находятся заголовки циклов, например, которые аннотированы в wasm. (Другие оптимизации OTOH выполнить сложнее, и для них у нас есть общий CFG IR...)

@неланс

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

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

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

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

@kripken

Спасибо за ваше объяснение.

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

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

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

@неланс

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

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

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

(Я согласен, что использование общих CFG дает и другие преимущества, помимо скорости, например, упрощение для компиляторов работы с wasm. Но мы можем решить эту проблему, не усложняя виртуальные машины wasm. Мы уже предоставляем поддержку произвольных CFG в LLVM и Binaryen. , позволяя компиляторам генерировать CFG и не беспокоиться о структурированном потоке управления. Если этого недостаточно, мы — инструменты, я имею в виду, включая меня — должны сделать больше.)

Funclet не столько о скорости, сколько о том, чтобы позволить языкам с нетривиальным потоком управления компилироваться в WebAssembly, C и Go являются наиболее очевидными, но это применимо к любому языку, который имеет async/await. Кроме того, выбор иерархического потока управления на самом деле приводит к большему количеству ошибок в виртуальных машинах, о чем свидетельствует тот факт, что все компиляторы Wasm, кроме V8, в любом случае разлагают иерархический поток управления на CFG. EBB в CFG могут представлять несколько конструкций потока управления в Wasm и других, и наличие одной конструкции для компиляции приводит к гораздо меньшему количеству ошибок, чем наличие множества разных типов с различным использованием.

Даже в Lightbeam, очень простом потоковом компиляторе, количество ошибок, связанных с неправильной компиляцией, значительно уменьшилось после добавления дополнительного шага трансляции, разбивающего поток управления на CFG. Это имеет двоякое значение для другой стороны этого процесса — Relooper гораздо более подвержен ошибкам, чем генерация функлетов, и мне говорили разработчики, работающие над бэкендами Wasm для LLVM и других компиляторов, которые должны были быть реализованы, они будут генерировать каждую функцию. использовать только функлеты, чтобы повысить надежность и простоту codegen. Все компиляторы, производящие Wasm, используют EBB, все компиляторы, использующие Wasm, кроме одного, используют EBB, этот отказ от реализации функлетов или какого-либо другого способа представления CFG просто добавляет шаг с потерями между ними, который вредит всем вовлеченным сторонам, кроме команды V8. .

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

@Вюрих

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

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

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

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

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

Функлеты нужны не столько для скорости, сколько для того, чтобы языки с нетривиальным потоком управления могли компилироваться в WebAssembly [..] Relooper гораздо более подвержен ошибкам, чем создание функлетов

Насчет инструмента согласен на 100%. Из большинства компиляторов труднее выдавать структурированный код! Но дело в том, что это упрощает работу на стороне ВМ, и wasm решил это сделать. Но опять же, я согласен, что у этого есть компромиссы, в том числе недостатки, о которых вы упомянули.

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

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

  • Я могу изучить портирование кода Binaryen CFG на Go, если это поможет компилятору Go — @neelance ?
  • Мы можем реализовать функлеты или что-то подобное исключительно на стороне инструментов. То есть мы предоставляем код библиотеки для этого сегодня, но могли бы также добавить двоичный формат. (Уже есть прецедент добавления к двоичному формату wasm на стороне инструментов, в объектных файлах wasm.)

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

Если над этим проделана какая-то конкретная работа, стоит отметить, что (AFAIU) самый простой идиоматический способ добавить это в Wasm (как намекнул @rossberg ) - это ввести инструкцию блока

многопетлевойв) _n_ т из (_instr_ * конец) _n_

который определяет n помеченных тел (с n предварительно объявленными аннотациями входных типов). Затем семейство инструкций br обобщается, так что все метки, определенные мультициклом, находятся внутри каждого тела по порядку (например, любое тело может быть разветвлено внутри любого другого тела). Когда многоцикловое тело разветвлено, выполнение переходит к _start_ тела (как в обычном цикле Wasm). Когда выполнение достигает конца тела без перехода к другому телу, возвращается вся конструкция (без провала).

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

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

РЕДАКТИРОВАТЬ: настройка этого, чтобы иметь сквозное поведение, незначительно усложнила бы формальную семантику, но, вероятно, была бы лучше для варианта использования подсказать базовому компилятору, что такое путь потока управления на трассировке.

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

Тем не менее, как и все нетривиальное, это компромисс, а не черное и белое. Я считаю, что здесь мы имеем случай, когда боль для производителей непропорциональна боли для двигателей. Большинство компиляторов, которые мы хотели бы добавить в Wasm, либо используют произвольные структуры CFG внутри (SSA), либо используются для работы с вещами, которые не возражают против goto (процессоров). Мы заставляем мир прыгать через обручи без особой выгоды.

Что-то вроде функлетов (или multiloop) хорошо, потому что оно модульное: если производителю это не нужно, все будет работать как раньше. Если движок действительно не может работать с произвольными CFG, то на данный момент они могут выдавать его, как если бы это была конструкция типа loop + br_table , и только те, кто ее использует, платят цену. . Затем «рынок решает», и мы видим, есть ли давление на движки, чтобы они выпускали для него лучший код. Что-то подсказывает мне, что если будет много кода Wasm, основанного на функлетах, для движков действительно не будет такой большой катастрофы, если они будут генерировать хороший код для них, как думают некоторые люди.

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

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

Насчет инструмента согласен на 100%. Из большинства компиляторов труднее выдавать структурированный код! Но дело в том, что это упрощает работу на стороне ВМ, и wasm решил это сделать. Но опять же, я согласен, что у этого есть компромиссы, в том числе недостатки, о которых вы упомянули.

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

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

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

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

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

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

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

@oridb

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

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

@gwvo

Тем не менее, как и все нетривиальное, это компромисс, а не черное и белое. [..] Мы заставляем мир прыгать через обручи без особой выгоды.

Полностью согласен, что это компромисс, и даже, возможно, wasm тогда ошибся. Но я считаю, что гораздо практичнее закрепить эти обручи на стороне инструментов.

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

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

@Вюрих

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

Очень интересно! Какая это была ВМ?

Мне также было бы особенно любопытно, был ли он однопроходным/потоковым или нет (если был, то как он обрабатывал инструменты обратной связи цикла?), и как он регистрирует распределение.

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

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

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

@kripken

Я могу изучить портирование кода Binaryen CFG на Go, если это поможет компилятору Go — @neelance ?

Для интеграции Asyncify? Пожалуйста, прокомментируйте предложение .

@комекс

Хорошие моменты!

Единственная дополнительная плата — отслеживание первого и последнего упоминания каждой переменной.

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

Потоковый подход V8 к распределению регистров должен работать так же хорошо, если блокам разрешено быть взаимно рекурсивными (как в https://github.com/WebAssembly/design/issues/796#issuecomment-742690194), поскольку единственные времена жизни, с которыми они имеют дело связаны внутри одного блока (стека) или считаются общефункциональными (локальными).

IIUC (со ссылкой на комментарий @titzer ) основная проблема для V8 заключается в типе CFG, которые может оптимизировать Turbofan.

@kripken

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

Это не «сложная оптимизация».. goto невероятно просты и естественны для многих систем. Бьюсь об заклад, есть много двигателей, которые могли бы добавить это бесплатно. Все, что я хочу сказать, это то, что если есть движки, которые по какой-либо причине хотят придерживаться структурированной модели CFG, они могут это сделать.

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

@kripken Это часть Wasmtime. Да, это потоковая передача, и предполагалось, что она будет O(N) сложности, но я перешел в новую компанию до того, как это было полностью реализовано, так что это всего лишь «O(N)-ish». https://github.com/bytecodealliance/wasmtime/tree/main/crates/lightbeam

Спасибо @Vurich , интересно. Было бы здорово увидеть показатели производительности, когда они будут доступны, особенно для запуска, а также для пропускной способности. Я предполагаю, что ваш подход будет компилироваться медленнее, чем подход, принятый инженерами V8 и SpiderMonkey, при этом выдавая более быстрый код. Так что это другой компромисс в этом пространстве. Кажется вероятным, что ваш подход не выигрывает от структурированного потока управления wasm, как вы сказали, в то время как их подход.

Нет, это потоковый компилятор, и он генерирует код быстрее, чем любой из этих двух движков (хотя есть вырожденные случаи, которые не были исправлены на момент моего ухода из проекта). Хотя я сделал все возможное, чтобы создать быстрый код, он в первую очередь предназначен для быстрого создания кода, а эффективность вывода имеет второстепенное значение. Стоимость запуска, насколько мне известно, равна нулю (выше внутренней стоимости Wasmtime, которая распределяется между бэкендами), потому что каждая структура данных запускается неинициализированной, а компиляция выполняется по инструкции. Хотя у меня нет под рукой цифр для сравнения с V8 или SpiderMonkey, у меня есть цифры для сравнения с Cranelift (основной двигатель wasmtime). На данный момент они устарели на несколько месяцев, но вы можете видеть, что он не только выдает код быстрее, чем Cranelift, но и выдает код быстрее, чем Cranelift. В то время он также выдавал более быстрый код, чем SpiderMonkey, хотя вам придется поверить мне на слово, поэтому я не буду винить вас, если вы мне не верите. Хотя у меня нет более свежих данных, я полагаю, что сейчас и Cranelift, и SpiderMonkey исправили небольшое количество ошибок, которые были основным источником их низкой производительности в этих микробенчмарках по сравнению с Lightbeam. но разница в скорости компиляции не изменилась за все время, пока я работал над проектом, потому что каждый компилятор по-прежнему принципиально одинаков, и именно соответствующая архитектура приводит к разным уровням производительности. Хотя я ценю ваше предположение, я не знаю, откуда взялось ваше предположение о том, что метод, который я изложил, будет медленнее.

Вот тесты, тесты ::compile для скорости компиляции, а тесты ::run для скорости выполнения вывода машинного кода. https://gist.github.com/Vurich/8696e67180aa3c93b4548fb1f298c29e

Методология здесь, вы можете клонировать ее и повторно запустить тесты, чтобы подтвердить результаты для себя, но PR, скорее всего, будет несовместим с последней версией wasmtime, поэтому он покажет вам сравнение производительности только на момент последнего обновления. пиар. https://github.com/bytecodealliance/wasmtime/pull/1660

При этом мой аргумент _не_ состоит в том, что CFG являются полезным внутренним представлением производительности в потоковом компиляторе. Мой аргумент заключается в том, что CFG не влияют отрицательно на производительность любого компилятора и, конечно, не до уровня, который оправдывал бы полное отстранение команд GCC и Go от создания WebAssembly. Почти никто в этой ветке, выступающий против функлетов или подобных расширений wasm, на самом деле не работал над проектами, на которые, как они утверждают, негативно повлияет это предложение. Не сказать, что вам вообще нужен личный опыт, чтобы комментировать эту тему, я думаю, что у каждого есть некоторый уровень ценного вклада, но это означает, что есть грань между другим мнением о цвете велопарковки и созданием утверждения, основанные не более чем на пустых домыслах.

@Вюрих

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

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

Поэтому я скептически отношусь к вы можете бить V8 и Spidermonkey раз базисные компиляции происходит потому , что, как и в тех связях я дал ранее, эти двух базовых компиляторы чрезвычайно настроены на время компиляции. В частности, они не генерируют никакого внутреннего IR, они просто переходят от wasm к машинному коду. Вы сказали, что ваш компилятор излучает внутренний IR (для CFG) - я ожидаю, что ваше время компиляции будет медленнее только из-за этого (из-за большего ветвления, пропускной способности памяти и т. д.).

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

Чтобы протестировать V8, вы можете запустить d8 --liftoff --no-wasm-tier-up , а для SpiderMonkey вы можете запустить sm --wasm-compiler=baseline .

(Спасибо за инструкции по сравнению с Cranelift, но Cranelift не является базовым компилятором, поэтому сравнение времени компиляции с ним не имеет значения в этом контексте. Хотя в остальном это очень интересно, я согласен.)

Моя интуиция подсказывает , что базовым компиляторам не придется существенно менять свою стратегию компиляции для поддержки funclets/ Надежная «структура потока управления, включая соединения и разбиения», на которую ссылается @kripken , удовлетворяется требованием, чтобы все типы ввода для набора взаимно рекурсивных блоков были предварительно объявлены (что в любом случае кажется естественным выбором для потоковой проверки) . Способность Lightbeam/Wasmtime превзойти базовые компиляторы движка не имеет значения; важным моментом является то, смогут ли базовые компиляторы движка оставаться такими же быстрыми, как сейчас.

FWIW, мне было бы интересно, чтобы эта функция была вынесена на обсуждение на будущем собрании компьютерной графики, и я в целом согласен с @Vurich в том, что представители движка могут возражать от себя, если они не готовы ее реализовать. При этом мы должны серьезно относиться к любому такому возражению (ранее на личных встречах я высказывал мнение, что при реализации этой функции мы должны попытаться избежать WebAssembly-версии саги JavaScript

@kripken

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

Вау, это действительно очень просто.

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

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

Фиксированный ABI выглядит следующим образом (на x86):

  • Если имеется ненулевое количество параметров (при входе в блок) или возвратов (при выходе из блока), то вершина стека wasm идет в rax , а остальная часть стека wasm соответствует x86 куча.
  • В противном случае весь стек wasm соответствует стеку x86.

Почему это важно?

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

Тогда алгоритм будет таким: выполнять инструкции линейно; перед прыжками и целями прыжков сбросить регистры в фиксированный ABI.

Единственное отличие состоит в том, что должен быть один фиксированный ABI, а не два. Он не мог отличить значение вершины стека, являющееся семантически «результатом» перехода, от того, чтобы просто остаться в стеке из внешнего блока. Таким образом, вершина стека должна быть безоговорочно помещена в rax .

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

(Проверка также будет другой, но по-прежнему однопроходной.)

Хорошо, предварительные оговорки:

  1. Это не альтернативная вселенная; мы застряли с созданием обратно совместимых расширений для существующей WebAssembly.
  2. Базовый компилятор SpiderMonkey — это всего лишь одна из реализаций, и вполне возможно, что он неоптимален в отношении распределения регистров: если бы он был немного умнее, выгода во время выполнения перевешивала бы затраты во время компиляции.
  3. Даже если базовым компиляторам не нужна дополнительная информация, оптимизирующим компиляторам она может понадобиться для быстрого построения SSA.

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

@конрад-ватт @comex

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

И @comex - да, как вы сказали, это обсуждение не связано с оптимизацией компиляторов, где SSA может извлечь выгоду из структуры. Может быть , стоит процитировать немного от одного из звеньев из ,

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

@ conrad-watt Я определенно согласен, что нам просто нужно получить прямую обратную связь от людей из VM и сохранять непредвзятость. Чтобы было ясно, моя цель здесь не в том, чтобы что-то остановить. Я прокомментировал здесь подробно, потому что некоторые комментарии, казалось, думали, что структурированный поток управления wasm был очевидной ошибкой или той, которая, очевидно, должна быть исправлена ​​с помощью funclets/multiloop - я просто хотел представить здесь историю мышления и что были веские причины для текущей модели, поэтому улучшить ее может быть нелегко.

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

На стороне «для» полезно заранее знать, какие блоки имеют задние ребра. Потоковый компилятор может отслеживать свойства, которые не видны в системе типов WebAssembly (например, индекс в локальном i находится в пределах границ массива в локальном arr ). При переходе вперед может быть полезно аннотировать цель, какие свойства сохраняются в этой точке. Таким образом, когда достигается метка, ее блок может быть скомпилирован с использованием свойств, которые сохраняются для всех внутренних ребер, скажем, для исключения проверок границ массива. Но если у метки потенциально может быть неизвестный бэкграунд, то его блок не может быть скомпилирован с этим знанием. Конечно, непотоковой компилятор может выполнять более важный циклический инвариантный анализ, но для потокового компилятора полезно не беспокоиться о том, что может произойти впереди. (Дополнительная мысль: @Vurich упоминает, что WebAssembly не является стековой машиной из-за использования локальные переменные и добавлять больше операций со стеком. Упрощение распределения регистров, по-видимому, является еще одной причиной это направление.)

Что касается стороны «против», то пока речь шла только о местном контроле. Это нормально для C, но как насчет C++ или других языков с похожими исключениями? Как насчет языков с другими формами нелокального управления? Вещи с динамической областью действия часто структурированы по своей сути (или, по крайней мере, я не знаю примеров взаимно рекурсивных динамических областей). Я думаю, что эти соображения можно решить, но вам нужно что-то спроектировать с их учетом, чтобы результат можно было использовать в этих настройках. Это то, над чем я размышлял, и я рад поделиться своими незавершенными мыслями (выглядящими примерно как расширение многоконтурности @conrad-watt) со всеми, кто заинтересован (хотя здесь кажется, что это не по теме), но Я хотел, по крайней мере, предупредить, что нужно помнить не только о локальном потоке управления.

(Я также хотел бы добавить еще один +1, чтобы услышать больше от людей VM, хотя я думаю, что

Когда я говорю, что Lightbeam производит внутренний ИК, это действительно вводит в заблуждение, и я должен был уточнить. Я работал над проектом какое-то время, и иногда вы можете получить туннельное зрение. По сути, Lightbeam потребляет входную инструкцию за инструкцией (на самом деле у него есть максимум одна инструкция просмотра вперед, но это не особенно важно), и для каждой инструкции он лениво и в постоянном пространстве создает ряд внутренних IR-инструкций. Максимальное количество инструкций на инструкцию Wasm постоянное и небольшое, что-то около 6. Это не создание буфера IR-инструкций для всей функции и работа над этим. Затем он считывает эти ИК-инструкции одну за другой. На самом деле вы можете просто думать об этом как о библиотеке более общих вспомогательных функций, с точки зрения которых он реализует каждую инструкцию Wasm, я просто называю его IR, потому что это помогает объяснить, как у него есть другая модель для потока управления и т. д. Вероятно, он не производит код так же быстро, как базовые компиляторы V8 или SpiderMonkey, но это потому, что он не полностью оптимизирован, а не потому, что он архитектурно несовершенен. Я хочу сказать, что я внутренне моделирую иерархический поток управления Wasm, как если бы это была CFG, а не фактически создаю буфер IR в памяти, как это делают LLVM или Cranelift.

Другой вариант — скомпилировать wasm во что-то, что, по вашему мнению, может оптимально обрабатывать поток управления, то есть «отменить» структурирование. LLVM должен уметь это делать, поэтому запуск wasm на виртуальной машине, которая использует LLVM (например, WAVM или wasmer), или через WasmBoxC, может быть интересным.

@kripken К сожалению, LLVM, похоже, пока не может отменить структурирование. Проход оптимизации многопоточности перехода должен уметь это делать, но пока не распознает этот шаблон. Вот пример, показывающий некоторый код C++, который имитирует то, как алгоритм relooper преобразует CFG в цикл+переключатель. GCC удается «отменить цикл», а clang — нет: https://godbolt.org/z/GGM9rP

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

@комекс

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

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

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

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

@РоссТейт

Что касается funclets/gotos, на днях я просмотрел спецификацию funclet, и, на первый взгляд, не похоже, что у однопроходного компилятора должны быть какие-то серьезные проблемы с ним, уж точно не с упрощенной схемой regalloc. Но даже с лучшей схемой все может быть в порядке: первое ребро, достигшее точки соединения, должно решить, каково назначение регистра, и другие ребра должны будут соответствовать.

@conrad-watt, как вы только что упомянули на собрании компьютерной графики, я думаю, нам было бы очень интересно узнать подробности того, как будет выглядеть ваш многоконтурный цикл.

@aardappel да, жизнь пришла ко мне быстро, но я должен сделать это на следующей встрече. Просто чтобы подчеркнуть, что эта идея не моя, поскольку @rossberg изначально набросал ее в ответ на первый набросок функлетов.

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

У нас было несколько дискуссий по этому поводу в CG, и я написал резюме и последующий документ. Из-за длины я сделал это отдельным смыслом.

https://gist.github.com/conrad-watt/6a620cb8b7d8f0191296e3eb24dffdef

Я думаю, что двумя немедленными практическими вопросами (более подробную информацию см. В разделе «Последующие действия») являются:

  • Можем ли мы найти «дикие» программы, которые в настоящее время страдают и выиграют с точки зрения производительности от multiloop ? Это могут быть программы, для которых преобразования LLVM вводят непреодолимый поток управления, даже если он не существует в исходной программе.
  • Есть ли мир, в котором multiloop реализуется сначала на стороне производителя, с некоторым уровнем развертывания ссылок/переводов для «Web» Wasm?

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

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

Я очень рад видеть прогресс в этом вопросе. Огромное "Спасибо" всем причастным!

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

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

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

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

  • Согласитесь, что это обсуждение должно быть перенесено на вопросы по функлетам (или новому) репо.
  • Согласитесь, что найти программу, которая выиграет от этого, трудно количественно, если LLVM (и Go, и другие) фактически не выдает наиболее оптимальный поток управления (который может быть непреодолимым). Неэффективность, вызванная FixIrreducibleControlFlow и друзьями, может быть проблемой «смерти от тысячи порезов» для большого двоичного файла.
  • Хотя я приветствовал бы реализацию только инструментов как абсолютный минимум прогресса, вытекающий из этого обсуждения, это все же не было бы оптимальным, поскольку производители теперь сталкиваются с трудным выбором использования этой функциональности для удобства (но затем сталкиваются с непредсказуемым спадом производительности). обрывы), или проделать тяжелую работу, чтобы привести их вывод к стандартному wasm, чтобы все было предсказуемо.
  • Если бы было решено, что "gotos" - это в лучшем случае функция только для инструментов, я бы сказал, что вы, вероятно, могли бы обойтись даже более простой функцией, чем multiloop, поскольку все, что вас волнует, - это удобство производителя. Как минимум, goto <function_byte_offset> будет единственной вещью, которую нужно будет вставить в обычные тела функций Wasm, чтобы позволить WABT или Binaryen преобразовать их в легальный Wasm. Такие вещи, как сигнатуры типов, полезны, если движку нужно быстро проверить многоконтурность, но если это удобный инструмент, он также может сделать его максимально удобным для генерации.

Согласитесь, что найти программу, которая выиграет от этого, трудно количественно, если LLVM (и Go, и другие) фактически не выдает наиболее оптимальный поток управления (который может быть непреодолимым).

Я согласен, что тестирование на модифицированных тулчейнах + ВМ было бы оптимальным. Но мы можем сравнить текущие сборки wasm с нативными сборками, которые имеют оптимальный поток управления. Not So Fast и другие рассматривали это по-разному (счетчики производительности, прямое исследование) и не обнаружили, что непреодолимый поток управления является существенным фактором.

В частности, они не сочли это важным фактором для C/C++. Возможно, это больше связано с C/C++, чем с производительностью непревзойденного потока управления. (Честно говоря, я не знаю.) Похоже, у @neelance есть основания полагать, что то же самое не будет верно для Go.

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

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

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

Несколько лет назад я пытался скомпилировать определенный эмулятор байт-кода для динамического языка (https://github.com/ciao-lang/ciao) в веб-сборку, и производительность была далека от оптимальной (иногда в 10 раз медленнее, чем нативная версия). Основной цикл выполнения содержал большой переключатель диспетчеризации байт-кода, а движок десятилетиями точно настраивался для работы на реальном оборудовании, и мы активно используем метки и переходы. Интересно, выиграет ли такое программное обеспечение от поддержки непреодолимого потока управления или проблема заключается в другом. У меня не было времени на дальнейшее расследование, но я буду рад попробовать еще раз, если станет известно, что ситуация улучшилась. Конечно, я понимаю, что компиляция других языков VM в wasm не является основным вариантом использования, но мне было бы полезно узнать, будет ли это в конечном итоге осуществимо, особенно потому, что универсальные двоичные файлы, которые эффективно работают везде, являются одним из обещанных преимуществ васм. (Спасибо и извините, если эта конкретная тема обсуждалась в каком-то другом выпуске)

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

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

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

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

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

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

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

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