Design: La forma más eficaz de transmitir datos en contexto JS / WASM

Creado en 12 sept. 2018  ·  18Comentarios  ·  Fuente: WebAssembly/design

Hola,

imaginemos que tengo unos 1024 f32 valores almacenados en algún búfer Float32Array , esperando ser procesados ​​por el código DSP basado en WASM :)

Todavía soy bastante nuevo en WebAssembly. Comprendí que solo puede pasar valores numéricos escritos como argumentos a funciones WASM exportadas. Eso tiene sentido para mí y es por eso que decidí pasar mis datos usando la memoria. Estoy bien con eso también ...

Entonces, para pasar mis 1024 valores, los asigno directamente al .memory . Igual que:

const mem = exports.memory.buffer;
const F32 = new Float32Array(mem);
F32[0] = 31337.777;

Esto es divertido, pero para asignar todos mis valores, tengo que recorrer todos los valores para asignarlos todos a la memoria. Bueno, de alguna manera eso se siente mal en cuanto a rendimiento. Hubiera esperado un impl nativo. para hacer eso por mí. por ejemplo, una llave como data en el memoryDescriptor argumento del WebAssembly.Memory constructor que permite inicializar la memoria con un ArrayBuffer .

Entonces, bueno, entonces hago mi llamada a la función WASM y cuando el WASM impl. hizo su magia, está escribiendo los valores de resultado en la memoria. Esto está sucediendo dentro del bucle DSP, por lo que no hay una sobrecarga dentro de mi código WASM para hacer eso, por lo que puedo ver.

Pero ahora, después de volver al contexto JS, tengo que iterar sobre toda la memoria nuevamente solo para leer todos los valores y construir otra representación de datos basada en JS. Y de alguna manera esperaba un impl nativo. estar presente también para este propósito. Tal vez algo como memory.read(Float32Array) para devolverme los datos del búfer como Float32Array , abstrayendo el puntero y el laberinto de iteración.

¿Me estoy perdiendo de algo?
¿Existe una mejor manera de pasar grandes cantidades de datos desde / hacia WASM que acabo de pasar por alto?

Gracias de antemano y lo mejor,
Aron

Comentario más útil

Debe decidir si desea copiar datos entre JavaScript y WebAssembly, o si desea que WebAssembly sea el propietario de los datos.

Si desea copiar los datos, puede usar TypedArray.prototype.set () en lugar de escribir el bucle for usted mismo:

let instance = ...;
let myJSArray = new Float32Array(...);
let length = myJSArray.length;
let myWasmArrayPtr = instance.exports.allocateF32Array(length);
let myWasmArray = new Float32Array(instance.exports.memory.buffer, myWasmArrayPtr, length);

// Copy data in to be used by WebAssembly.
myWasmArray.set(myJSArray);

// Process the data in the array.
instance.exports.processF32Array(myWasmArrayPtr, length);

// Copy data out to JavaScript.
myJSArray.set(myWasmArray);

Si WebAssembly es el propietario de los datos, puede crear una vista sobre el búfer WebAssembly.Memory y pasarla también a sus funciones de JavaScript:

let instance = ...;
let length = ...;
let myArrayPtr = instance.exports.allocateF32Array(length);
let myArray = new Float32Array(instance.exports.memory.buffer, myArrayPtr, length);

// Use myArray as a normal Float32Array in JavaScript, fill in data, etc.
...

// Process the data in the array.
instance.exports.processF32Array(myArrayPtr, length);

// No need to copy data back to JavaScript, just use myArray directly.

También puede usar herramientas como embind para que esto sea un poco más fácil de usar.

Todos 18 comentarios

Debe decidir si desea copiar datos entre JavaScript y WebAssembly, o si desea que WebAssembly sea el propietario de los datos.

Si desea copiar los datos, puede usar TypedArray.prototype.set () en lugar de escribir el bucle for usted mismo:

let instance = ...;
let myJSArray = new Float32Array(...);
let length = myJSArray.length;
let myWasmArrayPtr = instance.exports.allocateF32Array(length);
let myWasmArray = new Float32Array(instance.exports.memory.buffer, myWasmArrayPtr, length);

// Copy data in to be used by WebAssembly.
myWasmArray.set(myJSArray);

// Process the data in the array.
instance.exports.processF32Array(myWasmArrayPtr, length);

// Copy data out to JavaScript.
myJSArray.set(myWasmArray);

