Design: Die leistungsfähigste Methode zum Übergeben von JS/WASM-Kontexten

Erstellt am 12. Sept. 2018  ·  18Kommentare  ·  Quelle: WebAssembly/design

Hi,

Stellen wir uns vor, ich habe einige 1024 f32 Werte, die in einem Float32Array Puffer gespeichert sind und darauf warten, von WASM-basiertem DSP-Code verarbeitet zu werden :)

Ich bin noch ziemlich neu bei WebAssembly. Ich habe verstanden, dass Sie nur typisierte numerische Werte als Argumente an exportierte WASM-Funktionen übergeben können. Das macht für mich Sinn und deshalb habe ich mich entschieden, meine Daten über den Speicher weiterzugeben. damit komme ich auch gut zurecht...

Um also meine 1024 Werte zu übergeben, weise ich sie direkt den .memory . Mögen:

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

Das macht Spaß, aber um alle meine Werte zuzuweisen, muss ich alle Werte durchlaufen, um sie alle dem Speicher zuzuweisen. Nun, irgendwie fühlt sich das leistungsmäßig falsch an. Ich hätte einen einheimischen Impl erwartet. um das für mich zu tun. zB ein Schlüssel wie data im memoryDescriptor Argument des WebAssembly.Memory Konstruktors, der es erlaubt den Speicher mit einem ArrayBuffer zu initialisieren.

Also gut, dann mache ich meinen WASM-Funktionsaufruf und wenn die WASM-Impl. tat es magisch, es schreibt die Ergebniswerte zurück in den Speicher. Dies geschieht innerhalb der DSP-Schleife, daher gibt es in meinem WASM-Code keinen Overhead dafür - soweit ich sehen kann.

Aber jetzt, nachdem ich wieder im JS-Kontext bin, muss ich noch einmal über den gesamten Speicher iterieren, nur um alle Werte zu lesen und eine andere JS-basierte Datendarstellung zu erstellen. Und irgendwie erwartete ich einen einheimischen Impl. auch hierfür anwesend sein. Vielleicht so etwas wie memory.read(Float32Array) , um mir die Pufferdaten als Float32Array , was den Zeiger und das Iterationslabyrinth abstrahiert.

Verpasse ich etwas?
Gibt es eine bessere Möglichkeit, große Datenmengen von/zu WASM zu übergeben, die ich gerade übersehen habe?

Danke im Voraus und Beste,
Aron

Hilfreichster Kommentar

Sie müssen entscheiden, ob Sie Daten zwischen JavaScript und WebAssembly kopieren möchten oder ob WebAssembly die Daten besitzen soll.

Wenn Sie die Daten kopieren möchten, können Sie TypedArray.prototype.set() verwenden, anstatt die for Schleife selbst zu schreiben:

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

Wenn WebAssembly die Daten besitzt, können Sie eine Ansicht über den WebAssembly.Memory Puffer erstellen und diese auch an Ihre JavaScript-Funktionen übergeben:

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.

Sie können auch Tools wie embind verwenden , um die Verwendung etwas einfacher zu machen.

Alle 18 Kommentare

Sie müssen entscheiden, ob Sie Daten zwischen JavaScript und WebAssembly kopieren möchten oder ob WebAssembly die Daten besitzen soll.

Wenn Sie die Daten kopieren möchten, können Sie TypedArray.prototype.set() verwenden, anstatt die for Schleife selbst zu schreiben:

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

Wenn WebAssembly die Daten besitzt, können Sie eine Ansicht über den WebAssembly.Memory Puffer erstellen und diese auch an Ihre JavaScript-Funktionen übergeben:

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.

Sie können auch Tools wie embind verwenden , um die Verwendung etwas einfacher zu machen.

Wow, danke @binji das sieht toll aus. Es ist genau das, wonach ich gesucht habe. Vielen Dank auch, dass Sie sich die Zeit genommen haben, den Beispielcode zu schreiben. Dies ist auch sehr hilfreich. Ich werde es heute Abend versuchen und per Pull Request einige Verbesserungen für den Loader impl. die für die Schnittstelle mit WASM in https://github.com/AssemblyScript/assemblyscript verwendet wird :) (Deshalb bin ich nicht auf embind gestoßen, was für die Verwendung mit emscripten fantastisch aussieht)

Sehen Sie eine Option, die Dokumentation auf webassembly.org diesbezüglich zu erweitern?

Zum Beispiel hier:

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

Ich bin bereit, dies zu tun, wenn möglich.

Ich denke, es wäre auch eine gute Idee, es hier zu erklären:

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

@binji Off-topic, Sie mögen https://github.com/torch2424/wasmBoy, wenn Sie das Projekt noch nicht kennen. Und dieses Projekt könnte auch von der von Ihnen vorgeschlagenen Lösung profitieren, da sie for-Schleifen verwenden, um den ROM-Speicher einzustellen.

Sehen Sie eine Option, die Dokumentation auf webassembly.org diesbezüglich zu erweitern?

