Design: Le moyen le plus performant de transmettre le contexte JS/WASM de données

Créé le 12 sept. 2018  ·  18Commentaires  ·  Source: WebAssembly/design

Salut,

imaginons que j'ai quelques 1024 valeurs f32 stockées dans un tampon Float32Array , en attente d'être traitées par le code DSP basé sur WASM :)

Je suis encore assez nouveau sur WebAssembly. J'ai compris que vous ne pouvez transmettre que des valeurs numériques saisies en tant qu'arguments aux fonctions WASM exportées. Cela a du sens pour moi et c'est pourquoi j'ai décidé de transmettre mes données en utilisant la mémoire. ça me va aussi...

Donc, pour passer mes 1024 valeurs, je les assigne directement au .memory . Comme:

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

C'est amusant, mais pour attribuer toutes mes valeurs, je dois parcourir toutes les valeurs pour les attribuer toutes à la mémoire. Eh bien, d'une manière ou d'une autre, cela semble mal en termes de performances. Je me serais attendu à un impl natif. faire ça pour moi. par exemple une clé comme data dans le memoryDescriptor argument de la WebAssembly.Memory constructeur permettant d'initialiser la mémoire avec un ArrayBuffer .

Alors, eh bien, je fais mon appel de fonction WASM et quand l'impl. c'est magique, c'est écrire les valeurs du résultat dans la mémoire. Cela se produit à l'intérieur de la boucle DSP, il n'y a donc pas de surcharge dans mon code WASM pour le faire - pour autant que je puisse voir.

Mais maintenant, après être revenu dans le contexte JS, je dois à nouveau parcourir toute la mémoire juste pour lire toutes les valeurs et construire une autre représentation de données basée sur JS. Et d'une manière ou d'une autre, je m'attendais à un impl natif. être présent à cette fin également. Peut-être quelque chose comme memory.read(Float32Array) pour me renvoyer les données du tampon sous forme de Float32Array , en faisant abstraction du pointeur et du labyrinthe d'itérations.

Est-ce que j'ai raté quelque chose ?
Existe-t-il un meilleur moyen de transmettre de grandes quantités de données depuis/vers WASM que je viens de négliger ?

Merci d'avance et au mieux,
Aron

Commentaire le plus utile

Vous devez décider si vous souhaitez copier des données entre JavaScript et WebAssembly, ou si vous souhaitez que WebAssembly soit propriétaire des données.

Si vous souhaitez copier les données, vous pouvez utiliser TypedArray.prototype.set() plutôt que d'écrire vous-même la boucle for :

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 possède les données, vous pouvez créer une vue sur le tampon WebAssembly.Memory et la transmettre également à vos fonctions 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.

Vous pouvez également utiliser des outils comme embind pour rendre cela un peu plus facile à utiliser.

Tous les 18 commentaires

Vous devez décider si vous souhaitez copier des données entre JavaScript et WebAssembly, ou si vous souhaitez que WebAssembly soit propriétaire des données.

Si vous souhaitez copier les données, vous pouvez utiliser TypedArray.prototype.set() plutôt que d'écrire vous-même la boucle for :

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 possède les données, vous pouvez créer une vue sur le tampon WebAssembly.Memory et la transmettre également à vos fonctions 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.

Vous pouvez également utiliser des outils comme embind pour rendre cela un peu plus facile à utiliser.

Wow, merci @binji ça a l'air génial. C'est exactement ce que je cherchais. Merci également d'avoir pris le temps d'écrire l'exemple de code. Ceci est également très utile. Je vais l'essayer ce soir et demander des améliorations pour l'impl de Loader. qui est utilisé pour l'interfaçage avec WASM dans https://github.com/AssemblyScript/assemblyscript :) (C'est pourquoi je n'ai pas rencontré embind, qui a l'air génial pour une utilisation avec emscripten)

Voyez-vous une option pour étendre la documentation sur webassembly.org à ce sujet ?

Par exemple ici :

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

Je suis prêt à le faire si possible.

Je pense que ce serait aussi une bonne idée de l'expliquer ici:

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

@binji Hors sujet, vous aimerez peut-être https://github.com/torch2424/wasmBoy si vous n'êtes pas déjà au courant du projet. Et ce projet pourrait également bénéficier de la solution que vous avez proposée, car ils utilisent des boucles for pour définir la mémoire ROM.

Voyez-vous une option pour étendre la documentation sur webassembly.org à ce sujet ?