Si WebAssembly es el propietario de los datos, puede crear una vista sobre el búfer WebAssembly.Memory y pasarla también a sus funciones de JavaScript:

let instance = ...;
let length = ...;
let myArrayPtr = instance.exports.allocateF32Array(length);
let myArray = new Float32Array(instance.exports.memory.buffer, myArrayPtr, length);

// Use myArray as a normal Float32Array in JavaScript, fill in data, etc.
...

// Process the data in the array.
instance.exports.processF32Array(myArrayPtr, length);

// No need to copy data back to JavaScript, just use myArray directly.

También puede usar herramientas como embind para que esto sea un poco más fácil de usar.

Vaya, gracias @binji, esto se ve increíble. Es exactamente lo que estaba buscando. También gracias por tomarse el tiempo para escribir el código de ejemplo. Esto también es muy útil. Lo intentaré esta noche y solicitaré algunas mejoras para el Loader impl. que está en uso para interactuar con WASM en https://github.com/AssemblyScript/assemblyscript :) (Es por eso que no encontré embind, que se ve increíble para usar con emscripten)

¿Ve una opción para ampliar los documentos en webassembly.org con respecto a esto?

Por ejemplo aquí:

https://webassembly.org/docs/web/

Estoy dispuesto a hacer esto si es posible.

Creo que también sería una buena idea explicarlo aquí:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory

@binji Fuera del tema, es posible que le guste https://github.com/torch2424/wasmBoy si aún no conoce el proyecto. Y este proyecto también podría beneficiarse de la solución que propuso, ya que utilizan bucles for para configurar la memoria ROM.

¿Ve una opción para ampliar los documentos en webassembly.org con respecto a esto?

Estuve de acuerdo en que deberíamos actualizar webassembly.org, con este cambio y mucho más también. Los documentos todavía son del repositorio de diseño, pero el repositorio de especificaciones es mucho más reciente y preciso.

Agregar algunos ejemplos a MDN probablemente sería más fácil y mejor, considerando que muchos más desarrolladores web usarán ese sitio que webassembly.org.

Fuera del tema, es posible que le guste https://github.com/torch2424/wasmBoy si aún no conoce el proyecto.

Sí, lo sé @ torch2424. :-) También tengo mi propio emulador de gameboy basado en wasm: https://github.com/binji/binjgb

Agregar algunos ejemplos a MDN probablemente sería más fácil y mejor, considerando que muchos más desarrolladores web usarán ese sitio que webassembly.org.

Muy bien, publicaré una actualización esta noche. Veamos si se publica.

Sí, lo sé @ torch2424. :-) También tengo mi propio emulador de gameboy basado en wasm: https://github.com/binji/binjgb

¡Jaja bien! Yo def. pruébalo :) Apuesto a que tu emulador no falla en Zelda - Link's Awakening cuando obtienes la espada (en la playa);))

Espero encontrar algo de tiempo para solucionar el bloqueo en wasmBoy también :)

¡Oye! ¡Rad que encontraste wasmboy! 😄 Sí, el emulador de @binji es definitivamente mucho más preciso, ¡jaja! Algo en lo que necesito trabajar con seguridad. Pero, irónicamente, uso el despertar de Link para probar a wasmboy todo el tiempo, ¡jaja! Me sorprende que se haya estrellado contra ti.

Abrió algunos errores basados ​​en los comentarios:
https://github.com/torch2424/wasmBoy/issues/141 - Paso de memoria
https://github.com/torch2424/wasmBoy/issues/142 - Zelda Crash (con capturas de pantalla me funciona)

De todos modos, no quiero descarrilar la conversación / problema, pasaré a los problemas que abrí. ¡Pero gracias por el comentario! 😄

Si WebAssembly es el propietario de los datos, puede crear una vista sobre el búfer de WebAssembly.Memory y pasarla también a sus funciones de JavaScript:

Una advertencia importante: la vista en el búfer de memoria se invalidará si la instancia de WebAssembly aumenta su memoria. Evite almacenar referencias a la vista que persistan después de otras llamadas en la instancia de WebAssembly.

@binji ¿Puedes decirme qué pones en el lado de AssemblyScript cuando usas esto? Noté que no es una función estándar.

let myWasmArrayPtr = instance.exports.allocateF32Array(length);

Escribo lo siguiente y funciona, pero todavía quiero saber si es correcto.

export function allocateF32Array(length: usize): usize {
    return memory.allocate(length * sizeof<f32>());
}

¿Necesito escribir otra función AS para liberarla con memory.free() y llamarla después de transferir la matriz al lado JS?

