Design: Por favor, admita etiquetas y gotos arbitrarios.

Creado en 8 sept. 2016  ·  159Comentarios  ·  Fuente: WebAssembly/design

Me gustaría señalar que no he estado involucrado en el esfuerzo de ensamblaje web,
y no mantengo ningún compilador grande o ampliamente utilizado (solo mi propio
lenguaje de juguete, contribuciones menores al backend del compilador QBE y un
pasantía en el equipo de compiladores de IBM), pero terminé poniéndome un poco irritable y
se animó a compartir más ampliamente.

Por lo tanto, aunque me siento un poco incómodo al intervenir y sugerir cambios importantes
a un proyecto en el que no he estado trabajando... aquí va:

Mis quejas:

Cuando estoy escribiendo un compilador, lo primero que haría con el nivel alto
estructura (bucles, sentencias if, etc.) es validarlos para la semántica,
hacer verificación de tipo y así sucesivamente. Lo segundo que hago con ellos es tirarlos
y aplanar a bloques básicos, y posiblemente a la forma SSA. En algunas otras partes
del mundo de los compiladores, un formato popular es el estilo de paso de continuación. no soy
un experto en compilar con estilo de paso de continuación, pero tampoco parece
ser una buena opción para los bucles y bloques de alcance que parece tener el ensamblado web
abrazado

Me gustaría argumentar que un formato basado en goto más plano sería mucho más útil como
un objetivo para los desarrolladores de compiladores, y no obstaculizaría significativamente la
escritura de un polyfill utilizable.

Personalmente, tampoco soy un gran fanático de las expresiones complejas anidadas. son un poco
más torpe de consumir, especialmente si los nodos internos pueden tener efectos secundarios, pero yo
no se oponga fuertemente a ellos como un implementador del compilador: el ensamblado web
JIT puede consumirlos, puedo ignorarlos y generar las instrucciones que mapean
a mi IR. No me dan ganas de voltear mesas.

El mayor problema se reduce a bucles, bloques y otros elementos sintácticos.
que, como escritor de compiladores optimizadores, se esfuerza mucho por representar como un
gráfico con ramas que representan bordes; Las construcciones de flujo de control explícito
son un estorbo. Reconstruyéndolos a partir del gráfico una vez que hayas terminado
las optimizaciones que desea es sin duda posible, pero es un poco de
complejidad para trabajar en torno a un formato más complejo. Y eso me molesta: Tanto el
el productor y el consumidor están trabajando en torno a problemas completamente inventados
que se evitaría simplemente descartando construcciones complejas de flujo de control
del ensamblaje web.

Además, la insistencia en construcciones de nivel superior conduce a algunos
casos patológicos. Por ejemplo, el dispositivo de Duff termina con una web horrible
salida de ensamblaje, como se ve jugando en The Wasm Explorer .
Sin embargo, lo contrario no es cierto: todo lo que se puede expresar
en ensamblador web se puede convertir trivialmente a un equivalente en algunos
formato no estructurado, basado en goto.

Entonces, como mínimo, me gustaría sugerir que el equipo de ensamblaje web agregue
soporte para etiquetas y gotos arbitrarios. Si eligen mantener el más alto
construcciones de nivel, sería un poco de complejidad derrochadora, pero al menos
los escritores de compiladores como yo podrían ignorarlos y generar resultados
directamente.

Polirelleno:

Una de las preocupaciones que he escuchado al hablar de esto es que el bucle
y la estructura basada en bloques permite un polirrelleno más fácil del ensamblaje web.
Si bien esto no es del todo falso, creo que una solución simple de polyfill
para etiquetas y gotos es posible. Aunque podría no ser tan óptimo,
Creo que vale la pena un poco de fealdad en el código de bytes para
para evitar comenzar una nueva herramienta con una deuda técnica incorporada.

Si asumimos una sintaxis similar a LLVM (o QBE) para el ensamblaje web, entonces algo de código
eso parece como:

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

podría compilar a:

 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
 }

Esto podría ser polillenado a Javascript que se ve así:

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;
    }
}

¿Es feo? Sí. ¿Importa? Con suerte, si el ensamblaje web despega,
no por mucho tiempo.

Y si no:

Bueno, si alguna vez tuviera que apuntar al ensamblado web, creo que generaría código
usando el enfoque que mencioné en el polyfill, y hago todo lo posible para ignorar todos
las construcciones de alto nivel, con la esperanza de que los compiladores sean lo suficientemente inteligentes como para
captar este patrón.

Pero sería bueno si no tuviéramos que tener ambos lados de la generación de código
evitar el formato especificado.

control flow

Comentario más útil

El próximo lanzamiento de Go 1.11 tendrá soporte experimental para WebAssembly. Esto incluirá soporte completo para todas las características de Go, incluidas rutinas, canales, etc. Sin embargo, el rendimiento del WebAssembly generado actualmente no es tan bueno.

Esto se debe principalmente a la falta de la instrucción goto. Sin la instrucción goto, teníamos que recurrir al uso de un bucle de nivel superior y una tabla de saltos en cada función. Usar el algoritmo relooper no es una opción para nosotros, porque al cambiar entre goroutines necesitamos poder reanudar la ejecución en diferentes puntos de una función. El relooper no puede ayudar con esto, solo una instrucción goto puede hacerlo.

Es asombroso que WebAssembly haya llegado al punto en que puede admitir un lenguaje como Go. Pero para ser verdaderamente el ensamblador de la web, WebAssembly debería ser tan poderoso como otros lenguajes ensambladores. Go tiene un compilador avanzado que puede emitir un ensamblaje muy eficiente para otras plataformas. Es por eso que me gustaría argumentar que es principalmente una limitación de WebAssembly y no del compilador Go que no es posible usar también este compilador para emitir un ensamblado eficiente para la web.

Todos 159 comentarios

@oridb Wasm está algo optimizado para que el consumidor pueda convertir rápidamente al formulario SSA, y la estructura ayuda aquí para los patrones de código comunes, por lo que la estructura no es necesariamente una carga para el consumidor. No estoy de acuerdo con su afirmación de que 'ambos lados de la generación de código funcionan en torno al formato especificado'. Wasm tiene mucho que ver con un consumidor delgado y rápido, y si tiene algunas propuestas para hacerlo más delgado y rápido, eso podría ser constructivo.

Los bloques que se pueden ordenar en un DAG se pueden expresar en bloques y ramas wasm, como en su ejemplo. El bucle de cambio es el estilo que se usa cuando es necesario, y tal vez los consumidores puedan hacer algunos saltos de subprocesamiento para ayudar aquí. Tal vez eche un vistazo a binaryen, que podría hacer gran parte del trabajo para el backend de su compilador.

Ha habido otras solicitudes de soporte CFG más general y algunos otros enfoques que usan bucles mencionados, pero tal vez el enfoque esté en otra parte en este momento.

No creo que haya ningún plan para admitir el 'estilo de paso de continuación' explícitamente en la codificación, pero se han mencionado bloques y bucles que aparecen argumentos (como un lambda) y admiten múltiples valores (múltiples argumentos lambda) y agregar un Operador pick para que sea más fácil hacer referencia a las definiciones (los argumentos lambda).

la estructura ayuda aquí para patrones de código comunes

No veo ningún patrón de código común que sea más fácil de representar en términos de ramas a etiquetas arbitrarias, en comparación con el subconjunto de bloques y bucles restringidos que impone el ensamblaje web. Podría ver un beneficio menor si hubiera un intento de hacer que el código se pareciera mucho al código de entrada para ciertas clases de lenguaje, pero ese no parece ser un objetivo, y las construcciones están un poco vacías si estuvieran ahí para

Los bloques que se pueden ordenar en un DAG se pueden expresar en bloques y ramas wasm, como en su ejemplo.

Sí, pueden ser. Sin embargo, preferiría no agregar trabajo adicional para determinar cuáles se pueden representar de esta manera, en comparación con cuáles necesitan trabajo adicional. Siendo realistas, me saltearía el análisis adicional y siempre generaría el formulario de ciclo de cambio.

Una vez más, mi argumento no es que los bucles y los bloques hagan que las cosas sean imposibles; Es que todo lo que pueden hacer es más simple y fácil de escribir para una máquina con goto, goto_if y etiquetas arbitrarias sin estructura.

Tal vez eche un vistazo a binaryen, que podría hacer gran parte del trabajo para el backend de su compilador.

Ya tengo un backend útil con el que estoy bastante contento y planeo arrancar completamente todo el compilador en mi propio idioma. Preferiría no agregar una dependencia adicional bastante grande simplemente para evitar el uso forzado de bucles/bloques. Si simplemente uso bucles de cambio, emitir el código es bastante trivial. Si trato de usar las funciones presentes en el ensamblaje web de manera efectiva, en lugar de hacer todo lo posible para fingir que no existen, se vuelve mucho más desagradable.

Ha habido otras solicitudes de soporte CFG más general y algunos otros enfoques que usan bucles mencionados, pero tal vez la fuerza esté en otra parte en este momento.

Todavía no estoy convencido de que los bucles tengan ningún beneficio: cualquier cosa que se pueda representar con un bucle se puede representar con un goto y una etiqueta, y hay conversiones rápidas y bien conocidas a SSA desde listas de instrucciones planas.

En lo que respecta a CPS, no creo que sea necesario un soporte explícito: es popular en los círculos de FP porque es bastante fácil de convertir directamente a ensamblador y brinda beneficios similares a SSA en términos de razonamiento (http:// mlton.org/pipermail/mlton/2003-January/023054.html); Nuevamente, no soy un experto en eso, pero por lo que recuerdo, la continuación de la invocación se reduce a una etiqueta, algunos movimientos y un goto.

@oridb 'hay conversiones rápidas y bien conocidas a SSA desde listas de instrucciones planas'

Sería interesante saber cómo se comparan con los decodificadores wasm SSA, esa es la pregunta importante.

Wasm hace uso de una pila de valores en la actualidad, y algunos de los beneficios de eso se perderían sin la estructura, lo que afectaría el rendimiento del decodificador. Sin la pila de valores, la decodificación SSA también tendría más trabajo, probé un código base de registro y la decodificación fue más lenta (no estoy seguro de cuán significativo es eso).

¿Mantendría la pila de valores o usaría un diseño basado en registros? Si se mantiene la pila de valores, entonces tal vez se convierta en un clon de CIL, y tal vez el rendimiento de wasm podría compararse con CIL, ¿alguien ha verificado esto?

¿Mantendría la pila de valores o usaría un diseño basado en registros?

En realidad no tengo ningún sentimiento fuerte en ese sentido. Me imagino que la compacidad de la codificación sería una de las mayores preocupaciones; Es posible que un diseño de registro no funcione tan bien allí, o puede comprimirse fantásticamente con gzip. En realidad no lo sé de memoria.

El rendimiento es otra preocupación, aunque sospecho que podría ser menos importante dada la capacidad de almacenar en caché la salida binaria, además del hecho de que el tiempo de descarga puede superar la decodificación en órdenes de magnitud.

Sería interesante saber cómo se comparan con los decodificadores wasm SSA, esa es la pregunta importante.

Si está decodificando a SSA, eso implica que también estaría haciendo una cantidad razonable de optimización. En primer lugar, tendría curiosidad por comparar qué tan significativo es el rendimiento de decodificación. Pero, sí, definitivamente es una buena pregunta.

Gracias por sus preguntas e inquietudes.

Vale la pena señalar que muchos de los diseñadores e implementadores de
WebAssembly tiene experiencia en JIT industriales de alto rendimiento, no solo
para JavaScript (V8, SpiderMonkey, Chakra y JavaScriptCore), pero también en
LLVM y otros compiladores. Yo personalmente he implementado dos JIT para Java
bytecode y puedo dar fe de que una máquina de pila con gotos sin restricciones
introduce cierta complejidad en la decodificación, verificación y construcción de un
compilador IR. De hecho, hay muchos patrones que se pueden expresar en Java.
código de bytes que generará JIT de alto rendimiento, incluidos C1 y C2 en
HotSpot para simplemente darse por vencido y relegar el código para que solo se ejecute en el
Interprete. Por el contrario, construir un compilador IR a partir de algo como un
AST de JavaScript u otro idioma es algo que también he hecho. El
La estructura adicional de un AST hace que parte de este trabajo sea mucho más simple.

El diseño de las construcciones de flujo de control de WebAssembly simplifica a los consumidores al
Permitiendo una verificación rápida y simple, conversión fácil de un solo paso al formulario SSA
(incluso un gráfico IR), JIT efectivos de un solo paso, y (con posorden y el
máquina de apilamiento) interpretación relativamente simple en el lugar. Estructurado
el control hace que los gráficos de flujo de control irreducibles sean imposibles, lo que elimina
toda una clase de desagradables casos de esquina para decodificadores y compiladores. También
establece muy bien el escenario para el manejo de excepciones en el código de bytes WASM, para el cual V8
ya está desarrollando un prototipo en concierto con la producción
implementación.

Hemos tenido muchas discusiones internas entre los miembros sobre este mismo
tema, ya que, para un código de bytes, es una cosa que es más diferente de
otros objetivos a nivel de máquina. Sin embargo, no es diferente a la orientación
un lenguaje fuente como JavaScript (que muchos compiladores hacen en estos días) y
requiere solo una reorganización menor de los bloques para lograr la estructura. Ahí
se conocen algoritmos para hacer esto, y herramientas. Nos gustaría proporcionar algunos
una mejor orientación para aquellos productores que comienzan con un CFG arbitrario para
comunicar esto mejor. Para idiomas dirigidos a WASM directamente desde un AST
(que en realidad es algo que V8 hace ahora para el código asm.js, directamente
traducir un código de bytes JavaScript AST a WASM), no hay reestructuración
paso necesario. Esperamos que este sea el caso para muchas herramientas de lenguaje.
en todo el espectro que no tienen IR sofisticados en su interior.

El jueves 8 de septiembre de 2016 a las 9:53 a.m., Ori Bernstein [email protected]
escribió:

¿Mantendría la pila de valores o usaría un diseño basado en registros?

En realidad no tengo ningún sentimiento fuerte en ese sentido. me imagino
la compacidad de la codificación sería una de las mayores preocupaciones; Como tu
mencionado, el rendimiento es otro.

Sería interesante saber cómo se comparan con los decodificadores wasm SSA, que
es la pregunta importante?

Si está decodificando a SSA, eso implica que también estaría haciendo un
cantidad razonable de optimización. Me gustaría comparar cómo
El rendimiento de decodificación significativo es en primer lugar. Pero, sí, eso es
definitivamente una buena pregunta.


Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-245521009 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ALnq1Iz1nn4--NL32R9ev0JPKfEnDyvqks5qn77cgaJpZM4J3ofA
.

Gracias @titzer , estaba desarrollando la sospecha de que la estructura de Wasm tenía un propósito más allá de la simple similitud con asm.js. Sin embargo, me pregunto: el código de bytes de Java (y CIL) no modelan los CFG o la pila de valores directamente, deben ser inferidos por el JIT. Pero en Wasm (especialmente si se agregan firmas de bloques), el JIT puede averiguar fácilmente qué sucede con la pila de valores y el flujo de control, por lo que me pregunto si los CFG (o el flujo de control irreducible específicamente) se modelaron explícitamente como lo son los bucles y los bloques. ¿Eso podría evitar la mayoría de los desagradables casos en los que estás pensando?

Existe esta optimización ordenada que usan los intérpretes que se basa en un flujo de control irreducible para mejorar la predicción de bifurcaciones...

@oridb

Me gustaría argumentar que un formato basado en goto más plano sería mucho más útil como
un objetivo para los desarrolladores de compiladores

Estoy de acuerdo en que los gotos son muy útiles para muchos compiladores. Es por eso que herramientas como Binaryen le permiten generar CFG arbitrarios con gotos , y pueden convertir eso de manera muy rápida y eficiente en WebAssembly para usted.

Podría ser útil pensar en WebAssembly como algo optimizado para que lo consuman los navegadores (como señaló @titzer ). La mayoría de los compiladores probablemente no deberían generar WebAssembly directamente, sino usar una herramienta como Binaryen, para que puedan emitir gotos, obtener un montón de optimizaciones de forma gratuita y no necesitan pensar en detalles de formato binario de bajo nivel de WebAssembly (en lugar emite un IR usando una API simple).

Con respecto al polyfilling con el patrón while-switch que mencionas: en emscripten comenzamos de esa manera antes de desarrollar el método "relooper" para recrear bucles. El patrón de cambio while es alrededor de 4 veces más lento en promedio (pero en algunos casos significativamente menos o más, por ejemplo, los bucles pequeños son más sensibles). Estoy de acuerdo con usted en que, en teoría, las optimizaciones de subprocesamiento podrían acelerar eso, pero el rendimiento será menos predecible ya que algunas máquinas virtuales lo harán mejor que otras. También es significativamente más grande en términos de tamaño de código.

Podría ser útil pensar en WebAssembly como algo optimizado para que lo consuman los navegadores (como señaló @titzer ). La mayoría de los compiladores probablemente no deberían generar WebAssembly directamente, sino usar una herramienta como Binaryen...

Todavía no estoy convencido de que este aspecto vaya a importar tanto; nuevamente, sospecho que el costo de obtener el código de bytes dominaría la demora que ve el usuario, siendo el segundo mayor costo las optimizaciones realizadas, y no el análisis y la validación. . También asumo/espero que el código de bytes se elimine y la salida compilada sea lo que se almacenará en caché, lo que hace que la compilación sea efectivamente un costo único.

Pero si estaba optimizando para el consumo del navegador web, ¿por qué no simplemente definir el ensamblaje web como SSA, lo que me parece más acorde con lo que esperaría y menos esfuerzo para 'convertir' a SSA?

Puede comenzar a analizar y compilar durante la descarga, y es posible que algunas máquinas virtuales no realicen una compilación completa por adelantado (es posible que solo usen una línea de base simple, por ejemplo). Por lo tanto, los tiempos de descarga y compilación pueden ser menores de lo esperado y, como resultado, el análisis y la validación pueden convertirse en un factor significativo en el retraso total que ve el usuario.

En cuanto a las representaciones SSA, tienden a tener códigos de gran tamaño. SSA es excelente para optimizar el código, pero no para serializar el código de forma compacta.

@oridb Vea el comentario de @titzer 'El diseño de las construcciones de flujo de control de WebAssembly simplifica a los consumidores al permitir una verificación rápida y simple, una conversión fácil de

Gran parte de la eficiencia de codificación de wasm parece provenir de la optimización para el patrón de código común en el que las definiciones tienen un solo uso que se utiliza en el orden de la pila. Espero que una codificación SSA también pueda hacerlo, por lo que podría tener una eficiencia de codificación similar. Los operadores como if_else para patrones de diamantes también ayudan mucho. Pero sin la estructura wasm, parece que todos los bloques básicos necesitarían leer definiciones de registros y escribir resultados en registros, y eso podría no ser tan eficiente. Por ejemplo, creo que wasm puede hacerlo incluso mejor con un operador pick que podría hacer referencia a valores de pila con ámbito en la pila y a través de los límites de bloque básicos.

Creo que wasm no está muy lejos de poder codificar la mayoría de los códigos al estilo SSA. Si las definiciones se pasaran al árbol de alcance como salidas de bloques básicos, entonces podría estar completo. ¿Podría la codificación SSA ser ortogonal al asunto CFG? Por ejemplo, podría haber una codificación SSA con las restricciones de CFG de wasm, podría haber una VM basada en registros con las restricciones de CFG.

Un objetivo para wasm es trasladar la carga de optimización fuera del consumidor de tiempo de ejecución. Existe una fuerte resistencia a agregar complejidad en el compilador de tiempo de ejecución, ya que aumenta la superficie de ataque. Gran parte del desafío del diseño es preguntar qué se puede hacer para simplificar el compilador en tiempo de ejecución sin dañar el rendimiento, ¡y mucho debate!

Bueno, probablemente ya sea demasiado tarde, pero me gustaría cuestionar la idea de que el algoritmo relooper, o sus variantes, pueden producir resultados "suficientemente buenos" en todos los casos. Claramente pueden hacerlo en la mayoría de los casos, ya que la mayoría del código fuente no contiene un flujo de control irreducible para empezar, las optimizaciones no suelen complicar demasiado las cosas y, si lo hacen, por ejemplo, como parte de la fusión de bloques duplicados, probablemente se les pueda enseñar No a. Pero ¿qué pasa con los casos patológicos? Por ejemplo, qué sucede si tiene una corrutina que un compilador ha transformado en una función regular con una estructura como esta pseudo-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
}

Por lo tanto, tiene un flujo de control mayormente normal, pero con algunos gotos apuntando en el medio. Así es como funcionan las corrutinas LLVM .

No creo que haya una buena manera de volver a hacer un bucle de algo así, si el flujo de control 'normal' es lo suficientemente complejo. (Podría estar equivocado). O duplica partes masivas de la función, lo que podría necesitar una copia separada para cada punto de rendimiento, o convierte todo en un interruptor gigante, que según @kripken es 4 veces más lento que relooper en código típico ( que en sí mismo es probablemente algo más lento que no necesitar relooper en absoluto).

La VM podría reducir la sobrecarga de un conmutador gigante con optimizaciones de subprocesos de salto, pero seguramente es más costoso para la VM realizar esas optimizaciones, esencialmente adivinando cómo se reduce el código a gotos, que simplemente aceptar gotos explícitos. Como dice @kripken , también es menos predecible.

Tal vez hacer ese tipo de transformación es una mala idea para comenzar, ya que después nada domina nada, por lo que las optimizaciones basadas en SSA no pueden hacer mucho... tal vez sea mejor hacerlo a nivel de ensamblaje, tal vez wasm debería eventualmente obtener soporte de corutina nativo en su lugar. Pero el compilador puede realizar la mayoría de las optimizaciones antes de realizar la transformación, y parece que al menos los diseñadores de corrutinas LLVM no vieron una necesidad urgente de retrasar la transformación hasta la generación del código. Por otro lado, dado que hay una gran variedad en la semántica exacta que la gente quiere de las corrutinas (por ejemplo, la duplicación de corrutinas suspendidas, la capacidad de inspeccionar 'marcos apilados' para GC), cuando se trata de diseñar un código de bytes portátil (en lugar de un compilador), es más flexible admitir correctamente el código ya transformado que hacer que la máquina virtual realice la transformación.