Nous avons convenu que nous devrions mettre à jour webassembly.org, avec ce changement et bien d'autres aussi. Les documents sont toujours issus du référentiel de conception, mais le référentiel de spécifications est beaucoup plus récent et précis.

Ajouter quelques exemples à MDN serait probablement plus facile et meilleur, étant donné que beaucoup plus de développeurs Web utiliseront ce site que webassembly.org.

Hors sujet, vous aimerez peut-être https://github.com/torch2424/wasmBoy si vous n'êtes pas déjà au courant du projet.

Oui, je connais @torch2424. :-) J'ai aussi mon propre émulateur de gameboy basé sur wasm : https://github.com/binji/binjgb

Ajouter quelques exemples à MDN serait probablement plus facile et meilleur, étant donné que beaucoup plus de développeurs Web utiliseront ce site que webassembly.org.

D'accord, je posterai une mise à jour ce soir. Voyons s'il est publié.

Oui, je connais @torch2424. :-) J'ai aussi mon propre émulateur de gameboy basé sur wasm : https://github.com/binji/binjgb

Haha, cool ! Je vais déf. essayez-le :) Je parie que votre émulateur ne plante pas dans Zelda - Link's Awakening quand vous obtenez l'épée (à la plage) ;))

J'espère trouver un peu de temps pour réparer le crash dans wasmBoy aussi :)

Hey! Rad que vous avez trouvé wasmboy ! 😄 Oui, @binji « émulateur s est certainement wayyy haha plus précis! Quelque chose sur lequel je dois travailler, c'est sûr. Mais, ironiquement, j'utilise l'éveil de Link pour tester wasmboy tout le temps haha ​​! Je suis surpris qu'il te tombe dessus.

Ouverture de quelques bogues en fonction des commentaires :
https://github.com/torch2424/wasmBoy/issues/141 - Passage de mémoire
https://github.com/torch2424/wasmBoy/issues/142 - Zelda Crash (Avec des captures d'écran, cela fonctionne pour moi)

Quoi qu'il en soit, je ne veux pas faire dérailler la conversation/le problème, je vais passer aux problèmes que j'ai ouverts. Mais merci pour le retour ! ??

Si WebAssembly possède les données, vous pouvez créer une vue sur le tampon WebAssembly.Memory et la transmettre également à vos fonctions JavaScript :

Une mise en garde importante : la vue dans la mémoire tampon sera invalidée si l'instance WebAssembly augmente sa mémoire. Évitez de stocker des références à la vue qui persistent après d'autres appels dans l'instance WebAssembly.

@binji Pouvez-vous me dire ce que vous mettez du côté AssemblyScript lorsque vous utilisez cela ? J'ai remarqué que ce n'est pas une fonction standard.

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

J'écris comme suit et ça marche, mais je veux quand même savoir si c'est d'une manière correcte.

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

Dois-je écrire une autre fonction AS pour la libérer avec memory.free() et l'appeler après avoir transféré le tableau du côté JS ?

@fmkang Donc, ce que @binji a expliqué était du côté JS (veuillez me corriger si je me trompe).

En utilisant Typed Arrays , vous pouvez effectuer une écriture efficace dans la mémoire WASM du côté JS en utilisant set() avec le Wasm Array Buffer.

En écrivant dans Wasm Memory depuis Wasm/AS, j'utilise toujours l'approche de boucle standard, par exemple : https://github.com/torch2424/wasmBoy/blob/master/core/memory/dma.ts#L29

Cependant, peut-être que @MaxGraey ou @dcodeIO pourraient vous aider à le faire le plus efficacement possible depuis AS ou Wasm Land ? 😄 Bien qu'il soit préférable de déplacer cela vers le référentiel AS

@fmkang Vous voudriez simplement

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

côté AS et

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

côté JS, à l' aide

@torch2424 Oui, le code de @binji est du côté JS, mais let myWasmArrayPtr = instance.exports.allocateF32Array(length) appelle une fonction nommée allocateF32Array dans le module Wasm (probablement compilé par AssembleScript). Cette fonction n'est pas mentionnée dans son extrait ni dans les fonctions intégrées d'AS, je pense donc qu'elle devrait être implémentée par moi-même. Je demande comment obtenir ce pointeur, qui pointe vers le tableau Wasm correspondant.

@dcodeIO Merci. Il semble que cela simplifie le passage des tableaux. Je vais l'expérimenter soigneusement avant de poster de nouveaux commentaires ici. Mais mon module plante au chargement après avoir remplacé ma fonction par la vôtre. La console dit

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