Vereinbart, dass wir webassembly.org aktualisieren sollten, mit dieser Änderung und vielem mehr. Die dortigen Dokumente stammen noch aus dem Design-Repository, aber das Spec-Repository ist viel aktueller und genauer.

Einige Beispiele zu MDN hinzuzufügen wäre wahrscheinlich einfacher und besser, wenn man bedenkt, dass viel mehr Webentwickler diese Site nutzen werden als webassembly.org.

Off-Topic, Sie mögen https://github.com/torch2424/wasmBoy, wenn Sie das Projekt noch nicht kennen.

Ja, ich kenne @torch2424. :-) Ich habe auch meinen eigenen wasm-basierten Gameboy-Emulator: https://github.com/binji/binjgb

Einige Beispiele zu MDN hinzuzufügen wäre wahrscheinlich einfacher und besser, wenn man bedenkt, dass viel mehr Webentwickler diese Site nutzen werden als webassembly.org.

Okay, ich werde heute Abend ein Update posten. Mal sehen ob es veröffentlicht wird.

Ja, ich kenne @torch2424. :-) Ich habe auch meinen eigenen wasm-basierten Gameboy-Emulator: https://github.com/binji/binjgb

Haha cool! Ich werde def. Probier es aus :) Ich wette, dein Emulator stürzt in Zelda - Link's Awakening nicht ab, wenn du das Schwert bekommst (am Strand) ;))

Ich hoffe, dass ich auch etwas Zeit finde, um den Absturz in wasmBoy zu beheben :)

Hey! Rad, dass du wasmboy gefunden hast! 😄 Ja, @binji ‚s - Emulator ist definitiv wayyy genauer haha! Etwas, an dem ich auf jeden Fall arbeiten muss. Aber ironischerweise benutze ich Link's Erwachen die ganze Zeit, um Wasmboy zu testen, haha! Ich bin überrascht, dass es auf dich abgestürzt ist.

Einige Fehler wurden basierend auf dem Feedback geöffnet:
https://github.com/torch2424/wasmBoy/issues/141 - Gedächtnisübergabe
https://github.com/torch2424/wasmBoy/issues/142 - Zelda Crash (Mit Screenshots funktioniert es bei mir)

Wie auch immer, ich möchte das Gespräch/das Problem nicht entgleisen, ich gehe zu den Problemen über, die ich eröffnet habe. Aber danke für die Rückmeldung! 😄

Wenn WebAssembly die Daten besitzt, können Sie eine Ansicht über den WebAssembly.Memory-Puffer erstellen und diese auch an Ihre JavaScript-Funktionen übergeben:

Eine wichtige Einschränkung: Die Ansicht in den Speicherpuffer wird ungültig, wenn die WebAssembly-Instanz ihren Speicher vergrößert. Vermeiden Sie das Speichern von Verweisen auf die Ansicht, die nach weiteren Aufrufen in der WebAssembly-Instanz bestehen bleiben.

@binji Können Sie mir sagen, was Sie auf der AssemblyScript-Seite platzieren, wenn Sie dies verwenden? Mir ist aufgefallen, dass es keine Standardfunktion ist.

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

Ich schreibe wie folgt und es funktioniert, aber ich möchte trotzdem wissen, ob es richtig ist.

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

Muss ich eine andere AS-Funktion schreiben, um sie mit memory.free() , und sie aufrufen, nachdem ich das Array auf die JS-Seite übertragen habe?