De todos modos, las rutinas son solo un ejemplo. Otro ejemplo que se me ocurre es implementar una máquina virtual dentro de una máquina virtual. Si bien una característica más común de los JIT son las salidas laterales, que no requieren goto, hay situaciones que requieren entradas laterales, nuevamente, que requieren goto en medio de bucles y demás. Otro sería los intérpretes optimizados: no es que los intérpretes que se dirigen a wasm realmente puedan coincidir con los que se dirigen al código nativo, lo que, como mínimo, puede mejorar el rendimiento con gotos calculados y puede sumergirse en ensamblador para obtener más... pero parte de la motivación para gotos calculados es aprovechar mejor el predictor de bifurcación dando a cada caso su propia instrucción de salto, por lo que es posible que pueda replicar parte del efecto al tener un interruptor separado después de cada controlador de código de operación, donde todos los casos serían solo gotos. O al menos tenga un si o dos para verificar las instrucciones específicas que comúnmente vienen después de la actual. Hay algunos casos especiales de ese patrón que podrían representarse con un flujo de control estructurado, pero no el caso general. Y así…

Seguramente hay alguna forma de permitir un flujo de control arbitrario sin que la VM haga mucho trabajo. La idea del hombre de paja, podría estar rota: podría tener un esquema en el que se permitan los saltos a los ámbitos secundarios, pero solo si la cantidad de ámbitos que debe ingresar es inferior al límite definido por el bloque de destino. El límite predeterminado sería 0 (sin saltos desde los ámbitos principales), lo que conserva la semántica actual, y el límite de un bloque no puede ser mayor que el límite del bloque principal + 1 (fácil de verificar). Y la máquina virtual cambiaría su heurística de dominio de "X domina a Y si es un padre de Y" a "X domina a Y si es un padre de Y con una distancia mayor que el límite de salto del hijo de Y". (Esta es una aproximación conservadora, no se garantiza que represente el conjunto dominador exacto, pero lo mismo es cierto para la heurística existente: es posible que un bloque interno domine la mitad inferior de uno externo). Dado que solo el código con flujo de control irreducible necesitaría especificar un límite, no aumentaría el tamaño del código en el caso común.

Editar: Curiosamente, eso básicamente convertiría la estructura del bloque en una representación del árbol de dominancia. Supongo que sería mucho más simple expresar eso directamente: un árbol de bloques básicos, donde un bloque puede saltar a un bloque hermano, antepasado o hijo inmediato, pero no a un descendiente posterior. No estoy seguro de cómo se mapea mejor en la estructura de alcance existente, donde un "bloque" puede consistir en múltiples bloques básicos con subbucles en el medio.

FWIW: Wasm tiene un diseño particular, que se explica en solo unas pocas palabras muy significativas "excepto que la restricción de anidamiento hace que sea imposible ramificarse en el medio de un bucle desde fuera del bucle".

Si fuera solo un DAG, la validación solo podría verificar que las ramas estuvieran hacia adelante, pero con los bucles esto permitiría ramificarse en el medio del bucle desde fuera del bucle, de ahí el diseño de bloque anidado.

El CFG es solo una parte de este diseño, el otro es el flujo de datos, y hay una pila de valores y también se pueden organizar bloques para desenrollar la pila de valores que puede comunicar de manera muy útil el rango en vivo al consumidor, lo que ahorra trabajo al convertir a SSA .

Es posible extender wasm para que sea una codificación SSA (agregue pick , permita que los bloques devuelvan múltiples valores y tenga valores emergentes de entradas de bucle), por lo que, curiosamente, las restricciones exigidas para una decodificación SSA eficiente podrían no ser necesarias (porque ya podría estar codificado SSA)! Esto conduce a un lenguaje funcional (que podría tener una codificación de estilo de pila para mayor eficiencia).

Si esto se extendiera para manejar CFG arbitrario, entonces podría tener el siguiente aspecto. Esta es una codificación de estilo SSA, por lo que los valores son constantes. Parece que aún se ajusta al estilo de pila en gran medida, solo que no estoy seguro de todos los detalles. Por lo tanto, dentro de blocks se pueden hacer bifurcaciones a cualquier otro bloque etiquetado en ese conjunto, o alguna otra convención utilizada para transferir el control a otro bloque. El código dentro del bloque aún puede hacer referencia útil a los valores en la pila de valores más arriba de la pila para evitar pasarlos todos.

(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)
    ...
   ))

Pero, ¿los navegadores web alguna vez manejarían esta eficiencia internamente?

¿Alguien con experiencia en máquinas de pila reconocería el patrón de código y podría relacionarlo con una codificación de pila?

Hay una discusión interesante sobre bucles irreducibles aquí http://bboissin.appspot.com/static/upload/bboissin-thesis-2010-09-22.pdf

No lo seguí todo en un pase rápido, pero menciona convertir bucles irreducibles en bucles reducibles agregando un nodo de entrada. Para wasm, suena como agregar una entrada definida a los bucles que es específicamente para enviar dentro del bucle, similar a la solución actual pero con una variable definida para esto. Lo anterior menciona que esto está virtualizado, optimizado, en el procesamiento. ¿Quizás algo como esto podría ser una opción?

Si esto está en el horizonte, y dado que los productores ya necesitan usar una técnica similar pero usando una variable local, ¿valdría la pena considerarlo ahora para que wasm producido temprano tenga el potencial de ejecutarse más rápido en tiempos de ejecución más avanzados? Esto también podría crear un incentivo para la competencia entre los tiempos de ejecución para explorar esto.

Esto no sería exactamente etiquetas y gotos arbitrarios, sino algo en lo que podrían transformarse y que tiene alguna posibilidad de compilarse de manera eficiente en el futuro.

Para que conste, estoy firmemente con @oridb y @comex en este tema.
Creo que este es un tema crítico que debe abordarse antes de que sea demasiado tarde.

Dada la naturaleza de WebAssembly, es probable que cualquier error que cometas ahora se mantenga en las próximas décadas (¡mira Javascript!). Por eso el tema es tan crítico; evite admitir gotos ahora por la razón que sea (por ejemplo, para facilitar la optimización, que es, francamente, la influencia de una implementación específica sobre una cosa genérica y, sinceramente, creo que es perezoso), y terminará con problemas a largo plazo.

Ya puedo ver implementaciones futuras (o actuales, pero en el futuro) de WebAssembly que intentan reconocer en casos especiales los patrones habituales de while/switch para implementar etiquetas a fin de manejarlas correctamente. Eso es un truco.

WebAssembly es borrón y cuenta nueva, por lo que ahora es el momento de evitar los hacks sucios (o más bien, los requisitos para ellos).

@darkuranium :

WebAssembly, tal como se especifica actualmente, ya se incluye en navegadores y cadenas de herramientas, y los desarrolladores ya han creado un código que toma la forma establecida en ese diseño. Por lo tanto , no podemos cambiar el diseño de manera radical.

Sin embargo, podemos agregar al diseño de una manera compatible con versiones anteriores. No creo que ninguno de los involucrados piense que goto es inútil. Sospecho que todos usamos regularmente goto , y no solo como juguetes sintácticos.

En este momento, alguien con motivación necesita presentar una propuesta que tenga sentido e implementarla. No veo que se rechace tal propuesta si proporciona datos sólidos.

Dada la naturaleza de WebAssembly, es probable que cualquier error que cometas ahora se mantenga en las próximas décadas (¡mira Javascript!). Por eso el tema es tan crítico; evite admitir gotos ahora por la razón que sea (por ejemplo, para facilitar la optimización, que es, francamente, la influencia de una implementación específica sobre una cosa genérica y, sinceramente, creo que es perezoso), y terminará con problemas a largo plazo.

Así que llamaré a su farol: creo que tener la motivación que muestra, y no presentar una propuesta e implementación como detallé anteriormente, es francamente perezoso.

Estoy siendo descarado, por supuesto. Tenga en cuenta que tenemos personas llamando a nuestras puertas en busca de subprocesos, GC, SIMD, etc. Todos presentan argumentos apasionados y sensatos sobre por qué su función es más importante. Sería genial si pudiera ayudarnos a abordar uno de estos problemas. Hay gente que lo hace por las otras características que menciono. Ninguno por goto hasta ahora. Familiarícese con las pautas de contribución de este grupo y únase a la diversión.

De lo contrario, creo que goto es una gran característica futura . Personalmente, probablemente abordaría otros primero, como la generación de código JIT. Ese es mi interés personal después de GC e hilos.

Hola. Estoy escribiendo una traducción de webassembly a IR y de regreso a webassembly, y he tenido una discusión sobre este tema con la gente.

Me han señalado que el flujo de control irreducible es complicado de representar en un ensamblaje web. Puede resultar problemático para optimizar compiladores que ocasionalmente escriben flujos de control irreducibles. Esto podría ser algo así como el bucle de abajo, que tiene múltiples puntos de entrada:

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

Los compiladores de EBB producirían lo siguiente:

entry:
    cjump x, inside_loop
    // banana
    jump loop

loop:
    cjump y, exit
    // things
    jump inside_loop

inside_loop:
    // do things
    jump loop
exit:
    return

A continuación, pasamos a traducir esto a webassembly. El problema es que aunque tenemos descompiladores descubiertos hace mucho tiempo , siempre tuvieron la opción de agregar el goto en flujos irreducibles.

Antes de que se traduzca, el compilador hará trucos en esto. Pero eventualmente puedes escanear el código y posicionar los comienzos y finales de las estructuras. Terminas con los siguientes candidatos después de eliminar los saltos fallidos:

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

A continuación, debe construir una pila a partir de estos. ¿Cuál va al fondo? Es el 'bucle interior' o es el 'bucle'. No podemos hacer esto, así que tenemos que cortar la pila y copiar las cosas:

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

Ahora podemos traducir esto a webassembly. Perdóneme, todavía no estoy familiarizado con cómo se construyen estos bucles.

Esto no es un problema particular si pensamos en software antiguo. Es probable que el nuevo software se traduzca a ensamblaje web. Pero el problema está en cómo funcionan nuestros compiladores. Han estado haciendo el flujo de control con bloques básicos durante _décadas_ y asumen que todo funciona.

Técnicamente, el idioma se traduce y luego se traduce. Solo necesitamos un mecanismo que permita que los valores fluyan a través de los límites sin dramatismos. El flujo estructurado solo es útil para las personas que tienen la intención de leer el código.

Pero, por ejemplo, lo siguiente funcionaría igual de bien:

    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.

Los números estarían implícitos, es decir... cuando el compilador ve una 'etiqueta', sabe que inicia un nuevo bloque extendido y le asigna un nuevo número de índice, comenzando a incrementar desde 0.

Para producir una pila estática, puede rastrear cuántos elementos hay en la pila cuando encuentra un salto en la etiqueta. Si termina habiendo una pila inconsistente después de un salto a la etiqueta, el programa no es válido.

Si encuentra que lo anterior es malo, también puede intentar agregar una longitud de pila explícita en cada etiqueta (quizás delta del tamaño de pila de la última etiqueta indexada, si el valor absoluto es malo para la compresión), y un marcador para cada salto sobre cuántos valores se copia desde la parte superior de la pila durante el salto.

Podría apostar que no puedes ser más astuto que gzip de ninguna manera por el hecho de cómo representas el flujo de control, por lo que podrías elegir el flujo que sea bueno para los muchachos que tienen el trabajo más duro aquí. (Puedo ilustrar con mi cadena de herramientas de compilador flexible para 'burlar al gzip', si lo desea, ¡simplemente envíeme un mensaje y pongamos una demostración!)

Me siento como un cabeza rota en este momento. Simplemente vuelva a leer la especificación de WebAssembly y detecte que el flujo de control irreducible se omite intencionalmente del MVP, tal vez porque emscripten tuvo que resolver el problema en los primeros días.

La solución sobre cómo manejar el flujo de control irreducible en WebAssembly se explica en el documento "Emscripten: An LLVM-to-JavaScript Compiler". El relooper reorganiza el programa de la siguiente manera:

_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

Lo racional era que el flujo de control estructurado ayuda a leer el volcado del código fuente, y supongo que se cree que ayuda a las implementaciones de polyfill.

Las personas que compilan desde webassembly probablemente se adaptarán para manejar y separar el flujo de control colapsado.