Il plante même si j'écris quelque chose comme let a = new Float32Array(10); n'importe où dans mon code AS.

@dcodeIO Le module Wasm plante lorsque nous essayons d'initialiser un tableau avec des expressions (au lieu de littéraux, comme let c: f64[] = [a[0] + b[0], a[1] + b[1]]; ) ou d'attribuer des valeurs aux éléments d'un tableau (comme let a: f64[] = [0, 0, 0]; a[0] = 1; ou let array = new Array<i32>(); array.push(1); ).

Au début, nous pensions que c'était parce que nous n'avions pas mis memory dans env , alors nous avons écrit :

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

Mais le problème existait toujours. Ensuite, nous avons découvert que c'était à cause du manque de abort . On a écrit:

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

ou simplement env: { abort: function(){} } , et le problème est résolu. Cependant, il n'y a pas de message d'erreur, et l'exécution du code n'a pas vraiment « avorté ». Nous ne connaissons toujours pas la cause réelle de ce problème.

Nous sommes vraiment nouveaux sur WebAssembly. J'écris ce post juste pour donner une mise à jour. Vous n'avez pas vraiment besoin de répondre.

@fmkang Il semble que vous n'utilisiez pas le chargeur AssemblyScript pour instancier le module. Bien que vous puissiez implémenter tout cela vous-même, le chargeur ajoute déjà des fonctionnalités de base autour des exportations d'un module, comme la fonction d'abandon. Si vous avez d'autres questions concernant AssemblyScript, n'hésitez pas à les poser sur notre outil de suivi des problèmes et nous serons ravis de vous aider :)

J'ai donc essayé d'utiliser cette technique :

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

Mais ma fonction processF32Array donne RuntimeError: memory access out of bounds . Je n'utilise pas le chargeur, FWIW. J'ai aussi essayé :

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

Mais mon module n'a pas de "getArray" et je ne sais pas comment il est censé l'obtenir.

Je cherchais comment copier des tableaux vers/depuis WebAssembly et je suis tombé sur ce fil. Pour n'importe qui d'autre, j'ai pu copier des tableaux à l'aide d' AssemblyScript et de la bibliothèque as-bind de @torch2424

Voici mon test simple :

Script d'assemblage

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

JavaScript (nœud)

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

Sur ma machine, j'ai le résultat suivant :

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

On dirait qu'il y a une bonne surcharge lors de la copie du tableau sur WebAssembly avec cette méthode, bien que je puisse imaginer que le compromis en vaudra la peine pour une opération plus coûteuse qu'une somme.

Éditer:

J'ai supprimé la sommation de AssemblyScript pour isoler le temps d'exécution de la copie du tableau uniquement :

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

Et j'ai réexécuté les repères.

Sans sommation Wasm

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

Il semble donc que la copie des données elle-même ait pris 762,481 ms pour copier plus de 100 millions de f64 s.

@pwstegman J'ai créé un autre benchmark qui utilise des données d'entrée aléatoires et empêche le moteur js d'optimiser votre boucle : https://webassembly.studio/?f=5ux4ymi345e

Et les résultats sont différents pour Chrome et 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

Bonjour.
Je suis fondamentalement un nouveau-né en ce qui concerne la familiarité avec WASM, et je suis tombé sur le même problème que les gens ici et dans #1162. (Je demande ici vs #1162 puisque celui-ci a une activité plus récente.)
Après de brèves recherches, j'ai fait une démo en essayant de le comprendre, et j'ai fini avec une méthode où vous exposez des vues dans la mémoire WASM en tant que tableaux typés pour éviter de copier des données à l'intérieur et à l'extérieur, voici l'essentiel du point de vue 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

Est-ce la voie à suivre ? Existe-t-il une meilleure solution ? J'apprécierais tout commentaire / rafraîchissement sur ce sujet.

ps bien qu'un peu moche, cela fonctionne fantastiquement pour moi dans la pratique (pour autant que je puisse le comprendre et l'apprécier)

pp
voici ma question (encore sans réponse) de stackoverflow concernant un problème similaire:

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

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

thysultan picture thysultan  ·  4Commentaires

aaabbbcccddd00001111 picture aaabbbcccddd00001111  ·  3Commentaires

Artur-A picture Artur-A  ·  3Commentaires

beriberikix picture beriberikix  ·  7Commentaires

JimmyVV picture JimmyVV  ·  4Commentaires