@fmkang Entonces, lo que @binji explicó fue del lado JS (

Al usar Typed Arrays , puede escribir de manera eficiente en la memoria WASM desde el lado JS usando set () con Wasm Array Buffer.

Escribiendo en Wasm Memory desde dentro de Wasm / AS, todavía estoy usando el enfoque estándar para bucle, por ejemplo: https://github.com/torch2424/wasmBoy/blob/master/core/memory/dma.ts#L29

Sin embargo, tal vez @MaxGraey o @dcodeIO tal vez podrían ayudar con la forma de hacer esto de manera más eficiente desde dentro de AS o Wasm land. 😄 Aunque es mejor que cambiemos esto al repositorio de AS

@fmkang Simplemente

export function allocateF32Array(length: i32): Float32Array {
  return new Float32Array(length);
}

en el lado AS y

let myArray = module.getArray(Float32Array, module.allocateF32Array(length));

en el lado JS, usando el cargador .

@ torch2424 Sí, el código de @binji está en el lado JS, pero let myWasmArrayPtr = instance.exports.allocateF32Array(length) está llamando a una función llamada allocateF32Array en el módulo Wasm (probablemente compilado por AssembleScript). Esta función no se menciona en su fragmento ni es parte de las funciones integradas de AS, por lo que creo que debería implementarla yo mismo. Estoy preguntando cómo obtener este puntero, que apunta a la matriz Wasm correspondiente.

@dcodeIO Gracias. Parece que esto simplifica el paso de matrices. Lo experimentaré cuidadosamente antes de publicar cualquier comentario nuevo aquí. Pero mi módulo falla cuando se carga después de reemplazar mi función con la suya. La consola dice

astest.html:1 Uncaught (in promise) TypeError: WebAssembly Instantiation: Import #0 module="env" error: module is not an object or function

Incluso se bloquea si escribo algo como let a = new Float32Array(10); en cualquier parte de mi código AS.

@dcodeIO El módulo Wasm se bloquea cuando intentamos inicializar una matriz con expresiones (en lugar de literales, como let c: f64[] = [a[0] + b[0], a[1] + b[1]]; ) o asignar valores a elementos de una matriz (como let a: f64[] = [0, 0, 0]; a[0] = 1; o let array = new Array<i32>(); array.push(1); ).

Al principio, pensamos que esto se debe a que no pusimos memory en env , así que escribimos:

var importObject = {
    env: { memory: new WebAssembly.Memory({initial:10}) },
    imports: { imported_func: arg => console.log(arg) }
};
WebAssembly.instantiate(wasmBinary, importObject).then(...);

Pero el problema aún existía. Entonces nos dimos cuenta de que se debe a la falta de abort . Nosotros escribimos:

env: {
    abort(msg, file, line, column) {
        console.error("abort called at main.ts:" + line + ":" + column);
    }
}

o simplemente env: { abort: function(){} } , y el problema resuelto. Sin embargo, no hay ningún mensaje de error y la ejecución del código realmente no "abortó". Aún no conocemos la causa real de este problema.

Somos realmente nuevos en WebAssembly. Escribo esta publicación solo para actualizar. Realmente no necesitas responder.

@fmkang Parece que no está utilizando el cargador de AssemblyScript para crear una instancia del módulo. Si bien puede implementar todo esto usted mismo, el cargador ya agrega funcionalidad básica en torno a las exportaciones de un módulo, como la función de cancelación. Si tiene más preguntas sobre AssemblyScript, no dude en consultar nuestro rastreador de problemas y estaremos encantados de ayudarle :)

Entonces, intenté usar esta técnica:

let myArrayPtr = instance.exports.allocateF32Array(length);
let myArray = new Float32Array(instance.exports.memory.buffer, myArrayPtr, length);

// Use myArray as a normal Float32Array in JavaScript, fill in data, etc.
...

// Process the data in the array.
instance.exports.processF32Array(myArrayPtr, length);

Pero mi función processF32Array da como resultado RuntimeError: memory access out of bounds . No estoy usando el cargador, FWIW. También probé:

let myArray = module.getArray(Float32Array, module.allocateF32Array(length));

Pero mi módulo no tiene un "getArray" y no estoy seguro de cómo se supone que debe obtenerlo.

Estaba buscando cómo copiar matrices desde / hacia WebAssembly y encontré este hilo. Para cualquier otra persona, pude copiar matrices usando AssemblyScript y la biblioteca de @ torch2424 as-bind