Entonces:

  • Como se mencionó, WebAssembly ahora es estable, por lo que ya pasó el tiempo para cualquier reescritura total de cómo se expresa el flujo de control.

    • En cierto sentido, eso es desafortunado, porque nadie realmente probó si una codificación más directamente basada en SSA podría haber logrado la misma compacidad que el diseño actual.

    • Sin embargo, cuando se trata de especificar goto, ¡eso hace que el trabajo sea mucho más fácil! Las instrucciones basadas en bloques ya están más allá de la eliminación de bicicletas, y no es gran cosa esperar que los compiladores de producción que apuntan a wasm expresen un flujo de control reducible usándolos; el algoritmo no es tan difícil. El principal problema es que una pequeña fracción del flujo de control no se puede expresar usándolos sin un costo de rendimiento. Si solucionamos eso agregando una nueva instrucción goto, no tenemos que preocuparnos tanto por la eficiencia de codificación como lo haríamos con un rediseño total. El código que usa goto aún debe ser razonablemente compacto, por supuesto, pero no tiene que competir con otras construcciones por compacidad; es solo para flujo de control irreducible y debe usarse raramente.

  • La reducibilidad no es particularmente útil.

    • La mayoría de los backends del compilador usan una representación SSA basada en un gráfico de bloques básicos y ramas entre ellos. La estructura de bucle anidado, lo que garantiza la reducibilidad, prácticamente se descarta al principio.

    • Revisé las implementaciones actuales de WebAssembly en JavaScriptCore, V8 y SpiderMonkey, y todas parecen seguir este patrón. (V8 es más complicado, una especie de representación de "mar de nodos" en lugar de bloques básicos, pero también desecha la estructura de anidamiento).

    • Excepción : el análisis de bucles puede ser útil, y las tres implementaciones pasan información al IR sobre qué bloques básicos son los inicios de los bucles. (Compare con LLVM que, como un backend 'pesado' diseñado para la compilación AOT, lo descarta y lo vuelve a calcular en el backend. Esto es más robusto, ya que puede encontrar cosas que no parecen bucles en el código fuente pero sí después de un montón de optimizaciones, pero más lento).

    • El análisis de bucles funciona en "bucles naturales", que prohíben las bifurcaciones en el medio del bucle que no pasan por el encabezado del bucle.

    • WebAssembly debería seguir garantizando que los bloques loop sean bucles naturales.

    • Pero el análisis de bucles no requiere que toda la función sea reducible, ni siquiera el interior del bucle: simplemente prohíbe las ramificaciones desde el exterior hacia el interior. La representación base sigue siendo un gráfico de flujo de control arbitrario.

    • El flujo de control irreducible hace que sea más difícil compilar WebAssembly a JavaScript (polyfilling), ya que el compilador tendría que ejecutar el algoritmo relooper por sí mismo.

    • Pero WebAssembly ya toma múltiples decisiones que agregan una sobrecarga de tiempo de ejecución significativa a cualquier enfoque de compilación a JS (incluido el soporte de acceso a memoria no alineado y la captura de accesos fuera de los límites), lo que sugiere que no se considera muy importante.

    • Comparado con eso, hacer que el compilador sea un poco más complejo no es gran cosa.

    • Por lo tanto, no creo que haya una buena razón para no agregar algún tipo de soporte para el flujo de control irreducible.

  • La información principal necesaria para construir una representación SSA (que, por diseño, debería ser posible en una sola pasada) es el árbol de dominadores .

    • Actualmente, un backend puede estimar el dominio en función del flujo de control estructurado. Si entiendo la especificación correctamente, las siguientes instrucciones finalizan un bloque básico:

    • block :



      • La BB que inicia el bloque está dominada por la BB anterior.*


      • La BB que sigue al end está dominada por la BB que comienza el bloque, pero no por la BB antes de end (porque se omitirá si hubo un br fuera ).



    • loop :



      • La BB que inicia el bloque está dominada por la BB anterior.


      • El BB después de end está dominado por el BB antes de end (ya que no puede acceder a la instrucción después de end excepto ejecutando end ).



    • if :



      • El lado if, el lado else y la BB después de end están dominados por la BB antes de if .



    • br , return , unreachable :



      • (La BB inmediatamente después de br , return o unreachable está disponible).



    • br_if , br_table :



      • La BB antes de br_if / br_table domina a la siguiente.



    • En particular, esto es solo una estimación. No puede producir falsos positivos (decir que A domina a B cuando en realidad no lo hace) porque solo lo dice cuando no hay forma de llegar a B sin pasar por A, por construcción. Pero puede producir falsos negativos (decir que A no domina a B cuando en realidad lo hace), y no creo que un algoritmo de un solo paso pueda detectarlos (podría estar equivocado).

    • Ejemplo falso negativo:

      ```

      bloque $ exterior

      círculo

      br $exterior ;; ya que este incondicionalmente se rompe domina en secreto el final BB

      fin

      fin

    • Pero eso está bien, AFAIK.



      • Los falsos positivos serían malos, porque, por ejemplo, si se dice que el bloque básico A domina el bloque básico B, el código de máquina para B puede usar un registro establecido en A (si nada en el medio sobrescribe ese registro). Si A en realidad no domina a B, el registro podría tener un valor basura.


      • Los falsos negativos son esencialmente ramas fantasma que nunca ocurren. El compilador asume que esas bifurcaciones pueden ocurrir, pero no que deben ocurrir, por lo que el código generado es más conservador de lo necesario.



    • De todos modos, piense en cómo debería funcionar una instrucción goto en términos del árbol dominador. Supongamos que A domina a B, que domina a C.

    • No podemos saltar de A a C porque eso saltaría B (violando la suposición de dominancia). En otras palabras, no podemos saltar a los descendientes no inmediatos. (Y en el extremo del productor binario, si calcularon el verdadero árbol dominador, nunca habrá tal salto).

    • Podríamos saltar con seguridad de A a B, pero ir a un descendiente inmediato no es tan útil. Es básicamente equivalente a una instrucción if o switch, que ya podemos hacer (usando la instrucción if si solo hay una prueba binaria, o br_table si hay varias).

    • También es seguro, y más interesante, saltar a un hermano o al hermano de un antepasado. Si saltamos a nuestro hermano, preservamos la garantía de que nuestro padre domina a nuestro hermano, porque ya debemos haber ejecutado a nuestro padre para llegar aquí (ya que también nos domina). Del mismo modo para los antepasados.

    • En general, un binario malicioso podría producir falsos negativos en el dominio de esta manera, pero como dije, eso (a) ya es posible y (b) aceptable.

  • Basado en eso, aquí hay una propuesta de testaferro:

    • Una nueva instrucción de tipo bloque:
    • etiquetas resulttype N instr* end
    • Debe haber exactamente N instrucciones secundarias inmediatas, donde "secundario inmediato" significa una instrucción de tipo bloque ( loop , block , o labels ) y todo hasta el correspondiente end , o una única instrucción que no sea de bloque (que no debe afectar a la pila).
    • En lugar de crear una sola etiqueta como otras instrucciones de tipo bloque, labels crea N+1 etiquetas: N que apunta a los N elementos secundarios y una que apunta al final del bloque labels . En cada uno de los hijos, los índices de etiqueta 0 a N-1 se refieren a los hijos, en orden, y el índice de etiqueta N se refiere al final.

    En otras palabras, si tienes
    loop ;; outer labels 3 block ;; child 0 br X end nop ;; child 1 nop ;; child 2 end end

    Dependiendo de X, el br refiere a:

    | X | Objetivo |
    | ---------- | ------ |
    | 0 | fin de los block |
    | 1 | hijo 0 (comienzo de block ) |
    | 2 | niño 1 (nop) |
    | 3 | niño 2 (nop) |
    | 4 | fin de labels |
    | 5 | comienzo del bucle exterior |

    • La ejecución comienza en el primer hijo.

    • Si la ejecución llega al final de uno de los hijos, continúa con el siguiente. Si llega al final del último hijo, vuelve al primer hijo. (Esto es por simetría, porque el orden de los niños no debe ser significativo).

    • La bifurcación a uno de los elementos secundarios desenrolla la pila de operandos hasta su profundidad al comienzo de labels .

    • Lo mismo sucede con la bifurcación hasta el final, pero si el tipo de resultado no está vacío, la bifurcación hasta el final hace block .

    • Dominancia: El bloque básico antes de la instrucción labels domina a cada uno de los hijos, así como el BB después del final de labels . Los niños no se dominan entre sí ni al final.

    • Notas de Diseño:

    • N se especifica por adelantado para que el código se pueda validar en una sola pasada. Sería raro tener que llegar al final del bloque labels , para saber el número de hijos, antes de conocer los objetivos de los índices que contiene.

    • No estoy seguro de si eventualmente debería haber una forma de pasar valores en la pila de operandos entre etiquetas, pero por analogía con la incapacidad de pasar valores a block o loop , eso puede no ser compatible para comenzar con.

Sin embargo, sería muy bueno si fuera posible entrar en un bucle, ¿no? IIUC, si se tuviera en cuenta ese caso, entonces el desagradable combo loop+br_table nunca sería necesario...

Editar: oh, puedes hacer bucles sin loop saltando hacia arriba en labels . No puedo creer que me perdí eso.

@qwertie Si un bucle dado no es un bucle natural, el compilador de destino de wasm debería expresarlo usando labels lugar de loop . Nunca debería ser necesario agregar un interruptor para expresar el flujo de control, si es a eso a lo que te refieres. (Después de todo, en el peor de los casos, podría usar un bloque gigante labels con una etiqueta para cada bloque básico de la función. Esto no permite que el compilador conozca la dominancia y los bucles naturales, por lo que puede perderse optimizaciones. Pero labels solo se requiere en los casos en que esas optimizaciones no son aplicables).

La estructura de bucle anidado, lo que garantiza la reducibilidad, prácticamente se descarta al principio. [...] Verifiqué las implementaciones actuales de WebAssembly en JavaScriptCore, V8 y SpiderMonkey, y todas parecen seguir este patrón.

No del todo: al menos en SM, el gráfico IR no es un gráfico completamente general; asumimos ciertas invariantes gráficas que se derivan de la generación de una fuente estructurada (JS o wasm) y, a menudo, simplificamos u optimizamos los algoritmos. Admitir un CFG completamente general requeriría auditar/cambiar muchos de los pases en la canalización para no asumir estas invariantes (ya sea generalizándolas o pesimándolas en caso de irreductibilidad) o duplicando la división de nodos por adelantado para hacer que el gráfico sea reducible. Esto ciertamente es factible, por supuesto, pero no es cierto que esto sea simplemente una cuestión de que wasm sea un cuello de botella artificial.

Además, el hecho de que haya muchas opciones y diferentes motores harán cosas diferentes sugiere que hacer que el productor se ocupe de la irreductibilidad por adelantado producirá un rendimiento algo más predecible en presencia de un flujo de control irreducible.

Cuando discutimos rutas compatibles con versiones anteriores para extender wasm con soporte arbitrario de goto en el pasado, una gran pregunta es cuál es el caso de uso aquí: ¿"hace que los productores sean más simples al no tener que ejecutar un algoritmo de tipo relooper" o es "permitir codegen más eficiente para el flujo de control realmente irreducible"? Si es solo lo primero, entonces creo que probablemente querríamos algún esquema de incrustación de etiquetas/gotos arbitrarios (que sea compatible con versiones anteriores y también se componga con futuros intentos/capturas estructurados por bloques); es solo una cuestión de sopesar el costo/beneficio y los problemas mencionados anteriormente.

Pero para el último caso de uso, una cosa que hemos observado es que, mientras que de vez en cuando ves la carcasa de un dispositivo de Duff en la naturaleza (que en realidad no es una forma eficiente de desenrollar un bucle...), a menudo donde ve aparecer la irreductibilidad donde el rendimiento importa son los bucles de interpretación. Los bucles de intérprete también se benefician de subprocesos indirectos que necesitan goto calculado. Además, incluso en compiladores fuera de línea robustos, los bucles de intérprete tienden a obtener la peor asignación de registros. Dado que el rendimiento del bucle del intérprete puede ser bastante importante, una pregunta es si lo que realmente necesitamos es una primitiva de flujo de control que permita que el motor realice subprocesos indirectos y haga regalloc decente. (Esta es una pregunta abierta para mí.)

@lukewagner
Me gustaría escuchar más detalles sobre qué pases dependen de invariantes. El diseño que propuse, utilizando una construcción separada para el flujo irreducible, debería hacer que sea relativamente fácil para los pases de optimización como LICM evitar ese flujo. Pero si hay otros tipos de roturas en los que no estoy pensando, me gustaría comprender mejor su naturaleza para poder tener una mejor idea de si se pueden evitar y cómo.

Cuando discutimos rutas compatibles con versiones anteriores para extender wasm con soporte arbitrario de goto en el pasado, una gran pregunta es cuál es el caso de uso aquí: ¿"hace que los productores sean más simples al no tener que ejecutar un algoritmo de tipo relooper" o es "permitir codegen más eficiente para el flujo de control realmente irreducible"?

Para mí es lo último; mi propuesta espera que los productores aún ejecuten un algoritmo de tipo relooper para ahorrarle al backend el trabajo de identificar dominadores y bucles naturales, recurriendo a labels solo cuando sea necesario. Sin embargo, esto todavía simplificaría a los productores. Si el flujo de control irreducible tiene una gran penalización, un productor ideal debería trabajar muy duro para evitarlo, usando heurística para determinar si es más eficiente duplicar el código, la cantidad mínima de duplicación que puede funcionar, etc. Si la única penalización es potencialmente dar optimizaciones de bucle, esto no es realmente necesario, o al menos no es más necesario de lo que sería con un backend de código de máquina normal (que tiene sus propias optimizaciones de bucle).

Realmente debería recopilar más datos sobre qué tan común es el flujo de control irreducible en la práctica...

Sin embargo, creo que penalizar tal flujo es esencialmente arbitrario e innecesario. En la mayoría de los casos, el efecto sobre el tiempo de ejecución general del programa debería ser pequeño. Sin embargo, si un punto de acceso incluye un flujo de control irreducible, habrá una penalización severa; en el futuro, las guías de optimización de WebAssembly podrían incluir esto como un problema común y explicar cómo identificarlo y evitarlo. Si mi creencia es correcta, esta es una forma completamente innecesaria de sobrecarga cognitiva para los programadores. E incluso cuando la sobrecarga es pequeña, WebAssembly ya tiene suficiente sobrecarga en comparación con el código nativo por lo que debe tratar de evitar cualquier extra.

Estoy abierto a la persuasión de que mi creencia es incorrecta.

Dado que el rendimiento del bucle del intérprete puede ser bastante importante, una pregunta es si lo que realmente necesitamos es una primitiva de flujo de control que permita que el motor realice subprocesos indirectos y haga regalloc decente.

Eso suena interesante, pero creo que sería mejor comenzar con una primitiva de uso más general. Después de todo, una primitiva adaptada a los intérpretes aún requeriría backends para lidiar con el flujo de control irreducible; si va a morder esa bala, también puede respaldar el caso general.

Alternativamente, mi propuesta ya podría servir como una primitiva decente para los intérpretes. Si combina labels con br_table , puede apuntar una tabla de saltos directamente a puntos arbitrarios en la función, que no es tan diferente de un goto calculado. (A diferencia de un interruptor C, que al menos inicialmente dirige el flujo de control a puntos dentro del bloque de interruptores; si todos los casos son gotos, el compilador debería poder optimizar el salto adicional, pero también podría fusionar múltiples 'redundantes' cambie las declaraciones en una sola, arruinando el beneficio de tener un salto separado después de cada controlador de instrucciones). Sin embargo, no estoy seguro de cuál es el problema con la asignación de registros ...

@comex Supongo que uno podría simplemente desactivar pases de optimización completos a nivel de función en presencia de un flujo de control irreducible (aunque la generación SSA, regalloc y probablemente algunos otros serían necesarios y, por lo tanto, requerirían trabajo), pero asumí que nosotros quería generar código de calidad para funciones con flujo de control irreducible y eso implica auditar cada algoritmo que previamente asumía un gráfico estructurado.

>

La estructura de bucle anidado, lo que garantiza la reducibilidad, es
bastante tirado al principio. [...] revisé la corriente
Implementaciones de WebAssembly en JavaScriptCore, V8 y SpiderMonkey, y
todos parecen seguir este patrón.

No del todo: al menos en SM, el gráfico IR no es un gráfico completamente general; nosotros
asumir ciertas gráficas invariantes que se derivan de ser generadas a partir de un
fuente estructurada (JS o wasm) y a menudo simplifican y/u optimizan el
algoritmos

Lo mismo en V8. De hecho, es una de mis mayores quejas con SSA en ambos
respectiva literatura e implementaciones que casi nunca definen
lo que constituye un CFG "bien formado", pero tienden a asumir implícitamente varios
Restricciones no documentadas de todos modos, generalmente aseguradas por la construcción por el
interfaz de idioma. Apuesto a que muchas/la mayoría de las optimizaciones en los compiladores existentes
no sería capaz de lidiar con CFG verdaderamente arbitrarios.

Como dice @lukewagner , el caso de uso principal para el control irreducible probablemente sea
"código enhebrado" para intérpretes optimizados. Es difícil decir qué tan relevantes son esos
son para el dominio Wasm, y si su ausencia en realidad es la mayor
embotellamiento.

Habiendo discutido el flujo de control irreducible con varias personas
investigando los IR del compilador, la solución "más limpia" probablemente sería agregar
la noción de bloques mutuamente recursivos. Eso pasaría a encajar de Wasm
estructura de control bastante bien.

Las optimizaciones de bucle en LLVM generalmente ignorarán el flujo de control irreducible y no intentarán optimizarlo. El análisis de bucles en el que se basan solo reconocerá bucles naturales, por lo que debe tener en cuenta que puede haber ciclos CFG que no se reconozcan como bucles. Por supuesto, otras optimizaciones son de naturaleza más local y funcionan bien con CFG irreducibles.

De memoria, y probablemente mal, SPEC2006 tiene un solo bucle irreducible en 401.bzip2 y eso es todo. Es bastante raro en la práctica.

Clang solo emitirá una única instrucción indirectbr en funciones que usen goto calculado. Esto tiene el efecto de convertir intérpretes subprocesos en bucles naturales con el bloque indirectbr como encabezado de bucle. Después de salir de LLVM IR, el único indirectbr se duplica en el generador de código para reconstruir el enredo original.

No existe un algoritmo de verificación de un solo paso para el flujo de control irreducible
que soy consciente. La opción de diseño para flujo de control reducible solamente fue
muy influenciado por este requisito.

Como se mencionó anteriormente, el flujo de control irreducible se puede modelar al menos dos
diferentes caminos. Un bucle con una declaración de cambio en realidad se puede optimizar
en el gráfico irreducible original mediante un simple salto local
optimización (por ejemplo, plegando el patrón donde una asignación de una constante
a una variable local ocurre, entonces una rama a una rama condicional que
enciende inmediatamente esa variable local).

Entonces, las construcciones de control irreducible no son necesarias en absoluto, y es
solo es cuestión de una sola transformación de back-end del compilador para recuperar el
gráfico irreducible original y optimizarlo (para motores cuyos compiladores
admite un flujo de control irreducible, que ninguno de los 4 navegadores hace, al menos
lo mejor de mi conocimiento).

Mejor,
-Ben

El jueves 20 de abril de 2017 a las 5:20 a. m., Jakob Stoklund Olesen <
[email protected]> escribió:

Las optimizaciones de bucle en LLVM generalmente ignorarán el flujo de control irreducible
y no intentar optimizarlo. El análisis de bucle en el que se basan será
solo reconoce bucles naturales, por lo que solo debe tener en cuenta que puede haber
ser ciclos CFG que no se reconozcan como bucles. Por supuesto, otros
Las optimizaciones son de naturaleza más local y funcionan bien con irreducibles.
CFG.

De memoria, y probablemente equivocado, SPEC2006 tiene un solo bucle irreducible en
401.bzip2 y listo. Es bastante raro en la práctica.

Clang solo emitirá una sola instrucción indirectabr en funciones que usan
goto calculado. Esto tiene el efecto de convertir a los intérpretes subprocesos en
bucles naturales con el bloque indirectobr como encabezado de bucle. Despues de salir
LLVM IR, el único indirecto se duplica en la cola en el generador de código
para reconstruir el enredo original.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

También puedo decir además que si se agregaran construcciones irreducibles a
WebAssembly, no funcionarían en TurboFan (JIT de optimización de V8), por lo que tal
las funciones terminarían siendo interpretadas (extremadamente lentas) o siendo
compilado por un compilador de referencia (algo más lento), ya que probablemente no
invierta esfuerzo en actualizar TurboFan para admitir un flujo de control irreducible.
Eso significa que las funciones con flujo de control irreducible en WebAssembly
probablemente termine con un rendimiento mucho peor.

Por supuesto, otra opción sería que el motor WebAssembly en V8 ejecutara el
relooper para alimentar gráficos reducibles de TurboFan, pero eso haría que la compilación
(y el arranque peor). Relooping debe seguir siendo un procedimiento fuera de línea en mi
opinión, de lo contrario estamos terminando con costos de motor ineludibles.

Mejor,
-Ben

El lunes 1 de mayo de 2017 a las 12:48 p. m. , Ben L. Titzer

No existe un algoritmo de verificación de un solo paso para el control irreducible
flujo del que soy consciente. La opción de diseño solo para flujo de control reducible
fue muy influenciado por este requisito.

Como se mencionó anteriormente, el flujo de control irreducible se puede modelar al menos dos
diferentes caminos. Un bucle con una declaración de cambio en realidad se puede optimizar
en el gráfico irreducible original mediante un simple salto local
optimización (por ejemplo, plegando el patrón donde una asignación de una constante
a una variable local ocurre, entonces una rama a una rama condicional que
enciende inmediatamente esa variable local).

Entonces, las construcciones de control irreducible no son necesarias en absoluto, y es
solo es cuestión de una sola transformación de back-end del compilador para recuperar el
gráfico irreducible original y optimizarlo (para motores cuyos compiladores
admite un flujo de control irreducible, que ninguno de los 4 navegadores hace, al menos
lo mejor de mi conocimiento).

Mejor,
-Ben

El jueves 20 de abril de 2017 a las 5:20 a. m., Jakob Stoklund Olesen <
[email protected]> escribió:

Las optimizaciones de bucle en LLVM generalmente ignorarán el flujo de control irreducible
y no intentar optimizarlo. El análisis de bucle en el que se basan será
solo reconoce bucles naturales, por lo que solo debe tener en cuenta que puede haber
ser ciclos CFG que no se reconozcan como bucles. Por supuesto, otros
Las optimizaciones son de naturaleza más local y funcionan bien con irreducibles.
CFG.

De memoria, y probablemente mal, SPEC2006 tiene un solo bucle irreducible
en 401.bzip2 y listo. Es bastante raro en la práctica.

Clang solo emitirá una sola instrucción indirectabr en funciones que usan
goto calculado. Esto tiene el efecto de convertir a los intérpretes subprocesos en
bucles naturales con el bloque indirectobr como encabezado de bucle. Despues de salir
LLVM IR, el único indirecto se duplica en la cola en el generador de código
para reconstruir el enredo original.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

Existen métodos establecidos para la verificación en tiempo lineal del flujo de control irreducible. Un ejemplo notable es la JVM: con stackmaps, tiene verificación de tiempo lineal. WebAssembly ya tiene firmas de bloque en cada construcción similar a un bloque. Con información de tipo explícita en cada punto donde se fusionan varias rutas de flujo de control, no es necesario utilizar algoritmos de punto fijo.

(Aparte, hace un tiempo pregunté por qué no se permitiría que un operador pick hipotético lea fuera de su bloque a profundidades arbitrarias. Esta es una respuesta: a menos que las firmas se extiendan para describir todo lo que sea un pick podría leerse, marcar el tipo de pick requeriría más información).

Por supuesto, el patrón loop-with-a-switch se puede saltar, pero no es práctico confiar en él. Si un motor no lo optimiza, tendría un nivel disruptivo de gastos generales. Si la mayoría de los motores lo optimizan, entonces no está claro qué se logra al mantener el flujo de control irreducible fuera del lenguaje mismo.

Suspiro... Tenía la intención de responder antes, pero la vida se interpuso.

He estado analizando algunos motores JS y creo que tengo que debilitar mi afirmación sobre el flujo de control irreducible 'simplemente funcionando'. Todavía no creo que sea tan difícil hacer que funcione, pero hay algunas construcciones que serían difíciles de adaptar de una manera que realmente se beneficiaría más...

Bueno, supongamos, por el bien del argumento, que hacer que la canalización de optimización admita el flujo de control irreducible correctamente es demasiado difícil. Un motor JS aún puede admitirlo fácilmente de una manera pirateada, así:

Dentro del backend, trate un bloque labels como si fuera un loop+switch hasta el último minuto. En otras palabras, cuando ve un bloque labels , lo trata como un encabezado de bucle con un borde hacia afuera que apunta a cada etiqueta, y cuando ve un branch que apunta a una etiqueta, crea un borde que apunta al encabezado labels , no a la etiqueta de destino real, que debe almacenarse por separado en algún lugar. No es necesario crear una variable real para almacenar la etiqueta de destino, como tendría que hacer un bucle+interruptor real; debería ser suficiente esconder el valor en algún campo de la instrucción de bifurcación, o crear una instrucción de control separada para este propósito. Entonces, las optimizaciones, la programación e incluso la asignación de registros pueden simular que hay dos saltos. Pero cuando llega el momento de generar realmente una instrucción de salto nativo, verifica ese campo y genera un salto directamente a la etiqueta de destino.

Puede haber problemas con, por ejemplo, cualquier optimización que fusione/elimine ramas, pero debería ser bastante fácil evitar eso; los detalles dependen del diseño del motor.

En cierto sentido, mi sugerencia es equivalente a la "optimización de subprocesos de salto local simple" de @titzer. Sugiero hacer que el flujo de control irreducible 'nativo' parezca un bucle+interruptor, pero una alternativa sería identificar bucles+interruptores reales, es decir, el patrón de @titzer donde se produce una asignación de una constante a una variable local, luego una rama a una rama condicional que activa inmediatamente esa variable local” y agrega metadatos que permiten que la rama indirecta se elimine tarde en la canalización. Si esta optimización se vuelve omnipresente, podría ser un sustituto decente para una instrucción explícita.

De cualquier manera, la desventaja obvia del enfoque hacky es que las optimizaciones no entienden el gráfico de flujo de control real; actúan efectivamente como si cualquier etiqueta pudiera saltar a cualquier otra etiqueta. En particular, la asignación de registros tiene que tratar una variable como activa en todas las etiquetas, incluso si, por ejemplo, siempre se asigna justo antes de saltar a una etiqueta específica, como en este pseudocódigo:

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

Eso podría conducir a un uso de registro seriamente subóptimo en algunos casos. Pero como señalaré más adelante, los algoritmos de actividad que usan los JIT pueden ser fundamentalmente incapaces de hacer esto bien, de todos modos...

En cualquier caso, optimizar tarde es mucho mejor que no optimizar en absoluto. Un solo salto directo es mucho mejor que un salto + comparación + carga + salto indirecto; el predictor de bifurcación de la CPU eventualmente puede predecir el objetivo de este último en función del estado anterior, pero no tan bien como el compilador. Y puede evitar gastar un registro y/o memoria en la variable 'estado actual'.

En cuanto a la representación, ¿cuál es mejor: explícita (instrucción labels o similar) o implícita (optimización de bucle real+interruptores siguiendo un patrón específico)?

Beneficios a implícito:

  • Mantiene la especificación ajustada.

  • Es posible que ya funcione con el código loop+switch existente. Pero no he mirado las cosas que genera binaryen para ver si sigue un patrón lo suficientemente estricto.

  • Hacer que la forma bendita de expresar el flujo de control irreducible se sienta como un truco destaca el hecho de que es más lento en general y debe evitarse cuando sea posible.

Inconvenientes de implícito:

  • Se siente como un truco. Es cierto, como dice @titzer , en realidad no pone en desventaja a los motores que 'adecuadamente' admiten el flujo de control irreducible; pueden reconocer el patrón temprano y recuperar el flujo irreducible original antes de realizar las optimizaciones. Aún así, parece más ordenado permitir solo los saltos reales.

  • Crea un "acantilado de optimización", que generalmente se supone que WebAssembly debe evitar en comparación con JS. Para recordar, el patrón básico que se optimizará es "donde ocurre una asignación de una constante a una variable local, luego una rama a una rama condicional que inmediatamente enciende esa variable local". Pero, ¿qué sucede si, por ejemplo, hay otras instrucciones en el medio, o si la tarea no usa realmente una instrucción wasm const sino simplemente algo conocido como constante debido a las optimizaciones? Algunos motores pueden ser más liberales que otros en lo que reconocen como este patrón, pero luego el código que se aprovecha de eso (intencionalmente o no) tendrá un rendimiento muy diferente entre navegadores. Tener una codificación más explícita establece expectativas más claramente.

  • Hace que sea más difícil usar wasm como un IR en pasos hipotéticos de posprocesamiento. Si un compilador orientado a wasm hace las cosas de la manera normal y maneja todas las optimizaciones/transformaciones con un IR interno antes de ejecutar un relooper y finalmente generar wasm, entonces no le importaría la existencia de secuencias de instrucciones mágicas. Pero si un programa quiere ejecutar cualquier transformación en el código wasm, tendría que evitar dividir esas secuencias, lo que sería molesto.

De todos modos, no me importa mucho de cualquier manera, siempre que, si decidimos el enfoque implícito, los principales navegadores realmente se comprometan a realizar la optimización relevante.

Volviendo a la cuestión de admitir el flujo irreducible de forma nativa (cuáles son los obstáculos, cuánto beneficio hay), aquí hay algunos ejemplos específicos de IonMonkey de pases de optimización que deberían modificarse para admitirlo:

AliasAnalysis.cpp: itera sobre bloques en orden posterior inverso (una vez) y genera dependencias de ordenación para una instrucción (como se usa en InstrucciónReordenación) mirando solo las tiendas vistas anteriormente como posibles alias. Esto no funciona para el flujo de control cíclico. Pero los bucles (marcados explícitamente) se manejan de manera especial, con un segundo paso que verifica las instrucciones en los bucles contra cualquier almacenamiento posterior en cualquier lugar del mismo bucle.

-> Entonces tendría que haber alguna marca de bucle para los bloques labels . En este caso, creo que marcar todo el bloque labels como un bucle "simplemente funcionaría" (sin marcar especialmente las etiquetas individuales), ya que el análisis es demasiado impreciso para preocuparse por el flujo de control dentro del bucle.

FlowAliasAnalysis.cpp: un algoritmo alternativo que es un poco más inteligente. También itera sobre bloques en orden posterior inverso, pero al encontrar cada bloque fusiona la información calculada de las últimas tiendas para cada uno de sus predecesores (se supone que ya se ha calculado), excepto para los encabezados de bucle, donde tiene en cuenta el backedge.

-> Más desordenado porque asume (a) que los predecesores de los bloques básicos individuales siempre aparecen antes que él, excepto los bordes posteriores del ciclo, y (b) un ciclo solo puede tener un borde posterior. Hay diferentes formas en que esto podría arreglarse, pero probablemente requeriría un manejo explícito de labels , y para que el algoritmo se mantuviera lineal, probablemente tendría que funcionar bastante toscamente en ese caso, más como AliasAnalysis normal

BacktrackingAllocator.cpp: comportamiento similar para la asignación de registros: hace un recorrido inverso lineal a través de la lista de instrucciones y asume que todos los usos de una instrucción aparecerán después (es decir, se procesarán antes) de su definición, excepto cuando se encuentren con backedges de bucle: registros que son en vivo al comienzo de un ciclo, simplemente permanezca en vivo durante todo el ciclo.

-> Cada etiqueta debería tratarse como un encabezado de bucle, pero la vitalidad tendría que extenderse por todo el bloque de etiquetas. No es difícil de implementar, pero nuevamente, el resultado no sería mejor que el enfoque hacky. Creo.

@comex Otra consideración aquí es cuánto se espera que hagan los motores wasm. Por ejemplo, menciona el AliasAnalysis de Ion arriba, sin embargo, el otro lado de la historia es que el análisis de alias no es tan importante para el código WebAssembly, al menos por ahora, mientras que la mayoría de los programas usan memoria lineal.

El algoritmo de vida BacktrackingAllocator.cpp de Ion requeriría algo de trabajo, pero no sería prohibitivo. La mayor parte de Ion ya maneja varias formas de flujo de control irreducible, ya que OSR puede crear múltiples entradas en bucles.

Una pregunta más amplia aquí es qué optimizaciones se espera que hagan los motores de WebAssembly. Si uno espera que WebAssembly sea una plataforma similar a un ensamblaje, con un rendimiento predecible donde los productores/bibliotecas hacen la mayor parte de la optimización, entonces el flujo de control irreducible sería un costo bastante bajo porque los motores no necesitarían los grandes algoritmos complejos donde es una carga significativa. . Si uno espera que WebAssembly sea un código de bytes de nivel superior, que realiza más optimizaciones de alto nivel automáticamente, y los motores son más complejos, entonces se vuelve más valioso mantener el flujo de control irreducible fuera del lenguaje, para evitar la complejidad adicional.

Por cierto, también vale la pena mencionar en este número el algoritmo de construcción SSA sobre la marcha de Braun et al , que es un

Estoy interesado en usar WebAssembly como backend de qemu en iOS, donde WebKit (y el vinculador dinámico, pero que verifica la firma de código) es el único programa que puede marcar la memoria como ejecutable. El codegen de Qemu asume que las sentencias goto serán parte de cualquier procesador para el que tenga que generar código, lo que hace que un backend de WebAssembly sea casi imposible sin que se agreguen gotos.

@tbodt - ¿

@eholk Parece que sería mucho más lento que una traducción directa del código de máquina a wasm.

@tbodt El uso de Binaryen agrega un IR adicional en el camino, sí, pero no debería ser mucho más lento, creo, está optimizado para la velocidad de compilación. Y también puede tener otros beneficios además del manejo de gotos, etc., ya que opcionalmente puede ejecutar el optimizador Binaryen, que puede hacer cosas que el optimizador qemu no hace (cosas específicas de wasm).

De hecho, estaría muy interesado en colaborar contigo en eso, si quieres :) Creo que trasladar Qemu a Wasm sería muy útil.

Entonces, pensándolo bien, gotos realmente no ayudaría mucho. Codegen de Qemu genera el código para bloques básicos cuando se ejecutan por primera vez. Si un bloque salta a un bloque que aún no se ha generado, genera el bloque y parchea el bloque anterior con un goto al siguiente bloque. La carga de código dinámico y el parcheo de funciones existentes no son cosas que se puedan hacer en ensamblaje web, que yo sepa.

@kripken Me interesaría colaborar, ¿

No puede parchear las funciones existentes directamente, pero puede usar call_indirect y WebAssembly.Table para eliminar el código. Para cualquier bloque básico que no se haya generado, puede llamar a JavaScript, generar el módulo WebAssembly y la instancia de forma síncrona, extraer la función exportada y escribirla sobre el índice de la tabla. Las llamadas futuras usarán su función generada.

Sin embargo, no estoy seguro de que alguien haya probado esto todavía, por lo que es probable que haya muchas asperezas.

Eso podría funcionar si se implementaran tailcalls. De lo contrario, la pila se desbordaría bastante rápido.

Otro desafío sería asignar espacio en la tabla predeterminada. ¿Cómo asigna una dirección a un índice de tabla?

Otra opción es regenerar la función wasm en cada nuevo bloque básico. Esto significa una cantidad de recompilaciones igual a la cantidad de bloques usados, pero apuesto a que es la única forma de hacer que el código se ejecute rápidamente después de compilarlo (especialmente los bucles internos), y no necesita ser un completo recompilar, podemos reutilizar Binaryen IR para cada bloque existente, agregar IR para el nuevo bloque y simplemente ejecutar el relooper en todos ellos.

(¿Pero tal vez podamos hacer que qemu compile toda la función por adelantado en lugar de perezosamente?)

@tbodt por su colaboración para hacer esto con Binaryen, una opción es crear un repositorio con su trabajo (y puede usar problemas allí, etc.), otra es abrir un problema específico en Binaryen para qemu.

No podemos hacer que qemu compile una función completa a la vez, porque qemu no tiene un concepto de "función".

En cuanto a volver a compilar todo el caché de bloques, parece que podría llevar mucho tiempo. Averiguaré cómo usar el generador de perfiles incorporado de qemu y luego abriré un problema en binaryen.

Pregunta lateral. En mi opinión, un lenguaje dirigido a WebAssembly debería poder proporcionar una función recursiva mutua eficiente. Para ver una descripción de su utilidad, lo invito a leer: http://sharp-gamedev.blogspot.com/2011/08/forgotten-control-flow-construct.html

En particular, la necesidad expresada por Cheery parece ser abordada por una función mutuamente recursiva.

Entiendo la necesidad de recurrencia de cola, pero me pregunto si la función mutuamente recursiva solo se puede implementar si la maquinaria subyacente proporciona gotos o no. Si lo hacen, entonces, para mí, eso constituye un argumento legítimo a favor de ellos, ya que habrá un montón de lenguaje de programación que tendrá dificultades para apuntar a WebAssembly de otra manera. Si no lo hacen, quizás el mecanismo mínimo para admitir la función recursiva mutua sea todo lo que se necesitaría (junto con la recursividad de la cola).

@davidgrenier , las funciones en un módulo Wasm son mutuamente recursivas. ¿Puedes explicar qué consideras ineficiente de ellos? ¿Solo te refieres a la falta de llamadas de cola o algo más?

Se acercan llamadas de cola generales. La recursión de cola (mutua o de otro tipo) será un caso especial de eso.

No estaba diciendo que algo fuera ineficiente sobre ellos. Lo que digo es que, si los tiene, no necesita un goto general porque las funciones mutuamente recursivas proporcionan todo lo que necesita el implementador de lenguaje dirigido a WebAssembly.

Goto es muy útil para generar código a partir de diagramas en programación visual. Tal vez ahora la programación visual no sea muy popular, pero en el futuro puede atraer a más personas y creo que wasm debería estar preparado para ello. Más información sobre la generación de código a partir de los diagramas e ir a: http://drakon-editor.sourceforge.net/generation.html

El próximo lanzamiento de Go 1.11 tendrá soporte experimental para WebAssembly. Esto incluirá soporte completo para todas las características de Go, incluidas rutinas, canales, etc. Sin embargo, el rendimiento del WebAssembly generado actualmente no es tan bueno.

Esto se debe principalmente a la falta de la instrucción goto. Sin la instrucción goto, teníamos que recurrir al uso de un bucle de nivel superior y una tabla de saltos en cada función. Usar el algoritmo relooper no es una opción para nosotros, porque al cambiar entre goroutines necesitamos poder reanudar la ejecución en diferentes puntos de una función. El relooper no puede ayudar con esto, solo una instrucción goto puede hacerlo.

Es asombroso que WebAssembly haya llegado al punto en que puede admitir un lenguaje como Go. Pero para ser verdaderamente el ensamblador de la web, WebAssembly debería ser tan poderoso como otros lenguajes ensambladores. Go tiene un compilador avanzado que puede emitir un ensamblaje muy eficiente para otras plataformas. Es por eso que me gustaría argumentar que es principalmente una limitación de WebAssembly y no del compilador Go que no es posible usar también este compilador para emitir un ensamblado eficiente para la web.

Usar el algoritmo relooper no es una opción para nosotros, porque al cambiar entre goroutines necesitamos poder reanudar la ejecución en diferentes puntos de una función.

Solo para aclarar, un goto regular no sería suficiente para eso, se requiere un goto calculado para su caso de uso, ¿es correcto?

Creo que un goto regular probablemente sería suficiente en términos de rendimiento. Los saltos entre bloques básicos son estáticos de todos modos y para cambiar goroutines, un br_table con gotos en sus ramas debería ser lo suficientemente eficaz. Sin embargo, el tamaño de salida es una cuestión diferente.

Parece que tiene un flujo de control normal en cada función, pero también necesita la capacidad de saltar de la entrada de la función a otras ubicaciones en el "medio", al reanudar una rutina. ¿Cuántas de esas ubicaciones hay? Si se trata de cada bloque básico, entonces el relooper se vería obligado a emitir un bucle de nivel superior por el que pasa cada instrucción, pero si son solo unos pocos, eso no debería ser un problema. (Eso es realmente lo que sucede con la compatibilidad con setjmp en emscripten: simplemente creamos las rutas adicionales necesarias entre los bloques básicos de LLVM y dejamos que el relooper procese eso normalmente).

Cada llamada a alguna otra función es una ubicación de este tipo y la mayoría de los bloques básicos tienen al menos una instrucción de llamada. Estamos más o menos relajando y restaurando la pila de llamadas.

Ya veo, gracias. Sí, estoy de acuerdo en que para que eso sea práctico, necesita soporte de restauración de pila de llamadas o goto estático (que también se ha considerado).

¿Será posible llamar a la función en estilo CPS o implementar call/cc en WASM?

@Heimdell , el soporte para alguna forma de continuaciones delimitadas (también conocido como "cambio de pila") está en la hoja de ruta, lo que debería ser suficiente para casi cualquier abstracción de control interesante. Sin embargo, no podemos admitir continuaciones ilimitadas (es decir, llamada/cc completa), ya que la pila de llamadas de Wasm se puede entremezclar arbitrariamente con otros idiomas, incluidas las llamadas reentrantes al integrador y, por lo tanto, no se puede suponer que sea copiable o móvil.

Al leer este hilo, tengo la impresión de que las etiquetas arbitrarias y los gotos tienen un gran obstáculo antes de convertirse en una característica:

  • El flujo de control no estructurado hace posibles los gráficos de flujo de control irreducibles
  • Eliminando* cualquier "verificación rápida y simple, fácil conversión de un solo paso al formulario SSA"
  • Abriendo el compilador JIT al rendimiento no lineal
  • Las personas que navegan por páginas web no deberían tener que sufrir retrasos si el compilador del idioma original puede hacer el trabajo inicial.

_*aunque puede haber alternativas como el algoritmo de construcción sobre la marcha SSA de Braun et al que maneja el flujo de control irreducible_

Si todavía estamos atascados allí, las llamadas _and_ tail están avanzando, tal vez valdría la pena pedirles a los compiladores de lenguaje que aún traduzcan a gotos, pero como paso final antes de la salida de WebAssembly, divida los "bloques de etiquetas" en funciones, y convertir los gotos en llamadas de cola.

Según el artículo de 1977 del diseñador de esquemas Guy Steele, Lambda: The Ultimate GOTO , la transformación debería ser posible y el rendimiento de las llamadas de cola debería poder coincidir estrechamente con los gotos.

¿Pensamientos?

Si todavía estamos atascados allí, las llamadas _and_ tail están avanzando, tal vez valdría la pena pedirles a los compiladores de lenguaje que aún traduzcan a gotos, pero como paso final antes de la salida de WebAssembly, divida los "bloques de etiquetas" en funciones, y convertir los gotos en llamadas de cola.

Esto es esencialmente lo que todos los compiladores harían de todos modos, nadie que yo sepa aboga por gotos no administrados del tipo que causan tantos problemas en la JVM, solo para un gráfico de EBB escritos. LLVM, GCC, Cranelift y el resto tienen un CFG de forma SSA (posiblemente irreducible) como su representación interna y los compiladores desde Wasm hasta native tienen la misma representación interna, por lo que queremos conservar la mayor cantidad de información posible y reconstruir la menor cantidad posible de esa información. Los locales tienen pérdidas, ya que ya no son SSA, y el flujo de control de Wasm tiene pérdidas, ya que ya no es un CFG arbitrario. AFAIK hacer que Wasm sea una máquina de registro SSA de registro infinito con información de actividad de registro de grano fino integrada probablemente sería lo mejor para la generación de código, pero el tamaño del código se inflaría, una máquina de pila con flujo de control modelado en un CFG arbitrario es probablemente el mejor término medio . Sin embargo, podría estar equivocado sobre el tamaño del código con una máquina de registro, podría ser posible codificarlo de manera eficiente.

Lo que pasa con el flujo de control irreducible es que si es irreducible en el front-end, sigue siendo irreducible en wasm, la conversión de relooper/apilador no hace que el flujo de control sea reducible, simplemente convierte la irreductibilidad para que dependa de los valores de tiempo de ejecución. Esto le da al backend menos información y, por lo tanto, puede producir un código peor, la única forma de producir un buen código para CFG irreducibles en este momento es detectar los patrones emitidos por relooper y stackifier y convertirlos nuevamente en un CFG irreducible. A menos que esté desarrollando V8, que AFAIK solo admite el flujo de control reducible, admitir el flujo de control irreducible es puramente una victoria: hace que tanto los frontends como los backends sean mucho más simples (los frontends pueden emitir código en el mismo formato en el que lo almacenan internamente, los backends no no tiene que detectar patrones) mientras produce una mejor salida en el caso de que el flujo de control sea irreducible y una salida igual o mejor en el caso habitual de que el flujo de control sea reducible.

Además, permitiría a GCC y Go comenzar a producir WebAssembly.

Sé que V8 es un componente importante del ecosistema WebAssembly, pero parece ser la única parte de ese ecosistema que se beneficia de la situación actual del flujo de control, todos los demás backends que conozco se convierten en un CFG de todos modos y no se ven afectados por si WebAssembly puede representar un flujo de control irreducible o no.

¿No podría v8 simplemente incorporar relooper para aceptar CFG de entrada? Parece que grandes porciones del ecosistema están bloqueadas en los detalles de implementación de v8.

Solo como referencia, noté que las declaraciones de cambio en c ++ son muy lentas en wasm. Cuando perfilé el código, tuve que convertirlo a otras formas que operaron mucho más rápido para realizar el procesamiento de imágenes. Y nunca fue un problema en otras arquitecturas. Realmente me gustaría goto por razones de rendimiento.

@graph , ¿podría proporcionar más detalles sobre cómo las "instrucciones de cambio son lentas"? Siempre buscando una oportunidad para mejorar el rendimiento... (Si no quiere atascar este hilo, envíeme un correo electrónico directamente a [email protected]).

Publicaré aquí ya que esto se aplica a todos los navegadores. Las declaraciones simples como esta cuando se compilan con emscripten eran más rápidas cuando las convertía a declaraciones if.

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

Supongo que el compilador estaba convirtiendo una tabla de salto a lo que admita wasm. No miré el ensamblaje generado, así que no puedo confirmarlo.

Conozco un par de cosas no relacionadas con wasm que se pueden optimizar para el procesamiento de imágenes en la web. Ya lo envié a través del botón "Comentarios" en Firefox. Si te interesa avísame y te mando los problemas por correo electrónico.

@graph Un punto de referencia completo sería muy útil aquí. En general, un interruptor en C puede convertirse en una tabla de salto muy rápida en wasm, pero hay casos extremos que aún no funcionan bien, que es posible que debamos corregir, ya sea en LLVM o en los navegadores.

Específicamente en emscripten, la forma en que se manejan los interruptores cambia mucho entre el antiguo backend fastcomp y el nuevo upstream, por lo que si vio esto hace un tiempo, o recientemente pero usando fastcomp, sería bueno verificar upstream.

@graph , si emscripten produce una br_table, entonces el jit a veces generará una tabla de salto y, a veces (si cree que será más rápido), buscará el espacio clave linealmente o con una búsqueda binaria en línea. Lo que hace a menudo depende del tamaño del interruptor. Por supuesto, es posible que la política de selección no sea óptima... Estoy de acuerdo con @kripken , el código ejecutable sería muy útil aquí si tiene algo para compartir.

(No sé acerca de v8 o jsc, pero Firefox actualmente no reconoce una cadena if-then-else como un posible cambio, por lo que generalmente no es una buena idea abrir interruptores de código como cadenas if-then-else largas. El el punto de equilibrio es probablemente en no más de dos o tres comparaciones).

@lars-t-hansen @kripken @graph es muy posible que br_table actualmente no esté optimizado, como parece mostrar este intercambio: https://twitter.com/battagline/status/1168310096515883008

@aardappel , eso es curioso, los puntos de referencia que ejecuté ayer no mostraron esto, en firefox en mi sistema, el punto de equilibrio fue de alrededor de 5 casos según lo recuerdo y después de eso, br_table fue el ganador. microbenchmark por supuesto, y con algún intento de distribución uniforme de las claves de búsqueda. si el nido "si" está sesgado hacia las claves más probables, de modo que no se necesitan más de un par de pruebas, entonces el nido "si" ganará.

Si no puede hacer el análisis de rango en el valor del interruptor para evitarlo, br_table también tendrá que hacer al menos una prueba de filtrado para el rango del interruptor, lo que también reduce su ventaja.

@lars-t-hansen Sí, no conocemos su caso de prueba, puede que tenga un valor atípico. De cualquier manera, parece que Chrome tiene más trabajo que hacer que Firefox.

Estoy de vacaciones, de ahí mi falta de respuestas. Gracias por entender.

@kripken @ Realicé algunas pruebas, parece que sí, era mejor ahora en Firefox. Todavía hay algunos casos en los que if-else supera al switch. Aquí hay un caso:


Principal.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;
}

Dependiendo del valor de switchSelect. if-else supera. Salida de ejemplo:

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

Como puede ver, switchSelect = 32 if-else es mucho más rápido. Para los otros casos, if-else es un poco más rápido. Para el caso switchSelect = 1 & 0, la instrucción switch es más rápida.

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)

Usando el último emscripen estable a partir del 20 de octubre de 2019. Instalación nueva ./emcc activate latest .

Noté arriba que hay un error tipográfico, pero no debería afectar el asunto de que if-else es un caso SW3 más rápido ya que están ejecutando las mismas instrucciones.

de nuevo con esto yendo más allá del punto de equilibrio de 5: Interesante que para switchSelect=32 para este caso es similar en velocidad a if-else. Como puede ver, para 1003 if-else es un poco más rápido. Switch debería ganar en este caso.

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


principal.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;
}


Gracias por echar un vistazo a estos casos de prueba.

Sin embargo, ese es un switch muy escaso, que LLVM debería convertir al equivalente de un conjunto de si-entonces de todos modos, pero aparentemente lo hace de una manera que es menos eficiente que el manual si-entonces. ¿Ha intentado ejecutar wasm2wat para ver cómo estos dos bucles difieren en el código?

Esto también depende en gran medida de que esta prueba use el mismo valor en cada iteración. Esta prueba sería mejor si pasara por todos los valores, o mejor aún, si se eligiera al azar de ellos (si eso se puede hacer de forma económica).

Mejor aún, la verdadera razón por la que la gente usa el interruptor para el rendimiento es con un rango denso, por lo que puede garantizar que en realidad está usando br_table debajo. Ver en cuántos casos br_table es más rápido que if sería lo más útil que hay que saber.

Se usó el interruptor en los bucles estrechos porque era un código más limpio que el rendimiento. Pero para wasm, el impacto en el rendimiento era demasiado grande, por lo que se convirtió en declaraciones if más feas. Para el procesamiento de imágenes en muchos de mis casos de uso, si quiero obtener más rendimiento de un interruptor, movería el interruptor fuera del bucle y simplemente tendría copias del bucle para cada caso. Por lo general, el cambio es solo cambiar entre alguna forma de formato de píxel, formato de color, codificación, etc. Y, en muchos casos, las constantes se calculan a través de definiciones o enumeraciones y no lineales. Ahora veo que mi problema no está relacionado con el diseño de goto. Solo tenía una comprensión incompleta de lo que estaba sucediendo con mis instrucciones de cambio. Espero que mis notas sean útiles para los desarrolladores de navegadores que lean esto para optimizar wasm para el procesamiento de imágenes en estos casos. Gracias.

Nunca pensé que goto pudiera ser un debate tan acalorado 😮 . Estoy en el bote de que todos los idiomas deberían tener un goto 😁. Otra razón para agregar goto es que reduce la complejidad para que el compilador compile en wasm. Estoy bastante seguro de que se menciona arriba en alguna parte. Ahora no tengo nada de que quejarme 😞 .

¿Algún progreso adicional allí?

Debido al acalorado debate, asumiría que algún navegador agregaría soporte para goto como una extensión de código de bytes no estándar. Entonces, tal vez GCC pueda ingresar al juego como compatible con una versión no estándar. Lo cual no creo que sea bueno en general, pero permitirá una mayor competencia de compiladores. ¿Se ha considerado esto?

No ha habido mucho progreso últimamente, pero es posible que desee ver la propuesta de funclets .

@graph para mí, su sugerencia suena como "rompamos todo y esperemos que mejore".
No funciona así. Hay MUCHOS beneficios de la estructura WebAssembly actual (que no son obvios, desafortunadamente). Intenta profundizar en la filosofía de wasm.

Permitir "Etiquetas y Gotos arbitrarios" nos llevará de vuelta a los tiempos (antiguos) del código de bytes no verificable. Todos los compiladores simplemente cambiarán a una "manera perezosa" de hacer las cosas, en lugar de "hacerlo bien".

Está claro que wasm en su estado actual tiene algunas omisiones importantes. La gente está trabajando para llenar los vacíos (como el mencionado por @binji ), pero no creo que la "estructura wasm global" deba ser reelaborada. Solo mi humilde opinión.

@vshymanskyy La propuesta de funclets, que proporciona una funcionalidad equivalente a etiquetas y gotos arbitrarios, es completamente validable, en tiempo lineal.

También debo mencionar que en nuestro compilador Wasm de tiempo lineal, compilamos internamente todo el flujo de control de Wasm en una representación similar a funclets, sobre la cual tengo información en esta publicación de bloque y se implementa la conversión del flujo de control de Wasm a esta representación interna. aquí El compilador obtiene toda su información de tipo de esta representación tipo funclets, por lo que basta con decir que es trivial validar la seguridad de tipo en tiempo lineal.

Creo que esta idea errónea de que el flujo de control irreducible no se puede validar en tiempo lineal proviene de la JVM, donde el flujo de control irreducible debe ejecutarse utilizando el intérprete en lugar de compilarse. Esto se debe a que la JVM no tiene ninguna forma de representar metadatos de tipo para el flujo de control irreducible, por lo que no puede realizar la conversión de máquina de pila a máquina de registro. Los "gotos arbitrarios" (es decir, saltar al byte/instrucción X) no son verificables en absoluto, pero separar una función en bloques escritos, que luego se pueden saltar entre ellos en un orden arbitrario, no es más difícil de verificar que separar un módulo en funciones escritas. , que luego se pueden saltar en un orden arbitrario. No necesita gotos sin tipo de estilo jump-to-byte-X para implementar patrones útiles que serían emitidos por compiladores como GCC y LLVM.

Me encanta el proceso aquí. Lado A explica por qué esto es necesario en aplicaciones específicas. El lado B dice que lo están haciendo mal, pero no ofrece soporte para esa aplicación. El lado A explica cómo ninguno de los argumentos pragmáticos de B se sostiene. El lado B no quiere lidiar con eso porque cree que el lado A lo está haciendo mal. El lado A está tratando de lograr una meta. El lado B dice que ese es el objetivo equivocado, llamándolo perezoso o brutal. Los significados filosóficos más profundos se pierden en el lado A. Los pragmáticos se pierden en el lado B, ya que afirman tener algún tipo de fundamento moral superior. El lado A ve esto como una operación mecánica amoral. En última instancia, el lado B generalmente mantiene el control de la especificación para bien o para mal, y han hecho una cantidad increíble con su relativa pureza.

Honestamente, me metí aquí porque hace años, estaba tratando de hacer un puerto TinyCC a WASM para poder ejecutar un entorno de desarrollo en un ESP8266 dirigido al ESP8266. Solo tengo ~ 4 MB de almacenamiento, por lo que incluir un re-looper y cambiar a un AST, así como muchos otros cambios, está fuera de discusión. (Nota al margen: ¿Cómo es que relooper es lo único que se parece a relooper? ¡Es tan horrible y nadie ha reescrito ese tonto en In C!?) Incluso si fuera posible en este punto, no sé si escribiría un objetivo TinyCC a WASM ya que ya no es tan interesante para mí.

Este hilo, sin embargo. Dios mío, este hilo me ha traído tanta alegría existencial. Ver una bifurcación en la humanidad más profunda que la democracia, la república o la religión. Siento que esto se puede resolver alguna vez. Si A puede venir a vivir en el mundo de B, o B validar la afirmación de A de que la programación procedimental tiene su lugar... Siento que podríamos resolver la paz mundial.

¿Alguien a cargo de V8 podría confirmar en este hilo que la oposición contra el flujo de control irreducible no está influenciada por la implementación actual de V8?

Lo pregunto porque esto es lo que más me molesta. Me parece que esto debería ser una discusión a nivel de especificaciones sobre los pros y los contras de esta función. No debe verse influenciado en absoluto por cómo se diseña actualmente una implementación en particular. Sin embargo, ha habido declaraciones que me hacen creer que la implementación de V8 está influyendo en esto. Tal vez yo estoy equivocado. Una declaración abierta podría ayudar.

Bueno, por más desafortunado que sea, las implementaciones actuales existentes hasta ahora son tan importantes que el futuro (presumiblemente más largo que el pasado) no es tan importante. Estaba tratando de explicar eso en el n. ° 1202, que la consistencia es más importante que las pocas implementaciones, pero parece que estoy delirando. Buena suerte de explicar que algunas decisiones de desarrollo en algún lugar de algún proyecto no constituyen una verdad universal, o tienen que ser, por defecto, asumidas como correctas.

Este hilo es un canario en la mina de carbón W3C. Aunque tengo un gran respeto por muchas personas del W3C, la decisión de confiar JavaScript a Ecma International, y no al W3C, no se tomó sin prejuicios.

Al igual que @cnlohr , tenía esperanzas de un puerto wasm de TCC, y por una buena razón;

"Wasm está diseñado como un objetivo de compilación portátil para lenguajes de programación, lo que permite la implementación en la web para aplicaciones de cliente y servidor". - webassembly.org

Claro, cualquiera puede pontificar por qué goto es [INSERTAR JARGO], pero ¿qué tal si preferimos los estándares a las opiniones? Todos podemos estar de acuerdo en que POSIX C es un buen objetivo de referencia, especialmente dado que los langs de hoy en día se forjaron a partir de C o se compararon con ellos, y el titular de la página de inicio de WASM se promociona a sí mismo como un objetivo de compilación portátil para langs. Claro, algunas funciones se trazarán como subprocesos y simd. Pero, ignorar por completo algo tan fundamental como goto , ni siquiera darle la decencia de una hoja de ruta, no es consistente con el propósito declarado de WASM y la postura del organismo de estandarización que dio luz verde a <marquee> está más allá de los límites.

De acuerdo con el estándar de codificación SEI CERT C Rec. "Considere usar una cadena goto cuando deje una función con error al usar y liberar recursos" ;

Muchas funciones requieren la asignación de múltiples recursos. Fallar y regresar en algún lugar en medio de esta función sin liberar todos los recursos asignados podría producir una pérdida de memoria. Es un error común olvidarse de liberar uno (o todos) los recursos de esta manera, por lo que una cadena Goto es la forma más simple y limpia de organizar las salidas mientras se preserva el orden de los recursos liberados.

La recomendación luego ofrece un ejemplo con la solución POSIX C preferida usando goto . Los detractores señalarán la nota de que goto todavía se considera dañino . Curiosamente, esta opinión no está incorporada en uno de esos estándares de codificación particulares, solo una nota. Lo que nos lleva al canario, el "considerado nocivo".

En pocas palabras, la consideración de "Regiones CSS" o goto como dañinas solo debe sopesarse junto con una solución propuesta al problema para el que se utiliza dicha función. Si eliminar dicha característica "perjudicial" equivale a eliminar los casos de uso razonable sin alternativa, esa no es una solución, de hecho, es perjudicial para los usuarios del lenguaje.

Las funciones no son de coste cero, ni siquiera en C. Si alguien ofrece un reemplazo para gotos & label, ¡canihaz por favor! Si alguien dice que no lo necesito, ¿cómo lo sabe? Cuando se trata de rendimiento, goto puede darnos ese pequeño extra, difícil de discutir con los ingenieros que no necesitamos características de alto rendimiento y fáciles de asimilar que han existido desde los albores del lenguaje.

Sin un plan para admitir goto , WASM es un objetivo de compilación de juguete, y está bien, tal vez así es como el W3C ve la web. Espero que WASM como estándar llegue más alto, fuera del espacio de direcciones de 32 bits, y entre en la carrera de compilación. Espero que el discurso de la ingeniería pueda alejarse de "eso no es posible..." para acelerar las extensiones GCC C como Etiquetas como valores porque WASM debería ser IMPRESIONANTE. Personalmente, TCC es considerablemente más impresionante y más útil en este punto, sin toda la pontificación desperdiciada, sin la página de inicio hipster y el logotipo brillante.

@d4tocchini :

De acuerdo con el estándar de codificación SEI CERT C Rec. "Considere usar una cadena goto cuando deje una función con error al usar y liberar recursos" ;

Muchas funciones requieren la asignación de múltiples recursos. Fallar y regresar en algún lugar en medio de esta función sin liberar todos los recursos asignados podría producir una pérdida de memoria. Es un error común olvidarse de liberar uno (o todos) los recursos de esta manera, por lo que una cadena Goto es la forma más simple y limpia de organizar las salidas mientras se preserva el orden de los recursos liberados.

La recomendación luego ofrece un ejemplo con la solución POSIX C preferida usando goto . Los detractores señalarán la nota de que goto todavía se considera dañino . Curiosamente, esta opinión no está incorporada en uno de esos estándares de codificación en particular, solo una nota. Lo que nos lleva al canario, el "considerado nocivo".

El ejemplo dado en esa recomendación se puede expresar directamente con descansos etiquetados, que están disponibles en Wasm. No necesita el poder adicional de goto arbitrario. (C no proporciona interrupción y continuación etiquetadas, por lo que tiene que retroceder para ir a más a menudo de lo necesario).

@rossberg , buen punto sobre los descansos etiquetados en ese ejemplo, pero no estoy de acuerdo con su suposición cualitativa de que C debe "retroceder". goto es una construcción más rica que las rupturas etiquetadas. Si se va a incluir C entre los objetivos de compilación portátiles, y C no admite saltos etiquetados, eso es más bien un punto de silencio. Java ha etiquetado pausas/continuaciones mientras que Python rechazó la característica propuesta , y considerando que tanto la JVM de Sun como el CPython predeterminado están escritos en C, ¿no estaría de acuerdo en que C como lenguaje admitido debería estar más arriba en la lista de prioridades?

Si goto debe descartarse tan fácilmente, ¿deberían reconsiderarse también los cientos de usos de goto dentro de la fuente de emscripten ?

¿Hay algún lenguaje que no se pueda escribir en C? C como lenguaje debería estar informando las características de WASM. Si POSIX C no es posible con el WASM de hoy, entonces existe su hoja de ruta adecuada.

No realmente sobre el tema del argumento, pero para no dejar sombra de que los errores aleatorios están al acecho aquí y allá en la argumentación en general:

Python tiene descansos etiquetados

¿Puedes elaborar? (Aka: Python no tiene saltos etiquetados).

@pfalcon , sí, mi edité mi comentario para aclarar que Python propuso pausas/continuaciones etiquetadas y lo rechacé

Si goto debe descartarse tan fácilmente, ¿deberían reconsiderarse también los cientos de usos de goto dentro de la fuente de emscripten?

1) Tenga en cuenta cuánto de eso está presente en musl libc, no directamente en emscripten. (El segundo más utilizado es tests/third_party)
2) Las construcciones de nivel de fuente no son lo mismo que las instrucciones de código de bytes
3) Emscripten no tiene el mismo nivel de abstracción que el estándar wasm, por lo que no debería reconsiderarse sobre esa base.

En concreto, podría ser útil hoy en día para volver a escribir GOTOS fuera de libc, porque entonces tendríamos un mayor control sobre la CFG resultante en lugar de confiar Relooper / cfgstackify manejarlo bien. No lo hemos hecho porque no es una cantidad de trabajo trivial terminar con un código muy divergente de musl aguas arriba.

Los desarrolladores de Emscripten (la última vez que lo comprobé) tienden a opinar que una estructura similar a Goto sería realmente agradable, por esas razones obvias, por lo que es poco probable que la descarten, incluso si lleva años alcanzar un compromiso aceptable.

tal postura del organismo de estandarización que dio luz verde a <marquee> está más allá de los límites.

Esta es una declaración particularmente estúpida.

1) Nosotros, Internet más amplio, estamos a más de una década de haber tomado esa decisión.
2) We-the-wasm-CG somos un grupo de personas completamente (¿casi?) Separado de esa etiqueta, y probablemente también estén molestos individualmente por errores pasados ​​obvios.

sin todas las pontificaciones desperdiciadas, sin la página de inicio hipster y el logotipo brillante.

Esto podría haberse reformulado a "Estoy frustrado" sin tener problemas de tono.

Como muestra este hilo, estas conversaciones ya son bastante difíciles.

Hay un nuevo nivel de preocupación profunda cuando desea reescribir un conjunto de funciones profundamente confiables y entendidos para todo nuevo solo porque un entorno para su uso tiene que pasar por pasos adicionales para admitirlo. (aunque todavía estoy en el campo firmemente por favor agregue-goto porque odio estar atado a usar solo un compilador específico)

Creo que este hilo dejó de ser productivo: ha estado funcionando durante más de cuatro años y parece que aquí se han utilizado todos los argumentos posibles a favor y en contra de los goto arbitrarios; también se debe tener en cuenta que ninguno de esos argumentos es particularmente nuevo;)

Hay tiempos de ejecución administrados que optaron por no tener etiquetas de salto arbitrarias, lo que funcionó bien para ellos. Además, hay sistemas de programación donde se permiten saltos arbitrarios y también funcionan bien. Al final, los autores de un sistema de programación toman decisiones de diseño y solo el tiempo realmente muestra si esas elecciones son exitosas o no.

Las opciones de diseño de Wasm que prohíben los saltos arbitrarios son fundamentales para su filosofía. Es poco probable que pueda admitir goto s sin algo como funclets, por las mismas razones que no admite saltos indirectos puros.

Las opciones de diseño de Wasm que prohíben los saltos arbitrarios son fundamentales para su filosofía. Es poco probable que pueda admitir gotos sin algo como funclets, por las mismas razones que no admite saltos indirectos puros.

@penzn ¿ atasca la propuesta de funclets ? Existe desde octubre de 2018 y todavía está en fase 0.

Si estuviéramos discutiendo un proyecto de código abierto común y corriente, me bifurcaría y terminaría con él. Estamos hablando de un estándar de monopolio de gran alcance aquí. Se debe cultivar una respuesta comunitaria vigorosa porque nos importa.

@J0eCool

  1. Tenga en cuenta cuánto de eso está presente en musl libc, no directamente en emscripten. (El segundo más utilizado es tests/third_party)

Sí, el guiño fue a cuánto se usa en C en general.

  1. Las construcciones de nivel de origen no son lo mismo que las instrucciones de código de bytes

Por supuesto, lo que estamos discutiendo es una preocupación interna que afecta las construcciones a nivel de fuente. Eso es parte de la frustración, la caja negra no debería filtrar sus preocupaciones.

  1. Emscripten no tiene el mismo nivel de abstracción que el estándar wasm, por lo que no debería reconsiderarse sobre esa base.

El punto era que encontrará goto s en la mayoría de los proyectos C importantes, incluso dentro de la cadena de herramientas de WebAssembly en general. Un objetivo de compilador portátil para lenguajes en general que no es lo suficientemente expresivo como para apuntar a sus propios compiladores no es exactamente consistente con la naturaleza de nuestra empresa.

Específicamente, hoy podría ser útil reescribir los gotos fuera de libc, porque entonces tendríamos más control sobre el cfg resultante que confiar en relooper/cfgstackify para manejarlo bien.

Esto es circular. Muchos de los anteriores han planteado serias preguntas sin respuesta con respecto a la infalibilidad de tal requisito.

No lo hemos hecho porque no es una cantidad de trabajo trivial terminar con un código muy divergente de musl aguas arriba.

Es posible eliminar gotos, como dijiste, ¡ es una cantidad de trabajo no trivial ! ¿Está sugiriendo que todos los demás deberían divergir salvajemente en las rutas de código porque los gotos no deberían ser compatibles?

Los desarrolladores de Emscripten (la última vez que lo comprobé) tienden a opinar que una estructura similar a Goto sería realmente agradable, por esas razones obvias, por lo que es poco probable que la descarten, incluso si lleva años alcanzar un compromiso aceptable.

¡Un rayo de esperanza! Estaría satisfecho si el soporte goto/label se tomara en serio con un elemento de la hoja de ruta + una invitación oficial para poner la pelota en movimiento, incluso si faltan años.

Esta es una declaración particularmente estúpida.

Estás bien. Perdona la hipérbole, estoy un poco frustrado. Me encanta wasm y lo uso con frecuencia, pero finalmente veo un camino de dolor frente a mí si quiero hacer algo digno de mención con él, como port TCC. Después de leer todos los comentarios y artículos, sigo sin saber si la oposición es técnica, filosófica o política. Como expresó @neelance ,

“¿Podría alguien a cargo de V8 confirmar en este hilo que la oposición contra el flujo de control irreducible no está influenciada por la implementación actual de V8?

Lo pregunto porque esto es lo que más me molesta. [...]

Si escuchan algo útil, tomen en serio los comentarios de @neelance con respecto a Go 1.11. Eso es difícil de discutir. Claro, todos podemos hacer el desempolvado no trivial de goto, pero aun así, recibimos un serio golpe de rendimiento que solo se puede solucionar con una instrucción goto.

Nuevamente, perdone mi frustración, pero si este problema se cierra sin la dirección adecuada, me temo que enviará el tipo de señal equivocada que solo exasperará este tipo de respuestas de la comunidad y es inapropiado para uno de los mayores esfuerzos de estándares de nuestro campo. No hace falta decir que soy un gran fanático y partidario de todos en este equipo. ¡Gracias!

Aquí hay otro problema del mundo real causado por la falta de goto/funclets: https://github.com/golang/go/issues/42979

Para este programa, el compilador Go genera actualmente un binario wasm con 18 000 block anidados. El binario wasm en sí tiene un tamaño de 2,7 MB, pero cuando lo ejecuto a través de wasm2wat obtengo un archivo .wat de 4,7 GB. 🤯

Podría intentar darle al compilador Go algo heurístico para que, en lugar de una sola tabla de salto enorme, pudiera crear algún tipo de árbol binario y luego mirar la variable de destino de salto varias veces. Pero, ¿realmente es así como se supone que debe ser con wasm?

Me gustaría agregar que me parece extraño cómo la gente parece pensar que está perfectamente bien si solo un solo compilador (Emscripten[1]) puede admitir WebAssembly de manera realista.
Me recuerda un poco a la situación de libopus (un estándar que normativamente depende del código protegido por derechos de autor).

También me parece extraño cómo los desarrolladores de WebAssembly parecen estar tan vehementemente en contra de esto, a pesar de que casi todos, desde el final del compilador, les dicen que es necesario. Recuerde: WebAssembly es un estándar, no un manifiesto. Y el hecho es que la mayoría de los compiladores modernos usan alguna forma de bloques básicos SSA + internamente (o algo casi equivalente, con las mismas propiedades), que no tienen el concepto de bucles explícitos[2]. Incluso los JIT usan algo similar, así de común es.
El requisito absoluto para que el rebucle suceda sin la vía de escape de "simplemente use goto" es, que yo sepa[3], sin precedentes fuera de los traductores de idioma a idioma --- e incluso entonces, solo los traductores de idioma a idioma que apuntar a idiomas sin goto. En particular, nunca he oído que esto deba hacerse para ningún tipo de IR o código de bytes, que no sea WebAssembly.

Tal vez sea hora de cambiar el nombre de WebAssembly a WebEmscripten (¿WebScripten?).

Como dijo @d4tocchini , si no fuera por el estado de monopolio de WebAssembly (necesario, debido a la situación de estandarización), probablemente ya se habría bifurcado, en algo que pueda respaldar razonablemente lo que los desarrolladores del compilador ya saben que necesita admitir.
Y no, "simplemente use emscripten" no es un contraargumento válido, porque hace que el estándar dependa de un solo proveedor de compilador. Espero no tener que decirte por qué eso es malo.

EDITAR: Olvidé agregar una cosa:
Todavía no has aclarado si el tema es técnico, filosófico o político. Sospecho que esto último, pero con gusto se demostrará que está equivocado (porque los problemas técnicos y filosóficos se pueden solucionar mucho más fácilmente que los políticos).

Aquí hay otro problema del mundo real causado por la falta de goto/funclets: golang/go#42979

Para este programa, el compilador Go genera actualmente un binario wasm con 18 000 block anidados. El binario wasm en sí tiene un tamaño de 2,7 MB, pero cuando lo ejecuto a través de wasm2wat obtengo un archivo .wat de 4,7 GB. 🤯

Podría intentar darle al compilador Go algo heurístico para que, en lugar de una sola tabla de salto enorme, pudiera crear algún tipo de árbol binario y luego mirar la variable de destino de salto varias veces. Pero, ¿realmente es así como se supone que debe ser con wasm?

Este ejemplo es realmente interesante. ¿Cómo un programa de línea recta tan simple genera este código? ¿Cuál es la relación entre el número de elementos del arreglo y el número de bloques? En particular, ¿debería interpretar esto en el sentido de que el acceso a cada elemento de la matriz requiere que se compilen fielmente _múltiples_ bloques?

Y no, "simplemente use emscripten" no es un contraargumento válido

Creo que el verdadero contraargumento en este sentido sería que otro compilador que quiera apuntar a Wasm puede/debe implementar su propio algoritmo tipo relooper. Personalmente, creo que Wasm eventualmente debería tener un bucle de múltiples cuerpos (cercano a los funclets) o algo similar que sea un objetivo natural para goto .

@conrad-watt Hay varios factores que hacen que cada asignación use varios bloques básicos en el CFG. Uno de ellos es que hay una verificación de longitud en el segmento porque la longitud no se conoce en tiempo de compilación. En general, diría que los compiladores consideran los bloques básicos como una construcción relativamente barata, pero con wasm son algo caros, especialmente en este caso particular.

@neelance en el ejemplo modificado donde el código se divide entre varias funciones, se muestra que la sobrecarga de memoria (tiempo de ejecución/compilación) es mucho menor. ¿Se generan menos bloques en este caso, o es solo que las funciones separadas significan que el GC del motor puede ser más granular?

@conrad-watt Ni siquiera es el código Go el que usa la memoria, sino el host WebAssembly: cuando instalo el binario wasm con Chrome 86, mi CPU va al 100% durante 2 minutos y el uso de memoria de la pestaña alcanza su punto máximo en 11,3 GB. Esto es antes de que se ejecute el código wasm binary/Go. Es la forma del binario wasm lo que está causando el problema.

Eso ya era mi entendimiento. Esperaría que una gran cantidad de bloques/anotaciones de tipo causen una sobrecarga de memoria específicamente durante la compilación/creación de instancias.

Para tratar de eliminar la ambigüedad de mi pregunta anterior: si la versión dividida del código se compila en Wasm con menos bloques (debido a alguna peculiaridad de relooper), esa sería una explicación para la sobrecarga de memoria reducida y sería una buena motivación para agregar más general flujo de control a Wasm.

Alternativamente, puede ser que el código dividido resulte (aproximadamente) en la misma cantidad total de bloques, pero debido a que cada función se compila JIT por separado, los metadatos/IR utilizados para compilar cada función pueden ser analizados con mayor entusiasmo por el motor Wasm. . Un problema similar ocurrió en V8 hace años al analizar/compilar funciones grandes de asm.js. En este caso, la introducción de un flujo de control más general en Wasm no resolvería el problema.

En primer lugar, me gustaría aclarar: el compilador Go no utiliza el algoritmo relooper porque es intrínsecamente incompatible con el concepto de cambio de rutinas gor. Todos los bloques básicos se expresan a través de una tabla de saltos con un poco de caída cuando sea posible.

Supongo que hay un crecimiento exponencial de la complejidad en el tiempo de ejecución de wasm de Chrome con respecto a la profundidad de los block anidados. La versión dividida tiene el mismo número de bloques pero una profundidad máxima más pequeña.

En este caso, la introducción de un flujo de control más general en Wasm no resolvería el problema.

Estoy de acuerdo en que este problema de complejidad probablemente se pueda resolver al final de Chrome. Pero siempre me gusta hacer la pregunta "¿Por qué existe este problema en primer lugar?". Yo diría que con un flujo de control más general, este problema nunca habría existido. Además, todavía existe una sobrecarga de rendimiento general significativa debido a que todos los bloques básicos se expresan como tablas de salto, lo que creo que es poco probable que desaparezca mediante la optimización.

Supongo que hay un crecimiento exponencial de la complejidad en el tiempo de ejecución de wasm de Chrome con respecto a la profundidad de los bloques anidados. La versión dividida tiene el mismo número de bloques pero una profundidad máxima más pequeña.

¿Significa esto que en una función de línea recta con N accesos a la matriz, el acceso final a la matriz estará anidado (algún factor constante de) N bloques de profundidad? Si es así, ¿hay alguna manera de reducir esto al factorizar el código de manejo de errores de manera diferente? Espero que cualquier compilador se trague si tiene que analizar 3000 bucles anidados (una analogía muy aproximada), por lo que si esto es inevitable por razones semánticas, también sería un argumento para un flujo de control más general.

Si la diferencia de anidamiento es menos marcada que eso, mi corazonada sería que V8 casi no hace GC de metadatos _durante_ la compilación de una sola función Wasm, por lo que incluso si tuviéramos algo así como una propuesta de funclets modificada en el lenguaje desde el principio , los mismos gastos generales seguirían siendo visibles sin que hicieran una optimización de GC interesante.

Además, todavía existe una sobrecarga de rendimiento general significativa debido a que todos los bloques básicos se expresan como tablas de salto, lo que creo que es poco probable que desaparezca mediante la optimización.

De acuerdo en que es claramente preferible (desde un punto de vista puramente técnico) tener un objetivo más natural aquí.

¿Significa esto que en una función de línea recta con N accesos a la matriz, el acceso final a la matriz estará anidado (algún factor constante de) N bloques de profundidad? Si es así, ¿hay alguna manera de reducir esto al factorizar el código de manejo de errores de manera diferente? Espero que cualquier compilador se trague si tiene que analizar 3000 bucles anidados (una analogía muy aproximada), por lo que si esto es inevitable por razones semánticas, también sería un argumento para un flujo de control más general.

Al revés: la primera asignación está anidada tan profundamente, no la última. block anidados y un solo br_table en la parte superior es cómo se expresa una declaración tradicional de switch en wasm. Esta es la tabla de salto que mencioné. No hay 3000 bucles anidados.

Si la diferencia de anidamiento es menos marcada que eso, mi corazonada sería que V8 casi no hace GC de metadatos durante la compilación de una sola función Wasm, por lo que incluso si tuviéramos algo así como una propuesta de funclets modificada en el lenguaje desde el principio , los mismos gastos generales seguirían siendo visibles sin que hicieran una optimización de GC interesante.

Sí, también puede haber alguna implementación que tenga una complejidad exponencial con respecto a la cantidad de bloques básicos. Pero manejar bloques básicos (incluso en una gran cantidad) es lo que hacen muchos compiladores todo el día. Por ejemplo, el propio compilador de Go maneja fácilmente esta cantidad de bloques básicos durante su compilación, aunque se procesen mediante varios pases de optimización.

Sí, también puede haber alguna implementación que tenga una complejidad exponencial con respecto a la cantidad de bloques básicos. Pero manejar bloques básicos (incluso en una gran cantidad) es lo que hacen muchos compiladores todo el día. Por ejemplo, el propio compilador de Go maneja fácilmente esta cantidad de bloques básicos durante su compilación, aunque se procesen mediante varios pases de optimización.

Claro, pero un problema de rendimiento aquí sería ortogonal a cómo se expresa el flujo de control entre esos bloques básicos en el idioma de origen original (es decir, no es una motivación para un flujo de control más general en Wasm). Para ver si V8 es particularmente malo aquí, uno podría verificar si FireFox/SpiderMonkey o Lucet/Cranelift exhiben los mismos gastos generales de compilación.

He hecho algunas pruebas más: Firefox y Safari no muestran ningún problema. Curiosamente, Chrome incluso puede ejecutar el código antes de que finalice el proceso intensivo, por lo que parece que alguna tarea que no es estrictamente necesaria para ejecutar el binario wasm tiene un problema de complejidad.

Claro, pero un problema de rendimiento aquí sería ortogonal a cómo se expresa el flujo de control entre esos bloques básicos en el idioma fuente original.

Entiendo tu argumento.

Sigo creyendo que representar bloques básicos no a través de instrucciones de salto sino a través de una variable de salto y una tabla de salto enorme/bloques anidados es expresar el concepto simple de bloques básicos de una manera bastante compleja. Esto genera una sobrecarga de rendimiento y un riesgo de problemas de complejidad como el que vimos aquí. Creo que los sistemas más simples son mejores y más robustos que los sistemas complejos. Todavía no he visto argumentos que me convenzan de que el sistema más simple es una mala elección. Solo escuché que V8 tendría dificultades para implementar un flujo de control arbitrario y mi pregunta abierta para decirme que esta declaración es incorrecta (https://github.com/WebAssembly/design/issues/796#issuecomment-623431527) no ha sido respondido todavía.

@neelance

Chrome incluso puede ejecutar el código antes de que finalice el proceso intensivo.

Parece que el compilador de referencia Liftoff está bien, y el problema está en el compilador de optimización TurboFan. Presente un problema o proporcione un caso de prueba y puedo presentar uno si lo prefiere.

En términos más generales: ¿Cree que los planes de cambio de pila de wasm podrán resolver los problemas de implementación de goroutine de Go? Ese es el mejor vínculo que puedo encontrar, pero ahora está bastante activo, con una reunión quincenal y varios casos de uso sólidos que motivan el trabajo. Si Go puede usar corrutinas wasm para evitar el patrón de cambio grande, entonces creo que no serían necesarios gotos arbitrarios.

El compilador Go no usa el algoritmo relooper porque es intrínsecamente incompatible con el concepto de cambiar rutinas gor.

Es cierto que no se puede aplicar por sí mismo. Sin embargo, tenemos buenos resultados con el uso de flujo de control estructurado wasm + Asyncify . La idea allí es emitir el flujo de control de wasm normal tanto como sea posible (ifs, bucles, etc., sin un solo interruptor grande) y agregar instrumentación encima de ese patrón para manejar el desenrollado y rebobinado de la pila. Esto conduce a un tamaño de código bastante pequeño, y el código de conmutación que no es de pila puede ejecutarse básicamente a toda velocidad, mientras que un cambio de pila real puede ser un poco más lento (por lo que esto es bueno para el caso en que los cambios de pila no ocurren constantemente en cada iteración de bucle, etc. .).

¡Estaría muy feliz de experimentar con eso en Go, si estás interesado! Obviamente, esto no sería tan bueno como el soporte de cambio de pila integrado en wasm, pero podría ser mejor que el patrón de cambio grande que ya existe. Y sería más fácil cambiar al soporte de cambio de pila incorporado más adelante. Concretamente, la forma en que podría funcionar este experimento es hacer que Go emita un código estructurado normalmente, sin preocuparse en absoluto por el cambio de pila, y simplemente emita una llamada a una función especial maybe_switch_goroutine en los puntos apropiados. La transformación Asyncify se encargaría de todo el resto básicamente.

Estoy interesado en gotos para emuladores de recompilación dinámica como qemu. A diferencia de otros compiladores, qemu en ningún momento tiene conocimiento de la estructura de flujo de control del programa, por lo que los gotos son el único objetivo razonable. Tailcalls podría solucionar esto compilando cada bloque como una función y cada goto como tailcall.

@kripken Gracias por su publicación tan útil.

Parece que el compilador de referencia Liftoff está bien, y el problema está en el compilador de optimización TurboFan. Presente un problema o proporcione un caso de prueba y puedo presentar uno si lo prefiere.

Aquí hay un binario wasm que puede ejecutar con wasm_exec.html .

¿Cree que los planes de cambio de pila de wasm podrán resolver los problemas de implementación de goroutine de Go?

Sí, a primera vista parece que esto ayudaría.

Sin embargo, tenemos buenos resultados con el uso de flujo de control estructurado wasm + Asyncify.

Esto también parece prometedor. Tendríamos que implementar el relooper en Go, pero supongo que está bien. Una pequeña desventaja es que agrega una dependencia a binaryen para producir binarios wasm. Probablemente escribiré una propuesta pronto.

Creo que el algoritmo del apilador de LLVM es más fácil/mejor, en caso de que quiera implementarlo: https://medium.com/leaningtech/resolving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2

Presenté una propuesta para el proyecto Go: https://github.com/golang/go/issues/43033

@neelance , es bueno ver que la sugerencia de @kripken ayuda un poco con golang + wasm. Teniendo en cuenta que este problema es uno de goto/labels, no el cambio de pila, y dado que Asyncify presenta nuevas dependencias/construcciones de carcasas especiales con Asyncify hasta que se lanza el cambio de pila, etc., ¿caracterizaría esto como una solución o una mitigación menos que óptima? ¿Cómo se compara esto con los beneficios estimados si las instrucciones Goto estuvieran disponibles?

Si el argumento del “buen gusto” de Linus Torvalds para las listas enlazadas se basa en la elegancia de eliminar una sola declaración de rama de caso especial, es difícil ver este tipo de gimnasia de caso especial como una victoria o incluso un paso en la dirección correcta. Habiendo usado gotos personalmente para apis similares a async en C, para hablar sobre el cambio de pila antes de que las instrucciones goto activen todo tipo de olores.

Por favor, corríjame si estoy leyendo mal, pero aparte de respuestas aparentemente rápidas centradas en particularidades marginales a algunas preguntas planteadas, parece que los mantenedores aquí no han ofrecido ninguna claridad sobre el asunto en cuestión ni han respondido las preguntas difíciles. Con el debido respeto, ¿no es esta lenta osificación el sello distintivo de la callus política corporativa? Si este es el caso, entiendo la difícil situación... ¡Imagínese todos los lenguajes/compiladores que la marca de Wasm podría presumir de admitir si solo ANSI C fuera una prueba de fuego de compatibilidad!

@neelance @darkuranium @d4tocchini no todos los colaboradores de Wasm piensan que la falta de gotos es lo correcto, de hecho, personalmente lo calificaría como el error de diseño número 1 de Wasm. Estoy absolutamente a favor de agregarlo (ya sea como funclets o directamente).

Sin embargo, debatir en este hilo no hará que los gotos sucedan, y no hará que todos los involucrados en Wasm se convenzan mágicamente y hagan el trabajo por usted. Estos son los pasos a seguir:

  1. Únete a Wasm CG.
  2. Alguien invierte el tiempo para convertirse en campeón de una propuesta goto. Recomiendo comenzar con la propuesta de funclets existente, ya que @sunfishcode ya lo ha pensado bien para que sea el "menos intrusivo" para los motores y herramientas actuales que se basan en la estructura de bloques, por lo que tiene más posibilidades de éxito que un raw ir.
  3. Ayúdelo a ser empujado a través de las 4 etapas de la propuesta. Esto incluye hacer buenos diseños para cualquier objeción que se presente en tu camino, iniciar discusiones, con el objetivo de hacer felices a suficientes personas de modo que obtengas la mayoría de votos al avanzar a través de las etapas.

@ d4tocchini Honestamente, actualmente veo las soluciones sugeridas como "la mejor manera de avanzar dadas las circunstancias que no puedo cambiar", también conocido como "solución alternativa". Todavía considero las instrucciones jump/goto (o funclets) como la forma más simple y, por lo tanto, preferible. (Sin embargo, gracias a @kripken por sugerir amablemente las alternativas).

@aardappel Por lo que sé, @sunfishcode intentó impulsar la propuesta de funclets y fracasó. ¿Por qué sería diferente para mí?

@neelance No creo que @sunfishcode haya tenido mucho tiempo para impulsar la propuesta más allá de su creación inicial, por lo que está "estancada" en lugar de "fallida". Como estaba tratando de indicar, se requiere que un campeón haga un trabajo continuo para que una propuesta llegue hasta el final del proceso.

@neelance

¡Gracias por el caso de prueba! Puedo confirmar el mismo problema localmente. Presenté https://bugs.chromium.org/p/v8/issues/detail?id=11237

Necesitaríamos implementar el relooper en Go [..] Un pequeño inconveniente es que agrega una dependencia a binaryen para producir binarios wasm.

Por cierto, si ayudara, podemos crear una compilación de biblioteca de binaryen como un solo archivo C. ¿Quizás eso es más fácil de integrar?

Además, usando Binaryen puedes usar la implementación de Relooper que está ahí . Puede pasarle bloques básicos de IR y dejar que haga el rebucle.

@taralx

Creo que el algoritmo de apilamiento de LLVM es más fácil/mejor,

Tenga en cuenta que ese enlace no se trata de LLVM ascendente, ese es el compilador Cheerp (que es una bifurcación de LLVM). Su Stackifier tiene un nombre similar al de LLVM, pero es diferente.

Tenga en cuenta también que esa publicación de Cheerp se refiere al algoritmo original de 2011: la implementación moderna de relooper (como se mencionó anteriormente) no ha tenido los problemas que mencionan durante muchos años. No conozco una alternativa más simple o mejor a ese enfoque general, que es muy similar a lo que hacen Cheerp y otros: estas son variaciones sobre un tema.

@kripken Gracias por archivar el problema.

Por cierto, si ayudara, podemos crear una compilación de biblioteca de binaryen como un solo archivo C. ¿Quizás eso es más fácil de integrar?

Improbable. El compilador Go en sí mismo se convirtió a Go puro hace un tiempo y, afaik, no usa otras dependencias de C. No creo que esta sea una excepción.

Este es el estado actual de la propuesta de funclets: el siguiente paso en el proceso es solicitar una votación del CG para ingresar a la etapa 1.

Yo mismo estoy actualmente enfocado en otras áreas en WebAssembly y no tengo el ancho de banda para impulsar los funclets; si alguien está interesado en asumir el rol de Campeón de los funclets, estaré encantado de dárselo.

Improbable. El compilador Go en sí mismo se convirtió a Go puro hace un tiempo y, afaik, no usa otras dependencias de C. No creo que esta sea una excepción.

Además, esto no resuelve el problema del uso extensivo de relooper que causa serios desniveles de rendimiento en los tiempos de ejecución de WebAssembly.

@Vurich

Creo que ese podría ser el mejor caso para agregar gotos a wasm, pero alguien necesitaría recopilar datos convincentes del código del mundo real que muestren acantilados de rendimiento tan serios. Yo mismo no he visto tales datos. El trabajo que analiza los déficits de rendimiento de wasm como no son debido al flujo de control estructurado, más bien se debe a controles de seguridad).

@kripken ¿Tiene alguna sugerencia sobre cómo se podrían recopilar dichos datos? ¿Cómo se demostraría que un déficit de rendimiento se debe a un flujo de control estructurado?

Es poco probable que haya mucho trabajo analizando el rendimiento de la etapa de compilación, que es parte de la queja aquí.

Estoy un poco sorprendido de que aún no tengamos una construcción de caso de cambio, pero los funclets subsumen eso.

@neelance

No es fácil averiguar las causas específicas, sí. Por ejemplo, para verificaciones de límites, puede deshabilitarlas en la VM y medir eso, pero lamentablemente no hay una manera simple de hacer lo mismo para gotos.

Una opción es comparar el código de máquina emitido a mano, que es lo que hicieron en ese documento vinculado.

Otra opción es compilar el wasm en algo que crea que puede manejar el flujo de control de manera óptima, es decir, "deshacer" la estructuración. LLVM debería poder hacer eso, por lo que podría ser interesante ejecutar wasm en una máquina virtual que usa LLVM (como WAVM o wasmer) o a través de WasmBoxC. Quizás podría deshabilitar las optimizaciones de CFG en LLVM y ver cuánto importa.

@taralx

Interesante, ¿me perdí algo sobre los tiempos de compilación o el uso de memoria? El flujo de control estructurado debería ser mejor allí, por ejemplo, es muy simple ir al formulario SSA desde allí, en comparación con un CFG general. Esta fue, de hecho, una de las razones por las que wasm eligió el flujo de control estructurado en primer lugar. Eso también se mide con mucho cuidado porque afecta los tiempos de carga en la Web.

(¿O se refiere al rendimiento del compilador en la máquina del desarrollador? Es cierto que wasm se inclina por hacer más trabajo allí y menos en el cliente).

Me refiero a compilar el rendimiento en el integrador, pero parece que eso se está tratando como un error , no necesariamente como un problema de rendimiento puro.

@taralx

Sí, creo que eso es un error. Simplemente sucede en un nivel en una máquina virtual. Y no hay una razón fundamental para ello: el flujo de control estructurado no requiere más recursos, debería requerir menos. Es decir, apostaría a que tales errores de rendimiento serían más probables si wasm tuviera gotos.

@kripken

El flujo de control estructurado debería ser mejor allí, por ejemplo, es muy simple ir al formulario SSA desde allí, en comparación con un CFG general. Esta fue, de hecho, una de las razones por las que wasm eligió el flujo de control estructurado en primer lugar. Eso también se mide con mucho cuidado porque afecta los tiempos de carga en la Web.

Una pregunta muy específica, por si acaso: ¿Conoce algún compilador Wasm que realmente haga eso? "Muy simple" pasando de "flujo de control estructurado" a formulario SSA. Porque a simple vista, el flujo de control de Wasm no está tan estructurado (totalmente/en última instancia). El control formalmente estructurado es aquel en el que no hay break s, continue s, return s (más o menos, el modelo de programación de Scheme, sin magia como call/cc). Cuando están presentes, dicho flujo de control se puede denominar aproximadamente "semiestructurado".

Hay un algoritmo SSA bien conocido para el flujo de control completamente estructurado: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4503 . Esto es lo que tiene que decir sobre el flujo de control semiestructurado:

Para declaraciones estructuradas, hemos mostrado cómo generar tanto el formulario SSA como el árbol dominador en un solo paso durante el análisis. En la siguiente sección mostraremos que incluso es posible extender nuestro método a cierta clase de instrucciones no estructuradas (LOOP/EXIT y RETURN) que pueden causar salidas de estructuras de control en puntos arbitrarios. Sin embargo, dado que tales salidas son una especie de ir (disciplinado), no sorprende que sean mucho más difíciles de manejar que las declaraciones estructuradas.

OTOH, hay otro algoritmo bien conocido, https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf que podría decirse que también es de un solo paso, pero no tiene problemas no solo con el control no estructurado. flujo, pero incluso con flujo de control irreducible (aunque no produce un resultado óptimo para ello).

Entonces, la pregunta es nuevamente si sabe que algún proyecto se tomó la molestia de extender el algoritmo de Brandis/Mössenböck y logró beneficios tangibles en esa ruta en comparación con Braun et al. algoritmo (como nota al margen, mi corazonada intuitiva es que Braun algo es exactamente una extensión de "límite superior", aunque soy demasiado tonto para demostrármelo intuitivamente a mí mismo, sin hablar de una prueba formal, así que eso es todo - corazonada intuitiva ).

Y el tema general de la pregunta es establecer (aunque yo diría "mantener") la razón última de por qué Wasm optó por no recibir el apoyo arbitrario de goto. Porque viendo este hilo durante años, el modelo mental que construí es que está hecho para evitar enfrentar CFG irreducibles . Y, de hecho, el abismo se encuentra entre los CFG reducibles e irreducibles , con muchos algoritmos de optimización que son (mucho) más fáciles para los CFG reducibles , y eso es lo que han codificado muchos optimizadores. El flujo de control (semi)estructurado en Wasm es solo una forma económica de garantizar reducibilidad

La mención de cualquier facilidad especial de producción de SSA para CFG estructurados (y los CFG de Wasm realmente no parecen estar estructurados en el sentido formal) de alguna manera nubla la imagen clara anterior. Por eso pregunto si hay referencias específicas de que la construcción de SSA se ve prácticamente beneficiada por el formulario Wasm CFG.

Gracias.

@kripken Estoy un poco confundido en este momento y ansioso por aprender. Estoy mirando la situación y actualmente veo lo siguiente:


La fuente de su programa tiene un cierto flujo de control. Este CFG es reducible o no, por ejemplo, goto se ha utilizado en el idioma de origen o no. No hay manera de cambiar este hecho. Este CFG se puede convertir en código de máquina, por ejemplo, como lo hace el compilador Go de forma nativa.

Si el CFG ya es reducible, entonces todo está bien y la máquina virtual wasm puede cargarlo rápidamente. Cualquier pase de traducción debería poder detectar que este es el caso simple y hacer lo rápido. Permitir CFG irreducibles no debería ralentizar este caso.

Si el CFG no es reducible, entonces hay dos opciones:

  • El compilador lo hace reducible, por ejemplo, introduciendo una tabla de salto. Este paso pierde información. Es difícil restaurar el CFG original sin tener un análisis específico del compilador que produjo el binario. Debido a esta pérdida de información, cualquier código de máquina generado será algo más lento que el código generado a partir de la CFG inicial. Es posible que podamos generar este código de máquina con un algoritmo de un solo paso, pero es a costa de la pérdida de información. [1]

  • Permitimos que el compilador emita un CFG irreducible. Es posible que la máquina virtual tenga que hacerlo reducible. Esto ralentiza el tiempo de carga, pero solo en los casos en que el CFG no es realmente reducible. El compilador tiene la opción de elegir entre optimizar el rendimiento del tiempo de carga o el rendimiento del tiempo de ejecución.

[1] Soy consciente de que no es realmente una pérdida de información si todavía hay alguna forma de revertir la operación, pero no podría describirlo de mejor manera.


¿Dónde está la falla en mi pensamiento?

@falcon

¿Conoce algún compilador Wasm que realmente haga eso: "muy simple" pasando de "flujo de control estructurado" a forma SSA?

Acerca de las máquinas virtuales: no lo sé directamente. Pero IIRC en el pasado , @lukewagner dijeron que era conveniente implementarlo de esa manera, tal vez uno de ellos pueda dar más detalles. No estoy seguro de si la irreductibilidad era todo el problema allí o no. Y no estoy seguro de si han implementado esos algoritmos que mencionas o no.

Acerca de otras cosas además de las máquinas virtuales: el optimizador Binaryen definitivamente se beneficia del flujo de control estructurado de wasm, y no solo porque es reducible. Varias optimizaciones son más simples porque siempre sabemos dónde están los encabezados de bucle, por ejemplo, que están anotados en el archivo wasm. (OTOH otras optimizaciones son más difíciles de hacer, y también tenemos un CFG IR general para esos...)

@neelance

Si el CFG ya es reducible, entonces todo está bien y la máquina virtual wasm puede cargarlo rápidamente. Cualquier pase de traducción debería poder detectar que este es el caso simple y hacer lo rápido. Permitir CFG irreducibles no debería ralentizar este caso.

Tal vez no te estoy entendiendo del todo. Pero que una máquina virtual wasm pueda cargar código rápidamente depende no solo de si es reducible o no, sino de cómo está codificado. Específicamente, podríamos haber imaginado un formato que sea un CFG general, y luego la máquina virtual debe hacer el trabajo para verificar que sea reducible. Wasm optó por evitar ese trabajo: la codificación es necesariamente reducible (es decir, a medida que lee el wasm y realiza la validación trivial, también está demostrando que es reducible sin hacer ningún trabajo adicional).

Además, la codificación de wasm no solo brinda una garantía de reducibilidad sin necesidad de verificar eso. También anota encabezados de bucle, ifs y otras cosas útiles (como mencioné por separado anteriormente en este comentario). No estoy seguro de cuánto se benefician las máquinas virtuales de producción de eso, pero espero que lo hagan. (¿Quizás especialmente en los compiladores de referencia?)

En general, creo que permitir CFG irreducibles puede ralentizar el caso rápido, a menos que los irreducibles se codifiquen de manera separada (como se proponen los funclets).

@kripken

Gracias por tu explicación.

Sí, esta es exactamente la diferenciación que estoy tratando de hacer: veo la ventaja de la notación/codificación estructurada para el caso de CFG reducible. Pero no debería ser difícil agregar alguna construcción que permita la notación de un CFG irreducible y aún así mantener las ventajas existentes en el caso de un CFG de fuente reducible (por ejemplo, si no usa esta nueva construcción, entonces el CFG está garantizado ser reducible).

Como conclusión, no veo cómo se puede argumentar que una notación puramente reducible es más rápida. En el caso de una fuente reducible CFG es igualmente rápido. Y en el caso de una fuente irreducible CFG , como máximo se puede argumentar que no es significativamente más lento, pero algunos casos del mundo real ya han demostrado que es poco probable que sea el caso en general.

En resumen, no veo cómo las consideraciones de rendimiento pueden ser un argumento que impide el flujo de control irreducible y eso me hace preguntarme por qué el siguiente paso debe ser recopilar datos de rendimiento.

@neelance

Sí, estoy de acuerdo en que podríamos agregar una nueva construcción, como funclets, y al no usarla, no ralentizaría el caso existente.

Pero hay una desventaja en agregar cualquier construcción nueva, ya que agrega complejidad a wasm. En particular, significa un área de superficie más grande en las máquinas virtuales, lo que significa más posibles errores y problemas de seguridad. Wasm se ha inclinado por tener tanta complejidad del lado del desarrollador como sea posible para reducir la complejidad en la máquina virtual.

Algunas propuestas de wasm no se tratan solo de velocidad, como GC (que permite la recopilación de ciclos con JS). Pero para las propuestas que tienen que ver con la velocidad, como los funclets, debemos demostrar que la velocidad justifica la complejidad. Tuvimos este debate sobre SIMD, que también tiene que ver con la velocidad, y decidimos que valía la pena porque vimos que puede lograr de manera confiable aceleraciones muy grandes en el código del mundo real (2x o incluso más).

(Estoy de acuerdo, hay otros beneficios además de la velocidad al permitir CFG generales, como facilitar que los compiladores apunten a wasm. Pero podemos resolver eso sin agregar complejidad a las VM de wasm. Ya brindamos soporte para CFG arbitrarios en LLVM y Binaryen , lo que permite a los compiladores emitir CFG y no preocuparse por el flujo de control estructurado. Si eso no es lo suficientemente bueno, nosotros, la gente de herramientas, me refiero, incluyéndome a mí, deberíamos hacer más).

Funclet no se trata tanto de la velocidad como de permitir que los lenguajes con un flujo de control no trivial se compilen en WebAssembly, siendo C y Go los más obvios, pero se aplica a cualquier lenguaje que tenga async/await. Además, la elección de tener un flujo de control jerárquico en realidad conduce a _más_ errores en las máquinas virtuales, como lo demuestra el hecho de que todos los compiladores de Wasm, excepto V8, descomponen el flujo de control jerárquico en un CFG de todos modos. Los EBB en un CFG pueden representar las construcciones de flujo de control múltiple en Wasm y más, y tener una sola construcción para compilar conduce a muchos menos errores que tener muchos tipos diferentes con diferentes usos.

Incluso Lightbeam, un compilador de transmisión muy simple, vio una disminución masiva en los errores de compilación después de agregar un paso de traducción adicional que descompuso el flujo de control en un CFG. Esto se duplica para el otro lado de este proceso: Relooper es mucho más propenso a errores que emitir funclets, y los desarrolladores que trabajan en los backends de Wasm para LLVM y otros compiladores que eran funclets para implementar me han dicho que emitirían cada función usando funclets solo, para mejorar la confiabilidad y la simplicidad de codegen. Todos los compiladores que producen Wasm usan EBB, todos menos uno de los compiladores que consumen Wasm usan EBB, esta negativa a implementar funclets o alguna otra forma de representar CFG simplemente agrega un paso con pérdida en el medio que perjudica a todas las partes involucradas además del equipo V8 .

"El flujo de control irreducible considerado dañino" es simplemente un tema de conversación, puede agregar fácilmente la restricción de que el flujo de control de los funclets sea reducible y luego, si desea permitir un flujo de control irreducible en el futuro, todos los módulos Wasm existentes con flujo de control reducible funcionarían sin modificaciones. en un motor que además admite flujo de control irreducible. Sería simplemente un caso de eliminar la verificación de reducibilidad en el validador.

@Vurich

Puede agregar fácilmente la restricción de que el flujo de control de los funclets sea reducible

Puede, pero no es trivial: las máquinas virtuales deberían verificar eso. No creo que eso sea posible en un solo paso lineal, lo que sería un problema para los compiladores de referencia, que ahora están presentes en la mayoría de las máquinas virtuales. (De hecho, solo encontrar los bordes traseros de bucle, que es un problema más simple y necesario también por otras razones, no se puede hacer en un solo paso hacia adelante, ¿verdad?)

todos los compiladores de Wasm que no sean V8 descomponen el flujo de control jerárquico en un CFG de todos modos.

¿Se refiere al enfoque de "mar de nodos" que utiliza TurboFan? No soy un experto en eso, así que dejaré que otros respondan.

Pero, en términos más generales, incluso si no cree en el argumento anterior para optimizar los compiladores, es incluso más cierto para los compiladores básicos, como se mencionó anteriormente.

Los funclet no tienen tanto que ver con la velocidad como con permitir que los lenguajes con un flujo de control no trivial se compilen en WebAssembly [..] Relooper es mucho más propenso a errores que emitir funclets

Estoy 100% de acuerdo en el lado de las herramientas. ¡Es más difícil emitir código estructurado desde la mayoría de los compiladores! Pero el punto es que lo simplifica en el lado de la máquina virtual, y eso es lo que wasm eligió hacer. Pero, de nuevo, estoy de acuerdo en que esto tiene ventajas y desventajas, incluidas las desventajas que mencionaste.

¿Wasm se equivocó en esto en 2015? Es posible. Creo que nos equivocamos en algunas cosas (como la capacidad de depuración y el cambio tardío a una máquina de pila). Pero no es posible corregirlos en retrospectiva, y hay una barra alta para agregar cosas nuevas, especialmente las que se superponen.

Dado todo eso, tratando de ser constructivo, creo que deberíamos solucionar los problemas existentes en el lado de las herramientas. Hay una barra mucho, mucho más baja para los cambios de herramientas. Dos posibles sugerencias:

  • Puedo considerar la posibilidad de transferir el código Binaryen CFG a Go, si eso ayudaría al compilador Go - @neelance .
  • Podemos implementar funclets o algo parecido puramente en el lado de las herramientas. Es decir, proporcionamos código de biblioteca para esto hoy, pero también podríamos agregar un formato binario. (Ya existe un precedente para agregar al formato binario wasm en el lado de las herramientas, en los archivos de objetos wasm).

Podemos implementar funclets o algo parecido puramente en el lado de las herramientas. Es decir, proporcionamos código de biblioteca para esto hoy, pero también podríamos agregar un formato binario. (Ya existe un precedente para agregar al formato binario wasm en el lado de las herramientas, en los archivos de objetos wasm).

Si se ha realizado algún trabajo concreto sobre esto, vale la pena señalar que (AFAIU) la forma idiomática más pequeña de agregar esto a Wasm (como aludió @rossberg ) sería introducir la instrucción de bloque

multiloop (t in) _N_ t out (_instr_ final *) _N_

que define n cuerpos etiquetados (con n anotaciones de tipo de entrada declaradas hacia adelante). Luego, la familia de instrucciones br se generaliza para que todas las etiquetas definidas por el bucle múltiple estén dentro del alcance de cada cuerpo, en orden (como en, cualquier cuerpo puede ramificarse desde dentro de cualquier otro cuerpo). Cuando se ramifica un cuerpo de bucle múltiple , la ejecución salta al _inicio_ del cuerpo (como un bucle Wasm normal). Cuando la ejecución llega al final de un cuerpo sin bifurcarse a otro cuerpo, la construcción completa regresa (sin fallos).

Habría que hacer algunos ajustes para representar de manera eficiente las anotaciones de tipo de cada cuerpo (en la formulación anterior, n cuerpos pueden tener n tipos de entrada diferentes, pero todos deben tener el mismo tipo de salida, por lo que no puedo usar directamente índices regulares de _blocktype_ de valores múltiples sin requerir un cálculo LUB de sensación superflua), y cómo seleccionar el cuerpo inicial a ejecutar (siempre el primero, ¿o debería haber un parámetro estático?).

Esto consigue el mismo nivel de expresividad que los funclets pero evita tener que introducir un nuevo espacio de instrucciones de control. De hecho, si los funclets se hubieran iterado más, creo que se habría convertido en algo como esto.

EDITAR: ajustar esto para que tenga un comportamiento fallido complicaría marginalmente la semántica formal, pero probablemente sería mejor para el caso de uso de

El principio de diseño de Wasm de descargar trabajo en las herramientas para hacer que los motores sean más simples y rápidos es muy importante y seguirá siendo muy beneficioso.

Dicho esto, como todo lo que no es trivial, es una compensación, no blanco o negro. Creo que aquí tenemos un caso en el que el dolor para los productores es desproporcionado al dolor para los motores. La mayoría de los compiladores que nos gustaría traer a Wasm usan estructuras CFG arbitrarias internamente (SSA) o se usan para apuntar a cosas que no les importa gotos (CPU). Estamos haciendo que el mundo salte a través de aros sin mucha ganancia.

Algo como funclets (o multiloop) es bueno porque es modular: si un productor no lo necesita, las cosas funcionarán como antes. Si un motor realmente no puede lidiar con CFG arbitrarios, por el momento pueden emitirlo como si fuera un tipo de construcción loop + br_table , y solo aquellos que lo usan pagan el precio. . Entonces, “el mercado decide” y vemos si hay presión sobre los motores para que emitan mejor código para ello. Algo me dice que si va a haber una gran cantidad de código Wasm que se basa en funclets, realmente no va a ser un desastre tan grande para los motores emitir un buen código para ellos como algunas personas parecen pensar.

Puede, pero no es trivial: las máquinas virtuales deberían verificar eso. No creo que eso sea posible en un solo paso lineal, lo que sería un problema para los compiladores de referencia, que ahora están presentes en la mayoría de las máquinas virtuales.

Tal vez estoy malinterpretando las expectativas de un compilador de referencia, pero ¿por qué les importaría? Si ve un goto, inserte una instrucción de salto.

Estoy 100% de acuerdo en el lado de las herramientas. ¡Es más difícil emitir código estructurado desde la mayoría de los compiladores! Pero el punto es que lo simplifica en el lado de la máquina virtual, y eso es lo que wasm eligió hacer. Pero, de nuevo, estoy de acuerdo en que esto tiene ventajas y desventajas, incluidas las desventajas que mencionaste.

No, como digo varias veces en mi comentario original, _no_ facilita las cosas en el lado de la máquina virtual. Trabajé en un compilador de referencia durante más de un año y mi vida se hizo más fácil y el código emitido se volvió más rápido después de que agregué un paso intermedio que convirtió el flujo de control de Wasm en un CFG.

Puede, pero no es trivial: las máquinas virtuales deberían verificar eso. No creo que eso sea posible en un solo paso lineal, lo que sería un problema para los compiladores de referencia, que ahora están presentes en la mayoría de las máquinas virtuales. (De hecho, solo encontrar los bordes traseros de bucle, que es un problema más simple y necesario también por otras razones, no se puede hacer en un solo paso hacia adelante, ¿verdad?)

Ok, esta es la cuestión, mi conocimiento de los algoritmos utilizados en los compiladores no es lo suficientemente fuerte como para afirmar con absoluta certeza que el flujo de control irreducible puede o no detectarse en un compilador de transmisión, pero la cuestión es que no es necesario. La verificación puede ocurrir junto con la compilación. Si no existe un algoritmo de transmisión, que ni usted ni yo sabemos que no existe, puede usar un algoritmo que no sea de transmisión una vez que la función se haya recibido por completo. Si (por alguna razón) el flujo de control irreducible conduce a algo realmente malo como un bucle infinito, simplemente puede agotar el tiempo de espera de la compilación y/o cancelar el hilo de compilación. Sin embargo, no hay razón para creer que este sería el caso.

Tal vez estoy malinterpretando las expectativas de un compilador de referencia, pero ¿por qué les importaría? Si ve un goto, inserte una instrucción de salto.

No es tan simple debido a cómo necesita mapear la máquina de registro infinito de Wasm (no, no es una máquina de pila ) a los registros finitos del hardware físico, pero ese es un problema que cualquier compilador de transmisión debe resolver y es completamente ortogonal a CFGs vs flujo de control jerárquico.

El compilador de transmisión en el que trabajé puede compilar un CFG arbitrario, incluso irreducible, muy bien. No está haciendo nada particularmente especial. Simplemente asigne a cada bloque una "convención de llamada" (básicamente el lugar donde deberían estar los valores en el alcance de ese bloque) cuando necesite saltar a él por primera vez, y si alguna vez llega a un punto en el que necesita bifurcarse condicionalmente a dos o más objetivos con "convenciones de llamada" incompatibles, empuja un bloque "adaptador" a una cola y lo emite en el siguiente punto posible. Esto puede suceder tanto con el flujo de control reducible como con el irreducible, y casi nunca es necesario en ninguno de los dos casos. El argumento del "flujo de control irreducible considerado dañino", como dije antes, es un tema de conversación y no un argumento técnico. Representar el flujo de control como un CFG hace que los compiladores de transmisión sean mucho más fáciles de escribir y, como he dicho varias veces, lo sé por una amplia experiencia personal.

Cualquier caso en el que el flujo de control irreducible haga que las implementaciones sean más difíciles de escribir, de los cuales no se me ocurre ninguno, pueden simplemente apagarse y devolver un error, y si necesita un algoritmo separado que no transmita para detectar con 100% de certeza ese control el flujo es irreducible (por lo que no acepta accidentalmente el flujo de control irreducible), entonces eso puede ejecutarse por separado del compilador de referencia. Alguien que tengo razones para creer que es una autoridad en el tema me dijo que existe un algoritmo de transmisión relativamente simple. para detectar la irreductibilidad de un CFG, pero no puedo decir de primera mano que esto sea cierto.

@oridb

Tal vez estoy malinterpretando las expectativas de un compilador de referencia, pero ¿por qué les importaría? Si ve un goto, inserte una instrucción de salto.

Los compiladores de línea de base aún necesitan hacer cosas como insertar comprobaciones adicionales en los bordes posteriores del bucle (así es como en la Web una página colgante eventualmente mostrará un diálogo de script lento), por lo que necesitan identificar cosas como esa. Además, intentan hacer una asignación de registros razonablemente eficiente (los compiladores de línea de base a menudo se ejecutan a aproximadamente la mitad de la velocidad del compilador de optimización, lo cual es muy impresionante dado que son de un solo paso). Tener la estructura del flujo de control, incluidas las uniones y las divisiones, lo hace mucho más fácil.

@gwvo

Dicho esto, como todo lo que no es trivial, es una compensación, no blanco o negro. [..] Estamos haciendo que el mundo salte a través de aros sin mucha ganancia.

Totalmente de acuerdo en que es una compensación, e incluso tal vez Wasm se equivocó en ese entonces. Pero creo que es mucho más práctico arreglar esos aros del lado de las herramientas.

Entonces, “el mercado decide” y vemos si hay presión sobre los motores para que emitan mejor código para ello.

Esto es en realidad algo que hemos evitado hasta ahora. Hemos tratado de hacer que wasm sea lo más simple posible en la VM para que no requiera optimizaciones complejas, ni siquiera cosas como la inserción, tanto como sea posible. El objetivo es hacer el trabajo duro en el lado de las herramientas, no presionar a las máquinas virtuales para que lo hagan mejor.

@Vurich

Trabajé en un compilador de referencia durante más de un año y mi vida se hizo más fácil y el código emitido se volvió más rápido después de que agregué un paso intermedio que convirtió el flujo de control de Wasm en un CFG.

¡Muy interesante! ¿Qué máquina virtual fue esa?

También tendría curiosidad específica sobre si se trataba de un pase único/transmisión o no (si lo fuera, ¿cómo manejó la instrumentación de respaldo de bucle?), y cómo registra la asignación.

En principio, tanto los backedges de bucle como la asignación de registros se pueden manejar en función del orden de instrucción lineal, con la expectativa de que los bloques básicos se coloquen en un orden razonable similar al de topsort, sin requerirlo estrictamente.

For loop backedges: defina un backedge como una instrucción que salta a una etapa anterior en el flujo de instrucciones. En el peor de los casos, si los bloques se colocan al revés, se obtienen más comprobaciones de respaldo de las estrictamente necesarias.

Para la asignación de registros: esta es solo la asignación de registros de escaneo lineal estándar. El tiempo de vida de una variable para la asignación de registros se extiende desde la primera mención de la variable hasta la última mención, incluidos todos los bloques que se encuentran linealmente en el medio. En el peor de los casos, si los bloques se barajan, obtienes vidas más largas de lo necesario y, por lo tanto, derramas cosas innecesariamente en la pila. El único costo adicional es rastrear la primera y la última mención de cada variable, lo que se puede hacer para todas las variables con un solo escaneo lineal. (Para wasm, supongo que una "variable" es una ranura local o de pila).

@kripken

Puedo considerar la posibilidad de transferir el código Binaryen CFG a Go, si eso ayudaría al compilador Go - @neelance .

¿Para integrar Asyncify? Comente la propuesta .

@comex

¡Buenos puntos!

El único costo adicional es el seguimiento de la primera y última mención de cada variable

Sí, creo que es una diferencia significativa. La asignación de registro de exploración lineal es mejor (pero más lenta de hacer) que lo que hacen actualmente los compiladores de línea de base de wasm , ya que compilan en una forma de transmisión que es realmente rápida. Es decir, no hay un paso inicial para encontrar la última mención de cada variable: se compilan en un solo paso, emitiendo código a medida que avanzan sin siquiera ver el código más adelante en la función wasm, con la ayuda de la estructura, y también simplifican elecciones a medida que avanzan ("estúpido" es la palabra utilizada en esa publicación).

El enfoque de transmisión de V8 para la asignación de registros debería funcionar igual de bien si se permite que los bloques sean mutuamente recursivos (como en https://github.com/WebAssembly/design/issues/796#issuecomment-742690194), ya que las únicas vidas con las que tratan están enlazados dentro de un solo bloque (pila) o se supone que son de toda la función (local).

IIUC (con referencia a @titzer 's comentario ) el tema principal de V8 radica en el tipo de CFGs que Turboventilador puede optimizar.

@kripken

Hemos tratado de hacer que wasm sea lo más simple posible en la máquina virtual para que no requiera optimizaciones complejas.

Esto no es una "optimización compleja". Los gotos son increíblemente básicos y naturales para muchos sistemas. Apuesto a que hay muchos motores que podrían agregar esto sin costo alguno. Todo lo que digo es que si hay motores que quieren aferrarse a un modelo CFG estructurado por cualquier motivo, pueden hacerlo.

Por ejemplo, estoy bastante seguro de que LLVM (por mucho, nuestro productor de Wasm n.º 1 en la actualidad) no cambiará a usar funclets hasta que esté seguro de que no se trata de una regresión de rendimiento en los principales motores.

@kripken Es parte de Wasmtime. Sí, está transmitiendo y estaba destinado a tener una complejidad O (N), pero me mudé a una nueva empresa antes de que eso se realizara por completo, por lo que es solo "O (N)-ish". https://github.com/bytecodealliance/wasmtime/tree/main/crates/lightbeam

Gracias @Vurich , interesante. Sería genial ver los números de rendimiento cuando estén disponibles, especialmente para el inicio pero también para el rendimiento. Supongo que su enfoque se compilaría más lentamente que el enfoque adoptado por los ingenieros de V8 y SpiderMonkey, mientras emitía un código más rápido. Así que es una compensación diferente en este espacio. Parece plausible que su enfoque no se beneficie del flujo de control estructurado de wasm, como dijo, mientras que el de ellos sí.

No, es un compilador de transmisión y emite código más rápido que cualquiera de esos dos motores (aunque hay casos degenerados que no se solucionaron cuando dejé el proyecto). Si bien hice todo lo posible para emitir código rápido, está diseñado principalmente para emitir código rápidamente y la eficiencia de la salida es una preocupación secundaria. El costo de inicio es, que yo sepa, cero (por encima del costo inherente de Wasmtime que se comparte entre los backends) porque cada estructura de datos comienza sin inicializar y la compilación se realiza instrucción por instrucción. Si bien no tengo a mano números para comparar con V8 o SpiderMonkey, sí tengo números para comparar con Cranelift (el motor principal en wasmtime). Están varios meses desactualizados en este momento, pero puede ver que no solo emite código más rápido que Cranelift, sino que también emite código más rápido que Cranelift. En ese momento, también emitía un código más rápido que SpiderMonkey, aunque tendrás que creer en mi palabra, así que no te culparé si no me crees. Si bien no tengo números más recientes a mano, creo que el estado ahora es que tanto Cranelift como SpiderMonkey corrigieron el pequeño puñado de errores que eran la fuente principal de su bajo rendimiento en estos micropuntos de referencia en comparación con Lightbeam, pero el diferencial de velocidad de compilación no cambió todo el tiempo que estuve en el proyecto porque cada compilador todavía tiene la misma arquitectura fundamentalmente, y es la arquitectura respectiva la que conduce a los diferentes niveles de rendimiento. Si bien aprecio su especulación, no sé de dónde proviene su suposición de que el método que describí sería más lento.

Estos son los puntos de referencia, los puntos de referencia ::compile son para la velocidad de compilación y los puntos ::run son para la velocidad de ejecución de la salida del código de máquina. https://gist.github.com/Vurich/8696e67180aa3c93b4548fb1f298c29e

La metodología está aquí, puede clonarla y volver a ejecutar los puntos de referencia para confirmar los resultados por sí mismo, pero es probable que PR sea incompatible con la última versión de wasmtime, por lo que solo le mostrará la comparación de rendimiento en el momento en que actualicé por última vez el relaciones públicas https://github.com/bytecodealliance/wasmtime/pull/1660

Dicho esto, mi argumento _no_ es que los CFG sean una representación interna útil para el rendimiento en un compilador de transmisión. Mi argumento es que los CFG no afectan negativamente el rendimiento en ningún compilador, y ciertamente no al nivel que justificaría bloquear por completo a los equipos de GCC y Go para que no produzcan WebAssembly. Casi nadie en este hilo que se oponga a los funclets o una extensión similar a wasm ha trabajado en los proyectos que afirman que se verán afectados negativamente por esta propuesta. No quiere decir que necesite experiencia de primera mano para comentar sobre este tema, creo que todos tienen algún nivel de aporte valioso, pero sí que hay una línea entre tener una opinión diferente sobre el color del cobertizo para bicicletas y hacer afirmaciones basadas en nada más que especulaciones ociosas.

@Vurich

No, es un compilador de transmisión y emite código más rápido que cualquiera de esos dos motores (aunque hay casos degenerados que nunca se solucionaron porque dejé el proyecto).

Lo siento si no fui lo suficientemente claro antes. Para estar seguros de que estamos hablando de lo mismo, me refiero a los compiladores de referencia en esos motores. Y estoy hablando del tiempo de compilación, que es el objetivo de los compiladores básicos en el sentido en que V8 y SpiderMonkey usan el término.

La razón por la que soy escéptico de que pueda superar los tiempos de compilación de línea de base de V8 y SpiderMonkey es porque, como en los enlaces que brindé anteriormente, esos dos compiladores de línea de base están extraordinariamente ajustados para el tiempo de compilación. En particular, no generan ningún IR interno, solo van directamente de wasm a código de máquina. Usted dijo que su compilador emite un IR interno (para un CFG); esperaría que sus tiempos de compilación fueran más lentos solo por eso (debido a más bifurcaciones, ancho de banda de memoria, etc.).

¡Pero por favor compare con esos compiladores de referencia! Me encantaría ver datos que muestren que mi suposición es incorrecta, y estoy seguro de que también lo harían los ingenieros de V8 y SpiderMonkey. Significaría que ha encontrado un mejor diseño que deberían considerar adoptar.

Para probar contra V8, puede ejecutar d8 --liftoff --no-wasm-tier-up , y para SpiderMonkey puede ejecutar sm --wasm-compiler=baseline .

(Gracias por las instrucciones para comparar con Cranelift, pero Cranelift no es un compilador de referencia, por lo que comparar los tiempos de compilación no es relevante en este contexto. De lo contrario, es muy interesante, estoy de acuerdo).

Mi intuición es que los compiladores de referencia no tendrían que cambiar significativamente su estrategia de compilación para admitir funclets/ multiloop , ya que de todos modos no intentan realizar una optimización significativa entre bloques. La basó en- "estructura del flujo de control, incluyendo une y divide" referenciados por @kripken se satisfacen aplicando todos los tipos de entrada para una colección de bloques mutuamente recursivos para ser declarada hacia adelante-(que parece la opción natural para la transmisión de la validación de todos modos) . Si Lightbeam/Wasmtime puede vencer a los compiladores de referencia del motor no tiene en cuenta esto; el punto importante es si los compiladores de línea de base del motor pueden permanecer tan rápidos como lo son ahora.

FWIW, me interesaría ver que esta característica se discuta en una futura reunión de CG, y estoy ampliamente de acuerdo con @Vurich en que los representantes del motor pueden objetar por sí mismos si no están preparados para implementarla. Dicho esto, deberíamos tomarnos en serio cualquier objeción de este tipo (anteriormente opiné en reuniones en persona que al buscar esta función deberíamos tratar de evitar una versión WebAssembly de la saga JavaScript

@kripken

Sí, creo que es una diferencia significativa. La asignación de registro de exploración lineal es mejor (pero más lenta de hacer) que lo que hacen actualmente los compiladores de línea de base de wasm , ya que compilan en una forma de transmisión que es realmente rápida. Es decir, no hay un paso inicial para encontrar la última mención de cada variable: se compilan en un solo paso, emitiendo código a medida que avanzan sin siquiera ver el código más adelante en la función wasm, con la ayuda de la estructura, y también simplifican elecciones a medida que avanzan ("estúpido" es la palabra utilizada en esa publicación).

Wow, eso realmente es muy simple.

Por otro lado… ese algoritmo en particular es tan simple que no depende de ninguna propiedad profunda del flujo de control estructurado. Apenas depende de las propiedades superficiales del flujo de control estructurado.

Como se menciona en la publicación del blog, el compilador de línea de base wasm de SpiderMonkey no conserva el estado del asignador de registros a través de "uniones de flujo de control" (es decir, bloques básicos con múltiples predecesores), sino que utiliza un ABI fijo o mapea desde la pila wasm a la pila y los registros nativos. . Descubrí a través de las pruebas que también usa un ABI fijo al ingresar bloques , ¡aunque en la mayoría de los casos no es una unión de flujo de control!

La ABI fija es la siguiente (en x86):

  • Si hay un número distinto de cero de parámetros (al ingresar a un bloque) o retornos (al salir de un bloque), entonces la parte superior de la pila wasm va en rax , y el resto de la pila wasm corresponde al x86 apilar.
  • De lo contrario, toda la pila wasm corresponde a la pila x86.

¿Por qué importa esto?

Porque este algoritmo podría funcionar casi de la misma manera con mucha menos información. Como experimento mental, imagine una versión de universo alternativo de WebAssembly donde no hubiera instrucciones de flujo de control estructurado, solo instrucciones de salto, similar al ensamblado nativo. Tendría que aumentarse con solo una pieza de información adicional: una forma de saber qué instrucciones son los objetivos de los saltos.

Entonces el algoritmo simplemente sería: seguir las instrucciones linealmente; antes de los saltos y los objetivos de salto, vacíe los registros en el ABI fijo.

La única diferencia es que tendría que haber una sola ABI fija, no dos. No podía distinguir entre el valor de la parte superior de la pila siendo semánticamente el 'resultado' de un salto, versus simplemente dejarlo en la pila desde un bloque externo. Por lo tanto, tendría que poner incondicionalmente la parte superior de la pila en rax .

Pero dudo que esto tenga un costo medible para el rendimiento; en todo caso, podría ser una mejora.

(La verificación también sería diferente, pero seguiría siendo de un solo paso).

De acuerdo, advertencias por adelantado:

  1. Este no es un universo alternativo; estamos atascados con la creación de extensiones compatibles con versiones anteriores del WebAssembly existente.
  2. El compilador de referencia de SpiderMonkey es solo una implementación, y es posible que sea subóptimo con respecto a la asignación de registros: si fuera un poco más inteligente, el beneficio del tiempo de ejecución superaría el costo del tiempo de compilación.
  3. Incluso si los compiladores de referencia no necesitan información adicional, los compiladores de optimización pueden necesitarla para una construcción rápida de SSA.

Con esto en mente, el experimento mental anterior refuerza mi creencia de que los compiladores de referencia no necesitan un flujo de control estructurado . Independientemente de qué tan bajo nivel agreguemos una construcción, siempre que incluya información básica como qué instrucciones son objetivos de salto, los compiladores de referencia pueden manejarlo con solo cambios menores. O al menos este puede.

@conrad-watt @comex

¡Son muy buenos puntos! Mi intuición acerca de los compiladores de línea de base bien puede estar equivocada entonces.

Y @comex : sí, como dijiste, esta discusión es independiente de la optimización de los compiladores donde SSA puede beneficiarse de la estructura. Tal vez valga la pena citar un poco de uno de los enlaces de antes :

Por diseño, la transformación del código WebAssembly en IR de TurboFan (incluida la construcción SSA) en un solo paso directo es muy eficiente, en parte debido al flujo de control estructurado de WebAssembly.

@conrad-watt Definitivamente estoy de acuerdo en que solo necesitamos recibir comentarios directos de la gente de VM y mantener la mente abierta. Para ser claros, mi objetivo aquí no es detener nada. Comenté aquí extensamente porque varios comentarios parecían pensar que el flujo de control estructurado de wasm era un error obvio o que obviamente debería remediarse con funclets/multiloop. Solo quería presentar la historia del pensamiento aquí, y que había razones poderosas. para el modelo actual, por lo que puede que no sea fácil mejorarlo.

He disfrutado mucho leyendo esta conversación. Yo mismo me he hecho muchas de estas preguntas (procedentes de ambas direcciones) y he compartido muchos de estos pensamientos (nuevamente desde ambas direcciones), y la discusión ha ofrecido muchas ideas y experiencias útiles. No estoy seguro de tener una opinión fuerte todavía, pero tengo un pensamiento para contribuir en cada dirección.

En el lado "a favor", es útil saber por adelantado qué bloques tienen bordes posteriores. Un compilador de transmisión puede rastrear propiedades que no son evidentes en el sistema de tipos de WebAssembly (por ejemplo, el índice en i local está dentro de los límites de la matriz en arr local). Al saltar hacia adelante, puede ser útil anotar el objetivo con las propiedades que se mantienen en ese punto. De esa manera, cuando se alcanza una etiqueta, su bloque se puede compilar utilizando las propiedades que se mantienen en todos los bordes, por ejemplo, para eliminar las comprobaciones de los límites de la matriz. Pero si una etiqueta puede tener potencialmente un backedge desconocido, entonces su bloque no se puede compilar con este conocimiento. Por supuesto, un compilador que no es de transmisión puede hacer un análisis invariable de bucle más significativo, pero para un compilador de transmisión es útil no tener que preocuparse por lo que podría suceder. (Pensamiento adicional :

Del lado de los "en contra", hasta ahora la discusión se ha centrado únicamente en el control local. Eso está bien para C, pero ¿qué pasa con C++ u otros lenguajes con excepciones similares? ¿Qué pasa con los idiomas con otras formas de control no local? Las cosas con alcance dinámico a menudo están inherentemente estructuradas (o al menos no conozco ningún ejemplo de alcances dinámicos mutuamente recursivos). Creo que estas consideraciones son abordables, pero tendrías que diseñar algo con ellas en mente para que el resultado sea utilizable en estas configuraciones. Esto es algo en lo que he estado reflexionando, y estoy feliz de compartir mis pensamientos en progreso (pareciendo más o menos una extensión del bucle múltiple de @conrad-watt) con cualquiera que esté interesado (aunque aquí parece fuera de tema), pero Quería al menos avisar que hay más que solo un flujo de control local a tener en cuenta.

(También me gustaría agregar otro +1 por escuchar más de la gente de VM, aunque creo que

Cuando digo que Lightbeam produce un IR interno, eso es bastante engañoso y debería haberlo aclarado. Estuve trabajando en el proyecto durante un tiempo y, a veces, puedes tener una visión de túnel. Básicamente, Lightbeam consume la instrucción de entrada por instrucción (en realidad tiene un máximo de una instrucción anticipada, pero eso no es particularmente importante), y para cada instrucción produce, de forma perezosa y en un espacio constante, una serie de instrucciones IR internas. El número máximo de instrucciones por instrucción Wasm es constante y pequeño, algo así como 6. No está creando un búfer de instrucciones IR para toda la función y trabajando en eso. Luego, lee esas instrucciones IR una por una. Realmente puede pensar que tiene una biblioteca de funciones de ayuda más genéricas que implementa cada instrucción Wasm en términos de, solo me refiero a él como un IR porque eso ayuda a explicar cómo tiene un modelo diferente para control de flujo, etc. Probablemente no produzca código tan rápido como V8 o los compiladores básicos de SpiderMonkey, pero eso se debe a que no está totalmente optimizado y no a que su arquitectura sea deficiente. Mi punto es que modelé internamente el flujo de control jerárquico de Wasm como si fuera un CFG, en lugar de producir un búfer de IR en la memoria de la forma en que lo hacen LLVM o Cranelift.

Otra opción es compilar el wasm en algo que crea que puede manejar el flujo de control de manera óptima, es decir, "deshacer" la estructuración. LLVM debería poder hacer eso, por lo que podría ser interesante ejecutar wasm en una máquina virtual que usa LLVM (como WAVM o wasmer) o a través de WasmBoxC.

@kripken Desafortunadamente, LLVM no parece poder deshacer la estructuración todavía. El pase de optimización de subprocesos de salto debería poder hacer esto, pero aún no reconoce este patrón. Aquí hay un ejemplo que muestra un código C++ que imita cómo el algoritmo relooper convertiría un CFG en un loop+switch. GCC logra "dereloop", pero clang no: https://godbolt.org/z/GGM9rP

@AndrewScheidecker Interesante, gracias. Sí, estas cosas pueden ser bastante impredecibles, por lo que puede que no haya mejor opción que investigar el código emitido (como lo hace el documento "No tan rápido" vinculado anteriormente), y evitar intentos de atajos como confiar en el optimizador de LLVM.

@comex

El compilador de referencia de SpiderMonkey es solo una implementación, y es posible que sea subóptimo con respecto a la asignación de registros: si fuera un poco más inteligente, el beneficio del tiempo de ejecución superaría el costo del tiempo de compilación.

Claramente podría ser más inteligente sobre la asignación de registros. Se derrama indistintamente en las bifurcaciones de flujo de control, uniones y antes de las llamadas, y podría mantener más información sobre el estado del registro e intentar mantener los valores en los registros por más tiempo/hasta que estén muertos. Podría elegir un registro mejor que rax para los resultados de valor de los bloques, o mejor, no usar un registro fijo. Podría dedicar estáticamente un par de registros para contener variables locales; un análisis de corpus que hice sugirió que solo unos pocos registros enteros y FP serían suficientes para la mayoría de las funciones. Podría ser más inteligente acerca de los derrames en general; tal como está, lo derrama todo cuando se queda sin registros.

El costo de tiempo de compilación de esto es principalmente que cada borde de flujo de control tendrá una cantidad no constante de información asociada (el estado de registro) y esto puede conducir a un uso más generalizado de la asignación de almacenamiento dinámico, que el compilador de referencia tiene tan muy evitado. Y, por supuesto, habrá un costo asociado con el procesamiento de esa información de tamaño variable en cada unión (y en otros lugares). Pero ya hay un costo no constante ya que el estado de registro debe atravesarse para generar código de derrame y, en general, puede haber pocos valores en vivo, por lo que esto puede estar bien (o no). Por supuesto, ser más inteligente con el regalloc puede o no dar sus frutos en los chips modernos, con sus cachés rápidos y su ejecución ooo...

Un costo más sutil es la capacidad de mantenimiento del compilador... ya es bastante complejo, y dado que es de un solo paso y no genera un gráfico IR ni usa memoria dinámica en absoluto, es resistente a la superposición y la abstracción.

@RossTate

Con respecto a funclets/gotos, leí la especificación de funclet el otro día y, a primera vista, no parecía que un compilador de un solo paso debería tener ningún problema real con él, ciertamente no con un esquema simplista de regalloc. Pero incluso con un mejor esquema podría estar bien: el primer borde en llegar a un punto de unión podría decidir cuál es la asignación de registro, y otros bordes tendrían que conformarse.

@conrad-watt como acabas de mencionar en la reunión de CG, creo que estaríamos muy interesados ​​en ver detalles sobre cómo se vería tu bucle múltiple.

@aardappel sí, la vida me ha llegado rápido, pero debo hacer esto en la próxima reunión. Solo para enfatizar que la idea no es mía ya que @rossberg la dibujó originalmente en respuesta al primer borrador de funclets.

Una referencia que podría ser instructiva es un poco anticuada, pero generaliza las nociones familiares de bucles para manejar los irreducibles usando gráficos de DJ .

Hemos tenido un par de sesiones de discusión sobre esto en el GC, y he escrito un resumen y un documento de seguimiento. Debido a la longitud, lo he convertido en una esencia separada.

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

Creo que las dos preguntas de acción inmediata (consulte la sección de seguimiento para obtener más detalles) son:

  • ¿Podemos encontrar programas "salvajes" que actualmente están sufriendo y se beneficiarían en términos de rendimiento de multiloop ? Estos pueden ser programas para los cuales las transformaciones LLVM introducen un flujo de control irreducible incluso si no existe en el programa fuente.
  • ¿Existe un mundo en el que multiloop se implemente primero en el lado del productor, con alguna capa de implementación de enlace/traducción para "Web" Wasm?

Probablemente también haya una discusión más libre sobre las consecuencias de los problemas de manejo de excepciones que analizo en el documento de seguimiento y, por supuesto, el cambio estándar sobre los detalles semánticos si avanzamos con algo concreto.

Debido a que estas discusiones pueden ramificarse un poco, puede ser apropiado convertir algunas de ellas en problemas en el repositorio de funclets .

Estoy muy feliz de ver el progreso en este tema. Un enorme "Gracias" a todas las personas involucradas!

¿Podemos encontrar programas "salvajes" que actualmente están sufriendo y se beneficiarían en cuanto al rendimiento de multiloop? Estos pueden ser programas para los cuales las transformaciones LLVM introducen un flujo de control irreducible incluso si no existe en el programa fuente.

Me gustaría advertir un poco sobre el razonamiento circular: los programas que actualmente tienen un mal rendimiento tienen menos probabilidades de ocurrir "en la naturaleza" exactamente por esta razón.

Creo que la mayoría de los programas Go deberían beneficiarse mucho. El compilador de Go necesita corrutinas de WebAssembly o multiloop para poder emitir un código eficiente que admita las rutinas de Go.

Los comparadores de expresiones regulares precompilados, junto con otras máquinas de estado precompiladas, a menudo dan como resultado un flujo de control irreducible. Es difícil decir si el algoritmo de "fusión" para los tipos de interfaz dará como resultado un flujo de control irreducible.

  • De acuerdo, esta discusión debe trasladarse a problemas en el repositorio de funclets (o uno nuevo).
  • De acuerdo en que encontrar un programa que se beneficie de él es difícil de cuantificar sin que LLVM (y Go, y otros) emitan realmente el flujo de control más óptimo (que puede ser irreducible). La ineficiencia causada por FixIrreducibleControlFlow y amigos puede ser un problema de "muerte por mil cortes" en un binario grande.
  • Si bien agradecería una implementación solo de herramientas como el progreso mínimo absoluto que surge de esta discusión, aún no sería óptimo, ya que los productores ahora tienen la difícil elección de hacer uso de esta funcionalidad por conveniencia (pero luego enfrentan regresiones/regresiones de rendimiento impredecibles). acantilados), o hacer el trabajo duro para ajustar su producción a wasm estándar para que las cosas sean predecibles.
  • Si se decidiera que los "gotos" son, en el mejor de los casos, una función solo de herramientas, diría que probablemente podría salirse con la suya con una función aún más simple que multiloop, ya que lo único que le importa es la comodidad del productor. Como mínimo absoluto, un goto <function_byte_offset> sería lo único que se necesitaría insertar en los cuerpos de funciones regulares de Wasm para permitir que WABT o Binaryen lo transformen en Wasm legal. Cosas como las firmas de tipo son útiles si los motores necesitan verificar un bucle múltiple rápidamente, pero si es una herramienta conveniente, también podría hacer que sea lo más conveniente posible para emitir.

De acuerdo en que encontrar un programa que se beneficie de él es difícil de cuantificar sin que LLVM (y Go, y otros) emitan realmente el flujo de control más óptimo (que puede ser irreducible).

Acepto que las pruebas en cadenas de herramientas modificadas + máquinas virtuales serían óptimas. Pero podemos comparar las compilaciones actuales de wasm con las compilaciones nativas que tienen un flujo de control óptimo. Not So Fast y otros han analizado esto de varias maneras (contadores de rendimiento, investigación directa) y no han encontrado que el flujo de control irreducible sea un factor significativo.

Más específicamente, no encontraron que fuera un factor significativo para C/C++. Eso podría tener más que ver con C/C++ que con el rendimiento del flujo de control irreducible. (Honestamente, no lo sé). Parece que @neelance tiene razones para creer que lo mismo no sería cierto para Go.

Mi sensación es que hay múltiples facetas en este problema, y ​​vale la pena abordarlo en múltiples direcciones.

Primero, parece que hay un problema general con la capacidad de generación de WebAssembly. Gran parte de eso se debe a la restricción de WebAssembly de tener un binario compacto con verificación de tipo eficiente y compilación de transmisión. Podríamos abordar este problema, al menos en parte, mediante el desarrollo de un "pre"-WebAssembly estandarizado que sea más fácil de generar pero que se garantice que se puede traducir a un WebAssembly "verdadero", idealmente solo mediante la duplicación de código y la inserción de instrucciones/anotaciones "borrables", con al menos alguna herramienta que proporcione dicha traducción.

En segundo lugar, podemos considerar qué características de "pre"-WebAssembly vale la pena incorporar directamente en WebAssembly "verdadero". Podemos hacer esto de manera informada porque tendremos módulos "pre"-WebAssembly que podemos analizar antes de que se hayan convertido en módulos WebAssembly "verdaderos".

Hace algunos años intenté compilar un emulador de bytecode particular para un lenguaje dinámico (https://github.com/ciao-lang/ciao) para webassembly y el rendimiento estaba lejos de ser óptimo (a veces 10 veces más lento que la versión nativa). El bucle de ejecución principal contenía un gran interruptor de envío de código de bytes, y el motor se ajustó con precisión durante décadas para ejecutarse en hardware real, y hacemos un uso intensivo de etiquetas y gotos. Me pregunto si este tipo de software se beneficiaría del soporte para flujo de control irreducible o si el problema era otro. No tuve tiempo de investigar más, pero me encantaría volver a intentarlo si se sabe que las cosas han mejorado. Por supuesto, entiendo que compilar VM de otros lenguajes a wasm no es el caso de uso principal, pero sería bueno saber si esto eventualmente será factible, especialmente porque los binarios universales que se ejecutan de manera eficiente, en todas partes, es una de las ventajas prometidas de era m. (Gracias y disculpas si este tema en particular ha sido tratado en algún otro número)

@jfmc Tengo entendido que, si el programa es realista (es decir, no está diseñado para ser patológico) y le importa su rendimiento, entonces es un caso de uso perfectamente válido. WebAssembly pretende ser un buen objetivo de propósito general. Así que creo que sería genial comprender por qué vio una desaceleración tan significativa. Si eso se debe a restricciones en el flujo de control, sería muy útil saberlo en esta discusión. Si se debe a otra cosa, sería útil saber cómo mejorar WebAssembly en general.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

cretz picture cretz  ·  5Comentarios

artem-v-shamsutdinov picture artem-v-shamsutdinov  ·  6Comentarios

bobOnGitHub picture bobOnGitHub  ·  6Comentarios

aaabbbcccddd00001111 picture aaabbbcccddd00001111  ·  3Comentarios

dpw picture dpw  ·  3Comentarios