@fmkang Also, was @binji erklärte, war auf der JS-Seite (bitte korrigiert mich, wenn ich falsch

Mit Typed Arrays können Sie von der JS-Seite aus effizient in den WASM-Speicher schreiben, indem Sie set() mit dem Wasm Array Buffer verwenden.

Ich schreibe in Wasm Memory von innerhalb von Wasm/AS, ich verwende immer noch den Standard-For-Loop-Ansatz, zB: https://github.com/torch2424/wasmBoy/blob/master/core/memory/dma.ts#L29

Aber vielleicht könnten @MaxGraey oder @dcodeIO dabei helfen, wie man dies am effizientesten von AS oder Wasm Land aus macht? 😄 Obwohl es vielleicht besser ist, dies in das AS-Repository zu verschieben

@fmkang Du würdest einfach

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

auf der AS-Seite und

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

auf der JS-Seite mit dem Loader .

@torch2424 Ja, @binjis Code ist auf der JS-Seite, aber let myWasmArrayPtr = instance.exports.allocateF32Array(length) ruft eine Funktion namens allocateF32Array im Wasm-Modul auf (wahrscheinlich von AssembleScript kompiliert). Diese Funktion wird weder in seinem Snippet erwähnt noch ist sie Teil der AS-integrierten Funktionen, daher denke ich, dass sie von mir selbst implementiert werden sollte. Ich frage, wie man diesen Zeiger erhält, der auf das entsprechende Wasm-Array zeigt.

@dcodeIO Danke. Es scheint, dass dies die Übergabe von Arrays vereinfacht. Ich werde es sorgfältig ausprobieren, bevor ich neue Kommentare hier poste. Aber mein Modul stürzt beim Laden ab, nachdem ich meine Funktion durch deine ersetzt habe. Die Konsole sagt

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

Es stürzt sogar ab, wenn ich irgendwo in meinem AS-Code etwas wie let a = new Float32Array(10); schreibe.

@dcodeIO Das Wasm-Modul stürzt ab, wenn wir versuchten, ein Array mit Ausdrücken (anstelle von Literalen wie let c: f64[] = [a[0] + b[0], a[1] + b[1]]; ) zu initialisieren oder Elementen eines Arrays Werte zuzuweisen (wie let a: f64[] = [0, 0, 0]; a[0] = 1; oder let array = new Array<i32>(); array.push(1); ).

Zuerst dachten wir, das liegt daran, dass wir memory in env , also schrieben wir:

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

Aber das Problem bestand immer noch. Dann stellten wir zufällig fest, dass es am Mangel an abort . Wir schrieben:

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

oder einfach env: { abort: function(){} } , und das Problem ist gelöst. Es kommt jedoch keine Fehlermeldung, und die Ausführung des Codes ist nicht wirklich "abgebrochen". Wir kennen immer noch nicht die eigentliche Ursache dieses Problems.

Wir sind wirklich neu bei WebAssembly. Ich schreibe diesen Beitrag nur, um ein Update zu geben. Sie brauchen nicht wirklich zu antworten.

@fmkang Anscheinend verwenden Sie den AssemblyScript-Loader nicht , um das Modul zu instanziieren. Während Sie all dies selbst implementieren können, fügt der Loader bereits grundlegende Funktionen rund um die Exporte eines Moduls hinzu, wie die Abbruchfunktion. Wenn Sie weitere Fragen zu AssemblyScript haben, wenden Sie sich bitte an unseren Issue Tracker und wir helfen Ihnen gerne weiter :)

Also habe ich es mit dieser Technik versucht:

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

Aber meine Funktion processF32Array ergibt RuntimeError: memory access out of bounds . Ich benutze nicht den Loader, FWIW. Ich habe auch probiert:

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

Aber mein Modul hat kein "getArray", und ich bin mir nicht sicher, wie es es bekommen soll.

Ich war auf der Suche nach dem Kopieren von Arrays zu / von WebAssembly und bin auf diesen Thread gestoßen. Für alle anderen konnte ich Arrays mit AssemblyScript und der Bibliothek von @torch2424 as-bind kopieren

Hier ist mein einfacher Test:

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

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

Auf meinem Rechner habe ich folgende Ausgabe erhalten:

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

Es sieht so aus, als ob es beim Kopieren des Arrays mit dieser Methode zu WebAssembly ein gutes Stück Overhead gibt, obwohl ich mir vorstellen kann, dass sich der Kompromiss für eine teurere Operation als eine Summe lohnen wird.

Bearbeiten:

Ich habe die Summation aus dem AssemblyScript entfernt, um die Laufzeit nur der Array-Kopie zu isolieren:

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

Und ich habe die Benchmarks erneut ausgeführt.

Ohne Wasm-Summation

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

Es sieht also so aus, als ob die Datenkopie selbst 762.481 ms benötigt hat, um über 100 Millionen f64 s zu kopieren.

@pwstegman Ich habe einen weiteren Benchmark erstellt, der zufällige Eingabedaten verwendet und verhindert, dass die js-Engine Ihre Schleife optimiert: https://webassembly.studio/?f=5ux4ymi345e

Und die Ergebnisse sind für Chrome & FF unterschiedlich:

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

Hallo.
Ich bin im Grunde ein Neugeborenes in Bezug auf Vertrautheit mit WASM, und ich stolperte über das gleiche Problem wie die Leute hier und in #1162. (Ich frage hier im Vergleich zu #1162, da dieser neuere Aktivitäten aufweist.)
Nach kurzer Recherche machte ich eine Demo, während ich versuchte, es herauszufinden, und landete bei einer Methode, bei der Sie Ansichten in den WASM-Speicher als typisierte Arrays bereitstellen, um das Kopieren von Daten zu vermeiden. Hier ist das Wesentliche aus der Sicht von 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

Ist das der richtige Weg? Gibt es eine bessere Lösung? Würde mich über jeden Kommentar/Auffrischung zu diesem Thema freuen.

ps obwohl irgendwie hässlich, es funktioniert für mich in der Praxis fantastisch (soweit ich es verstehen und schätzen kann)

pps
Hier ist meine (noch unbeantwortete) Stackoverflow-Frage zu ähnlichen Bedenken:

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

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

frehberg picture frehberg  ·  6Kommentare

ghost picture ghost  ·  7Kommentare

arunetm picture arunetm  ·  7Kommentare

nikhedonia picture nikhedonia  ·  7Kommentare

aaabbbcccddd00001111 picture aaabbbcccddd00001111  ·  3Kommentare