Aquí está mi simple prueba:

AssemblyScript

export function sum(arr: Float64Array): f64 {
  let sum: f64 = 0;
  for(let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  return sum;
}

JavaScript (nodo)

const { AsBind } = require("as-bind");
const fs = require("fs");
const wasm = fs.readFileSync(__dirname + "/build/optimized.wasm");

const asyncTask = async () => {
  const asb = await AsBind.instantiate(wasm);

  // Make a large array
  console.time('Making array');
  let arr = new Float64Array(1e8).fill(1);
  console.timeEnd('Making array');

  // Find the sum using reduce
  console.time('Reduce');
  let sum1 = arr.reduce((acc, val) => acc + val, 0);
  console.timeEnd('Reduce');

  // Find the sum with for loops
  console.time('JS For');
  let sum2 = 0;
  for(let i = 0; i < arr.length; i++) sum2 += arr[i];
  console.timeEnd('JS For');

  // Find the sum with WebAssembly
  console.time('Wasm For');
  let sum3 = asb.exports.sum(arr);
  console.timeEnd('Wasm For');

  console.log(sum1, sum2, sum3);
};

asyncTask();

En mi máquina, obtuve el siguiente resultado:

Making array: 789.086ms
Reduce: 2452.922ms
JS For: 184.818ms
Wasm For: 2008.482ms
100000000 100000000 100000000

Parece que hay una buena sobrecarga al copiar la matriz en WebAssembly con este método, aunque puedo imaginar que valdrá la pena por una operación más cara que una suma.

Editar:

Eliminé la suma de AssemblyScript para aislar el tiempo de ejecución de solo la copia de la matriz:

export function sum(arr: Float64Array): f64 {
  let sum: f64 = 0;
  return sum;
}

Y volví a clasificar los puntos de referencia.

Sin suma de Wasm

Making array: 599.826ms
Reduce: 2810.395ms
JS For: 188.623ms
Wasm For: 762.481ms
100000000 100000000 0

Entonces, parece que la copia de datos en sí tomó 762.481ms para copiar más de 100 millones de f64 s.

@pwstegman Creé otro punto de referencia que usa datos de entrada aleatorios y evita que el motor js optimice su ciclo: https://webassembly.studio/?f=5ux4ymi345e

Y los resultados son diferentes para Chrome y FF:

Chrome 81.0.4044.129

js sum: 129.968017578125ms
sum result = 49996811.62100115

js reduce: 1436.532958984375ms
js reduce result = 49996811.62100115

wasm sum: 153.000244140625ms
wasm sum result = 49996811.62100115

wasm reduce: 125.009033203125ms
wasm reduce result = 49996811.62100115

wasm empty: 0.002685546875ms

Hola.
Soy básicamente un recién nacido con respecto a la familiaridad con WASM, y me encontré con el mismo problema que la gente de aquí y en el n. ° 1162. (Estoy preguntando aquí contra # 1162 ya que este tiene actividad más reciente).
Después de una breve investigación, hice una demostración mientras intentaba resolverlo, y terminé con un método en el que expones las vistas en la memoria WASM como matrices escritas para evitar copiar datos dentro y fuera, aquí está la esencia desde el punto de vista de JS :

// need to know the size in advance, that's OK since the impl makes some init 
const fft = new Module.KissFftReal(/*size=*/N);depending on the size anyway

// get view into WASM memory as Float64Array (of size=N in this case)
const input = fft.getInputTimeDataBuffer();

// fill 'input' buffer

// perform transformation, view into WASM memory is returned here (of size=(N + 2) in this case, +2 for Nyquist bin)
const output = fft.transform();

// use transformation result returned in 'output' buffer

¿Es este el camino a seguir? ¿Existe alguna solución mejor? Agradecería cualquier comentario / actualización sobre este tema.

ps, aunque un poco feo, funciona fantástico para mí en la práctica (hasta donde puedo entenderlo y apreciarlo)

pps
aquí está mi pregunta de stackoverflow (aún sin respuesta) con respecto a una preocupación similar:

https://stackoverflow.com/questions/65566923/is-there-a-more-efficient-way-to-return-arrays-from-c-to-javascript

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

Temas relacionados

thysultan picture thysultan  ·  4Comentarios

cretz picture cretz  ·  5Comentarios

mfateev picture mfateev  ·  5Comentarios

ghost picture ghost  ·  7Comentarios

chicoxyzzy picture chicoxyzzy  ·  5Comentarios