Binaryen es un compilador y una cadena de herramientas.
de infraestructura para WebAssembly, escrita en C++. Tiene como objetivo hacer
compilar en WebAssembly es intuitivo, rápido y eficaz. En esta publicación, usaremos
ejemplo de un lenguaje de juguete sintético llamado ExampleScript, aprende a escribir
Módulos de WebAssembly en JavaScript con la API de Binaryen.js Abarcarás los
básicos de la creación de módulos, la adición de funciones al módulo y la exportación
funciones del módulo. Esto te permitirá conocer los aspectos generales
de compilación de lenguajes de programación reales para WebAssembly. Además,
Aprenderás a optimizar los módulos de Wasm con Binaryen.js y
la línea de comandos con wasm-opt
.
Información sobre Binaryen
Binaryen tiene un enfoque API de C en un solo encabezado y también se pueden se usan desde JavaScript. Acepta entradas Formulario de WebAssembly, pero también acepta una gráfico de flujo de control para compiladores que lo prefieren.
Una representación intermedia (IR) es la estructura de datos o el código que se usa. internamente por un compilador o máquina virtual para representar el código fuente. de Binaryen la IR interna usa estructuras de datos compactas y está diseñada para procesos la generación y optimización de código con todos los núcleos de CPU disponibles. IR de Binaryen compila a WebAssembly debido a que es un subconjunto de WebAssembly.
El optimizador de Binaryen tiene muchos pases que pueden mejorar el tamaño y la velocidad del código. Estos El objetivo de las optimizaciones es hacer que Binaryen sea lo suficientemente potente para usarse como compilador el backend por sí solo. Incluye optimizaciones específicas de WebAssembly (que es posible que los compiladores de uso general no lo hagan), lo que se puede considerar como Wasm reducción.
AssemblyScript como usuario de ejemplo de Binaryen
Varios proyectos usan Binaryen, por ejemplo, AssemblyScript, que usa Binaryen para compilar a partir de un lenguaje similar a TypeScript directamente a WebAssembly. Prueba el ejemplo en el Playground de AssemblyScript.
Entrada de AssemblyScript:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Código WebAssembly correspondiente en formato textual generado por Binaryen:
(module
(type $0 (func (param i32 i32) (result i32)))
(memory $0 0)
(export "add" (func $module/add))
(export "memory" (memory $0))
(func $module/add (param $0 i32) (param $1 i32) (result i32)
local.get $0
local.get $1
i32.add
)
)
La cadena de herramientas de Binaryen
La cadena de herramientas de Binaryen ofrece varias herramientas útiles para JavaScript.
desarrolladores y usuarios de línea de comandos. Un subconjunto de estas herramientas se enumera en el
siguiente; el
lista completa de herramientas incluidas
está disponible en el archivo README
del proyecto.
binaryen.js
: Una biblioteca de JavaScript independiente que expone los métodos de Binaryen para Crea y optimiza módulos de Wasm. Para compilaciones, consulta binaryen.js en npm (o descárgala directamente en GitHub o unpkg).wasm-opt
: Es la herramienta de línea de comandos que carga WebAssembly y ejecuta Binaryen IR. le pase.wasm-as
ywasm-dis
: Herramientas de línea de comandos que ensamblan y desensamblan WebAssemblywasm-ctor-eval
: Herramienta de línea de comandos que puede ejecutar funciones (o partes de funciones) en el tiempo de compilación.wasm-metadce
: Herramienta de línea de comandos para quitar partes de los archivos Wasm en un entorno flexible que depende de cómo se use el módulo.wasm-merge
: Herramienta de línea de comandos que combina varios archivos Wasm en uno solo conectando las importaciones correspondientes con las exportaciones. Como un agrupador para JavaScript, pero para Wasm.
Cómo compilar en WebAssembly
Compilar un lenguaje a otro suele implicar varios pasos, el más las más importantes se enumeran en la siguiente lista:
- Análisis léxico: Divide el código fuente en tokens.
- Análisis sintáctico: Crea un árbol sintáctico abstracto.
- Análisis semántico: Verifica si hay errores y aplica las reglas del lenguaje.
- Generación de código intermedio: Crea una representación más abstracta.
- Generación de código: Traduce al idioma de destino.
- Optimización de código específico para el objetivo: Optimiza en función del objetivo.
En el mundo Unix, las herramientas que se usan con frecuencia para compilar son
lex
y
yacc
:
lex
(generador de Lexical Analyzer):lex
es una herramienta que genera modelos léxicos analizadores, también conocidos como léxicos o escáneres. Se necesita un conjunto de registros y las acciones correspondientes como entrada, y genera código para un y léxico que reconoce patrones en el código fuente de entrada.yacc
(Yet Another Compiler Compiler):yacc
es una herramienta que genera de análisis de sintaxis. Consta de una descripción gramatical formal de un de programación como entrada, y genera código para un analizador. Analizadores normalmente producen árboles de sintaxis abstractos (AST) que representan la estructura jerárquica del código fuente.
Un ejemplo trabajado
Dado el alcance de esta publicación, es imposible cubrir una programación completa de lenguaje extenso, así que, para una mayor simplicidad, considera un lenguaje de programación sintético llamado ExampleScript que funciona expresando operaciones genéricas a través de ejemplos concretos.
- Para escribir una función
add()
, debes codificar un ejemplo de cualquier suma, como2 + 3
- Para escribir una función
multiply()
, escribe, por ejemplo,6 * 12
.
Según la advertencia previa, es completamente inútil, pero lo suficientemente simple por su comportamiento léxico
analizador sea una sola expresión regular: /\d+\s*[\+\-\*\/]\s*\d+\s*/
.
Luego, debe haber un analizador. En realidad, una versión muy simplificada de
se puede crear un árbol sintáctico abstracto usando una expresión regular con
grupos de captura con nombre:
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/
Los comandos de ExampleScript son uno por línea, por lo que el analizador puede procesar el código. línea dividiendo en caracteres de línea nueva. Esto es suficiente para verificar el primer tres pasos de la lista anterior, el análisis léxico, sintaxis análisis y análisis semántico. El código para estos pasos se encuentra en que aparece a continuación.
export default class Parser {
parse(input) {
input = input.split(/\n/);
if (!input.every((line) => /\d+\s*[\+\-\*\/]\s*\d+\s*/gm.test(line))) {
throw new Error('Parse error');
}
return input.map((line) => {
const { groups } =
/(?<first_operand>\d+)\s*(?<operator>[\+\-\*\/])\s*(?<second_operand>\d+)/gm.exec(
line,
);
return {
firstOperand: Number(groups.first_operand),
operator: groups.operator,
secondOperand: Number(groups.second_operand),
};
});
}
}
Generación de código intermedia
Ahora que los programas de ExampleScript pueden representarse como un árbol de sintaxis abstracto (aunque sea bastante simplificado), el siguiente paso es crear una imagen intermediaria. El primer paso Crea un módulo nuevo en Binaryen:
const module = new binaryen.Module();
Cada línea del árbol de sintaxis abstracta contiene un triple compuesto por
firstOperand
, operator
y secondOperand
. Para cada uno de los cuatro posibles
operadores en ExampleScript, es decir, +
, -
, *
, /
, un nuevo
la función debe agregarse al módulo
con el método Module#addFunction()
de Binaryen. Los parámetros de la
Los métodos Module#addFunction()
son los siguientes:
name
: Es unstring
, representa el nombre de la función.functionType
: Es unSignature
, representa la firma de la función.varTypes
: Es unType[]
, indica locales adicionales, en el orden determinado.body
: Es unExpression
, el contenido de la función.
Aún hay más detalles para
relajarse y desglosarse y la
Documentación de Binary
puede ayudarte a navegar por el espacio, pero, con el tiempo, para +
de ExampleScript,
terminas en el método Module#i32.add()
como uno de varios
disponible
operaciones de números enteros.
La suma requiere dos operandos, el primero y el segundo. Para el
para que se puedan llamar realmente, se debe
exportado
con Module#addFunctionExport()
.
module.addFunction(
'add', // name: string
binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
binaryen.i32, // results: Type
[binaryen.i32], // vars: Type[]
// body: ExpressionRef
module.block(null, [
module.local.set(
2,
module.i32.add(
module.local.get(0, binaryen.i32),
module.local.get(1, binaryen.i32),
),
),
module.return(module.local.get(2, binaryen.i32)),
]),
);
module.addFunctionExport('add', 'add');
Después de procesar el árbol de sintaxis abstracto, el módulo contiene cuatro métodos:
tres que trabajan con números enteros, es decir, add()
basado en Module#i32.add()
,
subtract()
basado en Module#i32.sub()
; multiply()
basado en
Module#i32.mul()
y el valor atípico divide()
según Module#f64.div()
ya que ExampleScript también funciona con resultados de punto flotante.
for (const line of parsed) {
const { firstOperand, operator, secondOperand } = line;
if (operator === '+') {
module.addFunction(
'add', // name: string
binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
binaryen.i32, // results: Type
[binaryen.i32], // vars: Type[]
// body: ExpressionRef
module.block(null, [
module.local.set(
2,
module.i32.add(
module.local.get(0, binaryen.i32),
module.local.get(1, binaryen.i32)
)
),
module.return(module.local.get(2, binaryen.i32)),
])
);
module.addFunctionExport('add', 'add');
} else if (operator === '-') {
module.subtractFunction(
// Skipped for brevity.
)
} else if (operator === '*') {
// Skipped for brevity.
}
// And so on for all other operators, namely `-`, `*`, and `/`.
Si trabajas con bases de código reales, a veces habrá código muerto que nunca se llama. Para ingresar artificialmente código no muerto (que se optimizará y se eliminará en un paso posterior) en el ejemplo en ejecución de la API de ExampleScript. compilación a Wasm, agregar una función no exportada hace el trabajo.
// This function is added, but not exported,
// so it's effectively dead code.
module.addFunction(
'deadcode', // name: string
binaryen.createType([binaryen.i32, binaryen.i32]), // params: Type
binaryen.i32, // results: Type
[binaryen.i32], // vars: Type[]
// body: ExpressionRef
module.block(null, [
module.local.set(
2,
module.i32.div_u(
module.local.get(0, binaryen.i32),
module.local.get(1, binaryen.i32),
),
),
module.return(module.local.get(2, binaryen.i32)),
]),
);
El compilador ya está casi listo. No es estrictamente necesario,
una buena práctica para
validar el módulo
con el método Module#validate()
.
if (!module.validate()) {
throw new Error('Validation error');
}
Cómo obtener el código Wasm resultante
Para
obtener el código de Wasm resultante
en Binaryen existen dos métodos para obtener
representación textual
como un archivo .wat
en S-expression
en un formato legible por humanos, y
representación binaria
como un archivo .wasm
que se puede ejecutar directamente en el navegador. El código binario puede ser
se ejecuta directamente en el navegador. Para ver que funcionó, puedes registrar
ayuda.
const textData = module.emitText();
console.log(textData);
const wasmData = module.emitBinary();
const compiled = new WebAssembly.Module(wasmData);
const instance = new WebAssembly.Instance(compiled, {});
console.log('Wasm exports:\n', instance.exports);
La representación textual completa para un programa ExampleScript con los cuatro
las operaciones se enumeran a continuación. Observa que el código muerto sigue ahí,
pero no está expuesta según la captura de pantalla de
WebAssembly.Module.exports()
(module
(type $0 (func (param i32 i32) (result i32)))
(type $1 (func (param f64 f64) (result f64)))
(export "add" (func $add))
(export "subtract" (func $subtract))
(export "multiply" (func $multiply))
(export "divide" (func $divide))
(func $add (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(local.set $2
(i32.add
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
(func $subtract (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(local.set $2
(i32.sub
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
(func $multiply (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(local.set $2
(i32.mul
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
(func $divide (param $0 f64) (param $1 f64) (result f64)
(local $2 f64)
(local.set $2
(f64.div
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
(func $deadcode (param $0 i32) (param $1 i32) (result i32)
(local $2 i32)
(local.set $2
(i32.div_u
(local.get $0)
(local.get $1)
)
)
(return
(local.get $2)
)
)
)
Optimiza WebAssembly
Binaryen ofrece dos formas de optimizar el código Wasm. uno en Binaryen.js y uno para la línea de comandos. El primero aplica el conjunto estándar de optimización de forma predeterminada y te permite establecer el nivel de optimización y este último no usa reglas, sino que permite una personalización completa. lo que significa que, con suficiente experimentación, puedes adaptar la configuración para resultados óptimos en función de tu código.
Cómo optimizar con Binaryen.js
La forma más directa de optimizar un módulo de Wasm con Binaryen es
llamar directamente al método Module#optimize()
de Binaryen.js y, de forma opcional,
estableciendo
optimizar y reducir.
// Assume the `wast` variable contains a Wasm program.
const module = binaryen.parseText(wast);
binaryen.setOptimizeLevel(2);
binaryen.setShrinkLevel(1);
// This corresponds to the `-Os` setting.
module.optimize();
Esto quitará el código muerto
que se introdujo artificialmente antes,
representación textual de la versión de Wasm del ejemplo de juguete ExampleScript no
ya que la contiene. Observa también cómo quita los pares local.set/get
mediante la
pasos de optimización
SimplifyLocals
(varias optimizaciones relacionadas con lugares locales) y el
Aspiradora
(quita el código innecesario evidente) y se quita return
RemoveUnusedBrs
(quita las pausas de las ubicaciones que no son necesarias).
(module
(type $0 (func (param i32 i32) (result i32)))
(type $1 (func (param f64 f64) (result f64)))
(export "add" (func $add))
(export "subtract" (func $subtract))
(export "multiply" (func $multiply))
(export "divide" (func $divide))
(func $add (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
(i32.add
(local.get $0)
(local.get $1)
)
)
(func $subtract (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
(i32.sub
(local.get $0)
(local.get $1)
)
)
(func $multiply (; has Stack IR ;) (param $0 i32) (param $1 i32) (result i32)
(i32.mul
(local.get $0)
(local.get $1)
)
)
(func $divide (; has Stack IR ;) (param $0 f64) (param $1 f64) (result f64)
(f64.div
(local.get $0)
(local.get $1)
)
)
)
Existen muchas
pases de optimización,
y Module#optimize()
usa los niveles de optimización y reducción específicos predeterminado
conjuntos. Para una personalización completa, debes usar la herramienta de línea de comandos wasm-opt
.
Realiza optimizaciones con la herramienta de línea de comandos wasm-opt
Para una personalización completa de los pases que se usarán, Binaryen incluye
Herramienta de línea de comandos de wasm-opt
. Para obtener un
Lista completa de las opciones de optimización posibles
consulta el mensaje de ayuda de la herramienta. La herramienta wasm-opt
es probablemente la más popular
de las herramientas, y varias cadenas de herramientas de compiladores lo usan para optimizar el código de Wasm.
incluidos Emscripten,
J2CL,
Kotlin/Wasm
dart2wasm,
wasm-pack y más.
wasm-opt --help
Para que te hagas una idea de los pases, aquí tienes un extracto de algunos de los que son comprensibles sin el conocimiento experto:
- CodeFolding: Evita el código duplicado combinándolo (por ejemplo, si dos
if
los grupos tienen algunas instrucciones compartidas al final). - DeadArgumentElimination: Pase de optimización del tiempo de vinculación para quitar argumentos. a una función si siempre se llama con las mismas constantes.
- MinifyImportsAndExports: Los reduce a
"a"
,"b"
. - DeadCodeElimination: Quita el código no alcanzado.
Hay un
guía de soluciones de optimización
con varios consejos para identificar cuáles de las distintas marcas son más
es importante y vale la pena
probarlo primero. Por ejemplo, a veces se ejecuta wasm-opt
.
repetidamente una y otra vez reduce aún más la entrada. En esos casos, ejecutar
con el
Marca --converge
continúa iterando hasta que no se produce más optimización y se alcanza un punto fijo
alcanzada.
Demostración
Para ver los conceptos presentados en esta publicación en acción, prueba con los y proporciona cualquier entrada de ExampleScript que se te ocurra. Además, asegúrate de consultar el código fuente de la demostración
Conclusiones
Binaryen ofrece un potente kit de herramientas para compilar lenguajes para WebAssembly y optimizar el código resultante. Su biblioteca de JavaScript y las herramientas de línea de comandos ofrecer flexibilidad y facilidad de uso. Esta publicación demostró que los principios centrales de Compilación de Wasm, en la que se destaca la eficacia de Binaryen y su potencial para lograr la máxima optimización. Aunque muchas de las opciones para personalizar de las optimizaciones requieren un conocimiento profundo de los aspectos internos de Wasm, la configuración predeterminada ya funciona muy bien. Por lo tanto, te deseamos mucho éxito en la compilación y optimización con Binaryen.
Agradecimientos
Alon Zakai revisó esta publicación: Thomas alegre y Rachel Andrew.