Design: Kann WebAssembly.Instance ein bereits kompiliertes Modul neu kompilieren?

Erstellt am 27. Okt. 2016  ·  94Kommentare  ·  Quelle: WebAssembly/design

Angenommen, ich habe diesen Code:

let descriptor = /* ... */;
let memories = Array.apply(null, {length: 1024}).map(() => new WebAssembly.Memory(descriptor));
let instance = fetch('foo.wasm')
  .then(response => response.arrayBuffer())
  .then(buffer => WebAssembly.compile(buffer))
  .then(module => new WebAssembly.Instance(module, { memory: memories[1023] }));

Ist WebAssembly.Instance erlaubt, für einen beträchtlichen Zeitraum zu blockieren. Könnte es zum Beispiel das WebAssembly.Module neu kompilieren?

In den meisten Fällen würde ich nein sagen, aber was ist, wenn der bereits kompilierte Code den Speicher, den er empfängt, nicht besonders mag? Sagen wir, weil dieser Speicher ein Slow-Mode-Speicher ist und der Code unter Annahme des Fast-Mode kompiliert wurde? vielleicht war memories[0] ein Schnellmodus-Speicher, aber memories[1023] sicher nicht sein.

Was ist stattdessen mit diesem Code:

let instances = [0,1,2,3,4,5,6,7].map(v => fetch(`foo${v}.wasm`)
  .then(response => response.arrayBuffer())
  .then(buffer => WebAssembly.compile(buffer))
  .then(module => new WebAssembly.Instance(module)));

Dürfen diese Aufrufe von WebAssembly.Instance eine Neukompilierung bewirken?

Unter der Annahme, dass das oben Genannte sinnvoll ist, sind hier einige verwandte Fragen:

  • Wollen wir eine asynchrone Funktion, die ein Versprechen zurückgibt, die _und_ instanziieren kann? Ich sage nicht, dass wir eine der synchronen und asynchronen APIs, die wir bereits haben, löschen sollten, ich schlage eine neue asynchrone API vor.
  • Wie stellt ein Browser dar, dass kompilierter Code in einer WebAssembly.Module WebAssembly.Memory Instanz schnell ist und dass eine
  • Woher weiß ein Benutzer, wie viele WebAssembly.Memory Instanzen ihm erlaubt sind, bevor er langsamen Code erhält (zählt die impliziten, zB wie im zweiten Beispiel erzeugt)?
JS embedding

Hilfreichster Kommentar

@kgryte Ich hätte klarstellen sollen, dass sich mein Kommentar hauptsächlich auf den Browser als Ausführungskontext bezog. Wir sind auf einer API-Oberfläche gelandet, die immer noch die synchronen APIs verfügbar macht. Browser können Modulen, die an die synchronen APIs übergeben werden, eine Größenbeschränkung auferlegen (Chrome tut dies beispielsweise bereits), aber diese Beschränkung ist vom Einbetter konfigurierbar und sollte nicht für Node.

Alle 94 Kommentare

Es wäre schön, wenn WebAssembly.Instance manchmal eine Neukompilierung verursacht, auf diese Weise könnten unveränderliche globale Variablen im generierten Code konstant gefaltet werden. Emscripten generiert beispielsweise verschiebbaren Code, indem alle Zeiger auf statische Daten verschoben werden. Der Offset wird bei der Instanziierung des Moduls als unveränderliche globale Variable übergeben. Wenn WebAssembly.Instance neu kompilieren kann, könnte es den generierten Code spezialisieren.

Die Spezifikation definiert nicht, was "Kompilierung" ist und würde sie auch nicht machen
Sinn dafür, denn Umsetzungsansätze können sehr unterschiedlich sein
(einschließlich Dolmetscher). Es kann also hierzu keine normative Aussage treffen
in jedem Fall. Das Beste, was wir tun können, ist eine Notiz hinzuzufügen, die
WebAssembly.Instance soll "schnell" sein.

Am 27. Oktober 2016 um 03:24, Michael Bebenita [email protected]
schrieb:

Wäre schön, wenn WebAssembly.Instance manchmal eine Neukompilierung verursacht,
Auf diese Weise könnten unveränderliche globale Variablen im generierten konstant gefaltet werden
Code. Emscripten generiert beispielsweise verschiebbaren Code, indem alle
Zeiger auf statische Daten. Der Offset wird als unveränderliche globale Variable übergeben
wenn das Modul instanziiert wird. Wenn WebAssembly.Instance neu kompiliert werden kann,
es könnte den generierten Code spezialisieren.


Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/WebAssembly/design/issues/838#issuecomment -256522163,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/AEDOO9sJPgujK3k0f6P7laYV_zaJxES5ks5q3_1LgaJpZM4Kh1gM
.

Zugegeben, dies wäre höchstens ein nicht normativer Hinweis.

In SM beabsichtigen wir derzeit auch, dass die Instanziierung nie neu kompiliert wird, damit es ein vorhersehbares Kompilierungskostenmodell für Entwickler gibt (insbesondere damit Entwickler WebAssembly.compile und IDB verwenden können, um zu kontrollieren, wann sie den Kompilierungstreffer nehmen). . Die Neukompilierung zur Instanziierungszeit innerhalb des synchronen Instance Konstruktors würde dieses Kostenmodell sicherlich durchbrechen und könnte zu einem großen Fehler führen.

Ich weiß jedoch zu schätzen, dass eine separate Kompilierung grundsätzlich im Widerspruch zu einer Vielzahl von Optimierungen steht, die man möglicherweise durchführen möchte, um generierten Code auf Umgebungsparameter zu spezialisieren. Die Verschmelzung von Kompilierung und Instanziierung in einer asynchronen Operation ist sinnvoll und wurde in der Vergangenheit in Betracht gezogen. Der Nachteil ist natürlich, dass dies explizites Caching verhindert (es gibt kein Module ), sodass der Entwickler einen unangenehmen Kompromiss eingehen muss. Einige Optionen:

  • Das impl könnte implizites inhaltsadressiertes Caching durchführen (das Umgebungsparameter im Schlüssel enthalten könnte), wie wir es mit asm.js derzeit in FF tun. Dies wäre ein bisschen mühsam und hat alle Vorhersagbarkeits- / Heuristikprobleme eines impliziten Caches.
  • Wir könnten einen neuen Weg erstellen (z. B. eine neue WebAssembly.Cache API, bei der Sie Bytecode und Instanziierungsparameter übergeben und eine Promise<Instance> .

Letzteres fasziniert mich und könnte eine viel angenehmere Entwicklererfahrung bieten als die Verwendung von IDB und vielleicht eine Chance, das Caching noch weiter zu optimieren (da der Cache auf seinen Zweck spezialisiert ist), aber es ist sicherlich ein großes Feature und etwas, das wir etwas Zeit in Anspruch nehmen möchten berücksichtigen.

@rossberg-chromium Ich scheine meinen Zweck schlecht erklärt zu haben: Ich möchte nicht darüber streiten, was die Spezifikation sagt. Ich versuche, darauf hinzuweisen, was für Entwickler eine ernsthafte Überraschung zu sein scheint, die sich unter der API verbirgt. Ein Entwickler erwartet nicht, dass das Ergebnis von .compile neu kompiliert wird. Das scheint mir ein Konstruktionsfehler zu sein.

@lukewagner selbst bei implizitem oder explizitem Caching haben wir möglicherweise das gleiche Problem: Wie viele WebAssembly.Memory im selben Adressraum / Ursprung erstellt werden können, ist eine Browserbeschränkung. Mir gefällt, was Sie vorschlagen, aber ich denke, es ist orthogonal zum Thema. Lassen Sie mich wissen, wenn ich Ihren Vorschlag falsch verstanden habe.

Vielleicht könnten .compile und Module Memory , und Instance hat eine .memory Eigenschaft, die an andere Zusammenstellungen / Instanziierungen übergeben werden kann ?

Ich versuche nicht, die Möglichkeit der Neukompilierung zu eliminieren, ich denke, wir wollen eher eine gemeinsame idiomatische API-Nutzung, die perfekte Informationen bezüglich Memory beim ersten Kompilieren (oder beim Abrufen des Caches) enthält, also ob die Kompilierung bei Bedarf Grenzüberprüfungen ausgibt oder nicht.

@jfbastien Bei implizitem/explizitem Caching, das den jeweiligen Instanziierungsparametern bereitgestellt wurde (also Memory ), sehe ich keine Notwendigkeit für eine Neukompilierung.

@jfbastien Bei implizitem/explizitem Caching, das den jeweiligen Instanziierungsparametern bereitgestellt wurde (also Memory ), sehe ich keine Notwendigkeit für eine Neukompilierung.

Es kann sein:

  1. Erstellen Sie viele Memory s.
  2. Kompilieren Sie Code mit expliziter (langsamer) Überprüfung der Grenzen, da zu viele Memory ies vorhanden waren.
  3. Cachen Sie diesen Code.
  4. Seite verlassen.
  5. Seite erneut laden.
  6. Weisen Sie nur einen Memory , der die schnelle Version erhält.
  7. Holen Sie sich aus dem Cache.
  8. Langsamen Code Instance .

An dieser Stelle stimme ich zu, dass Sie keine Neukompilierung _brauchen_, aber wir sind ein bisschen albern, wenn wir langsame Grenzüberprüfungen durchführen, wenn wir es nicht müssen.

Wie ich schon sagte: Ich mag diese Cache API, die Sie vorschlagen, ich denke, sie macht WebAssembly besser nutzbar, aber ich denke, das Problem besteht immer noch. 😢

Nun, das ist mein Punkt mit einem erweiterten Cache, der Instanziierungsparameter und Bytecode akzeptiert: Der Cache kann neu kompiliert werden, wenn das, was er zwischengespeichert hat, nicht mit den Instanziierungsparametern übereinstimmt. Die Schritte wären also nur:

  1. erstelle viele Memory s
  2. Fordern Sie ein Instance aus dem Cache an und übergeben Sie eines dieser (langsamen) Memory s
  3. Slow-Code wird kompiliert, zwischengespeichert und als Instance
  4. Seite verlassen
  5. Seite neu laden
  6. nur ein Memory zuordnen
  7. Fordern Sie ein Instance aus dem Cache an und bestehen Sie das schnelle Memory
  8. Fast-Code wird kompiliert, zwischengespeichert und als Instance

und nach Schritt 8 erhalten alle zukünftigen Seitenladevorgänge schnellen oder langsamen Code im Cache.

@lukewagner Zunächst schlagen Sie eine Minderung vor, die dem erklärten Ziel von WebAssembly zuwiderläuft, deterministische Leistung bereitzustellen. Der Unterschied zwischen langsam und schnell wurde zuletzt mit etwa 20 % angegeben. Es würde also wirklich stinken, wenn eine Spezifikation, die akribisch auf eine deterministische Leistung abzielt, sie aufgrund einer API-Eigenart auf den Boden fallen lässt. Ich glaube nicht, dass der Browser mit einem inhaltsadressierten Cache die richtige Lösung ist, denn die Spezifikation gibt sich bereits an anderer Stelle viel Mühe, um die Notwendigkeit von Profil-Neukompilierungs-Cache-Optimierungen zu vermeiden. Zum Beispiel versprechen wir die Kompilierung genau, damit die App auch dann ein vernünftiges Verhalten erzielen kann, wenn der Code nicht zwischengespeichert wird. Wenn die Art und Weise, wie dies spezifiziert ist, von uns allen die Implementierung von Caches oder anderen Abschwächungen erfordert, haben wir unser Ziel verfehlt, den Leuten ein vernünftig tragbares Kostenmodell zu bieten.

Für mich ist das Problem nur folgendes: Eine der Optimierungen, die wir alle aus Wettbewerbsgründen effektiv durchführen müssen (die Überprüfung der 4 GB virtuellen Speichergrenzen, die ich einfach den 4 GB-Hack nenne) kann in der aktuellen Spezifikation nicht ohne Einbußen durchgeführt werden eines dieser Dinge:

  • Sie können damit davonkommen, wenn Sie immer 4 GB virtuellen Speicher für jeden Wasm-Speicher zuweisen. Dies wird Benutzer davon abhalten, WebAssembly für kleine Module zu verwenden, da Sie die Zuweisungsgrenzen für virtuellen Speicher, die Fragmentierung des virtuellen Speichers oder andere Probleme erreichen, wenn Sie viele davon zuweisen. Ich befürchte auch, dass Sie die Wirksamkeit von Sicherheitsmaßnahmen wie ASLR verringern, wenn Sie viele davon zuweisen. Beachten Sie, dass vorhandene APIs diese Gefahr nicht teilen, da sie den Speicher, den sie zuweisen, festschreiben und entweder OOM werden oder abstürzen, bevor Sie viel mehr zuweisen lassen, als der physische Speicher zulässt.
  • Sie können damit davonkommen, wenn Sie eine Neukompilierung zulassen, wenn Sie während der Instanziierung eine Nichtübereinstimmung feststellen (kompilierter Code will 4 GB hacken, aber der Speicher verfügt nicht über diese virtuelle Speicherzuweisung). Sie könnten auch damit durchkommen, wenn die Instanziierung den Speicher in eine 4-GB-Region verschiebt, aber siehe den vorherigen Punkt. Also, wahrscheinlich immer wenn dies passiert, wird es ein P1-Bug für den Browser sein, der darauf gestoßen ist.

Ich denke, dies bedeutet, dass die Spezifikation die Anbieter dazu ermutigen wird, immer nur 4 GB-Reservierungen zuzulassen, wenn wasm-Speicher zugewiesen wird, oder Cache- / Lazy-Compile- / Profiloptimierungen durchzuführen, um dies zu erkennen.

Schließlich verstehe ich den Sinn nicht, dies nicht normativ zu machen. Dies kann normativ sein, da wir die API dazu bringen könnten, die Möglichkeit auszuschließen, dass der Browser etwas kompilieren muss, ohne zu wissen, welche Art von Speicher er haben wird. Ich stelle mir vor, dass es viele Möglichkeiten gibt, dies zu tun. Beispielsweise könnte die Instanziierung ein Promise zurückgeben und wir könnten den separaten Kompilierungsschritt entfernen. Dies würde deutlich machen, dass die Instanziierung der Schritt ist, der eine Weile dauern kann, was für den Client stark impliziert, dass dies der Schritt ist, der die Kompilierung durchführt. In einer solchen API weiß der Compiler immer, ob der Speicher, für den er kompiliert, den 4 GB-Hack hat oder nicht.

Es ist traurig, dass wir dies erst jetzt bemerken, aber ich bin überrascht, dass ihr Jungs nicht sieht, dass dies ein größeres Problem ist. Gibt es eine andere Abschwächung als das Caching, die ich übersehe?

@jfbastien In Ihrem motivierenden Szenario haben Sie darauf hingewiesen, dass das Modul so geschrieben wurde, dass es schnellen Speicher bevorzugt. Ich gehe davon aus, dass Sie in erster Linie darauf aus sind, die schnelle Speicheroptimierung zu aktivieren, wenn ein bestimmtes Modul dies wünscht, und es möglicherweise in Ordnung ist, dies nicht zu tun, wenn das Modul dies nicht möchte (nichts Schlimmes, wenn Sie in diesem Fall auch opportunistisch darüber stolpern). , nur versuchen, Prioritäten auseinander zu setzen).

Wenn ja, wie würden sich diese Alternativen zum Caching oder zum asynchronen Instanziieren anfühlen:

  1. Modulautor muss 4 GB als min./max. Speicher benötigen
  2. Eine Variante des Kompilierens (zumindest async, vielleicht auch sync), die eine Instanz erzeugt, die nur schnellen Speicher akzeptiert.

Wäre es für das Problem des "4GB-Hack" und der Diskrepanzen zwischen dem Speicher, der ihn verwendet, und dem Code, der ihn erwartet, für die Kompilierung sinnvoll, intern zwei Versionen des Codes auszugeben? (Natürlich würde dies mehr Speicher verbrauchen, was traurig ist, aber hoffentlich wäre die Kompilierzeit nicht viel schlechter, der Autor könnte beides gleichzeitig generieren?)

@mtrofin Ich denke nicht, dass es sinnvoll ist, nach 4GiB zu fragen, wenn Sie es nicht verwenden möchten. Die virtuelle Zuweisung ist von der Nutzungsabsicht getrennt, daher denke ich, dass wir beide trennen müssen.

Zu 2.: Es ist immer noch nicht sehr hilfreich für den Entwickler: Wenn sie diese Variante verwenden und sie fehlschlägt, was dann?

@kripken Ich glaube nicht, dass eine doppelte Zusammenstellung eine gute Idee ist.

@kripken Ich denke, das würden wir ohne eine andere Lösung dieses Problems tun.

Ich möchte, dass WebAssembly beim gelegentlichen Surfen großartig ist: Sie erzählen mir von einem coolen Ding, senden mir die URL, ich klicke darauf und ich amüsiere mich für ein paar Minuten. Das macht das Web cool. Das bedeutet jedoch, dass viele Kompilierungen aus Code bestehen, der nicht zwischengespeichert ist, sodass die Kompilierzeit eine große Rolle bei der Akkulaufzeit eines Benutzers spielt. Also, doppeltes Kompilieren macht mich traurig.

@mtrofin

Modulautor muss 4 GB als min./max. Speicher benötigen

Das ist nicht wirklich praktikabel, da viele Geräte nicht über 4 GB physischen Speicher verfügen. Außerdem ist das schwer zu spezifizieren.

Eine Variante des Kompilierens (zumindest async, vielleicht auch sync), die eine Instanz erzeugt, die nur schnellen Speicher akzeptiert.

Ich glaube nicht, dass wir doppelte Kompilierungen wollen.

@pizlonator Bisher haben wir noch keine Designs in Betracht gezogen, die unterschiedliche Codegen-Modi erforderten: Wir haben einfach immer 4 GB-Regionen auf 64-Bit zugewiesen und beobachtet, dass dies für viele, viele Tausend Speicher unter Linux, OSX und Windows erfolgreich ist. Wir haben eine konservative Obergrenze, um eine triviale totale Erschöpfung des verfügbaren Adressraums zu verhindern, von der ich erwarte, dass sie ausreicht, um den Anwendungsfall "viele kleine Bibliotheken" zu unterstützen. Ich denke, die neue Einschränkung, die wir hier ansprechen, besteht darin, dass iOS einige Einschränkungen des virtuellen Adressraums hat, die die Anzahl der 4 GB-Zuweisungen reduzieren könnten.

Eine Beobachtung ist also, dass ein großer Teil der durch den 4-GB-Hack ermöglichten Bounds-Check-Eliminierung vermieden werden kann, indem nur eine kleine Guard-Region am Ende des Wasm-Speichers verwendet wird. Unsere ersten Experimente zeigen, dass grundlegende Analysen (die nichts mit Schleifen zu tun haben, sondern nur Überprüfungen von Lasten/Speichern mit demselben Basiszeiger eliminieren) bereits ungefähr die Hälfte der Grenzüberprüfungen eliminieren können. Und wahrscheinlich könnte das besser werden. Der 4-GB-Hack wäre also eine bescheidenere und weniger notwendige Beschleunigung.

Eine andere Idee, die ich früher hatte, wäre, Code pessimistisch mit Bounds-Checks zu kompilieren (mit Eliminierung basierend auf der Guard-Seite) und sie dann bei der Instanziierung mit einem Fast-Mode-Memory auszublenden. Zusammengenommen könnte der Overhead im Vergleich zu idealisiertem Fast-Mode-Code ziemlich gering sein.

@lukewagner

Bisher haben wir Designs nicht in Betracht gezogen, die unterschiedliche Codegen-Modi erforderten: Wir haben einfach immer 4 GB-Regionen auf 64-Bit zugewiesen und beobachtet, dass dies für viele, viele Tausend Speicher unter Linux, OSX und Windows erfolgreich ist. Wir haben eine konservative Gesamtzahl, um eine triviale totale Erschöpfung des verfügbaren Adressraums zu verhindern, von der ich erwarte, dass sie ausreicht, um den Anwendungsfall Viele-kleine-Bibliotheken zu unterstützen. Ich denke, die neue Einschränkung, die wir hier ansprechen, besteht darin, dass iOS einige Einschränkungen des virtuellen Adressraums hat, die die Anzahl der 4 GB-Zuweisungen reduzieren könnten.

Dies ist kein iOS-spezifisches Problem. Das Problem besteht darin, dass es ein Sicherheitsrisiko darstellt, wenn Sie viele solcher Zuweisungen zulassen, da jede solche Zuweisung die Wirksamkeit von ASLR verringert. Ich denke also, dass die VM die Möglichkeit haben sollte, ein sehr niedriges Limit für die Anzahl von 4 GB Speicherplatz festzulegen, die sie zuweist, aber das bedeutet, dass der Fallback-Pfad nicht zu teuer sein sollte (dh er sollte keine Neukompilierung erfordern).

Welche Grenze haben Sie für die Anzahl der 4 GB Speicher, die Sie zuweisen würden? Was tun Sie, wenn Sie dieses Limit erreichen - ganz aufgeben oder bei der Instanziierung neu kompilieren?

Eine Beobachtung ist also, dass ein großer Teil der durch den 4-GB-Hack ermöglichten Bounds-Check-Eliminierung vermieden werden kann, indem nur eine kleine Guard-Region am Ende des Wasm-Speichers verwendet wird. Unsere ersten Experimente zeigen, dass grundlegende Analysen (die nichts mit Schleifen zu tun haben, sondern nur Überprüfungen von Lasten/Speichern mit demselben Basiszeiger eliminieren) bereits ungefähr die Hälfte der Grenzüberprüfungen eliminieren können. Und wahrscheinlich könnte das besser werden. Der 4-GB-Hack wäre also eine bescheidenere und weniger notwendige Beschleunigung.

Ich stimme zu, dass die Analyse es uns ermöglicht, mehr Überprüfungen zu eliminieren, aber der 4-GB-Hack ist der richtige Weg, wenn Sie eine Spitzenleistung wünschen. Jeder will Spitzenleistung, und ich denke, es wäre großartig, es möglich zu machen, Spitzenleistung zu erreichen, ohne auch Sicherheitsprobleme, Ressourcenprobleme und unerwartete Neukompilierungen zu verursachen.

Eine andere Idee, die ich früher hatte, wäre, Code pessimistisch mit Bounds-Checks zu kompilieren (mit Eliminierung basierend auf der Guard-Seite) und sie dann bei der Instanziierung mit einem Fast-Mode-Memory auszublenden. Zusammengenommen könnte der Overhead im Vergleich zu idealisiertem Fast-Mode-Code ziemlich gering sein.

Code mit Grenzüberprüfungen ist am besten, ein Register für die Speichergröße und ein Register für die Speicherbasis zu fixieren.

Code, der den 4-GB-Hack verwendet, muss nur ein Register für die Speicherbasis anheften.

Das ist also keine großartige Lösung.

Was sind die Nachteile der Kombination von Kompilierung und Instanziierung in einer versprochenen Aktion, abgesehen von dem Ärger, die Spezifikation und die Implementierungen durcheinander bringen zu müssen?

Das Problem ist, dass es ein Sicherheitsrisiko darstellt, wenn Sie viele solcher Zuweisungen zulassen, da jede solche
Allokation verringert die Wirksamkeit von ASLR.

Ich bin kein Experte für ASLR aber, iiuc, auch wenn wir nicht eine konservative gebunden hatte (das heißt, wenn wir Ihnen erlaubt , bis zu halten Zuteilung von mmap schlug fehl , da der Kernel traf seine Nummer-of- Adressbereiche max) würde nur ein kleiner Bruchteil des gesamten adressierbaren 47-Bit-Raums verbraucht, so dass die Codeplatzierung über diesen 47-Bit-Raum weiterhin höchst zufällig wäre. Die Platzierung des IIUC- und ASLR-Codes ist auch nicht vollständig zufällig; gerade genug, um es schwer vorherzusagen, wo etwas sein wird.

Welche Grenze haben Sie für die Anzahl der 4 GB Speicher, die Sie zuweisen würden? Wie geht's
wenn Sie dieses Limit erreichen - ganz aufgeben oder bei der Instanziierung neu kompilieren?

Nun, da es aus asm.js-Tagen ist, nur 1000. Dann wirft die Speicherzuweisung einfach aus. Vielleicht müssen wir das ändern, aber selbst mit vielen supermodularisierten Apps (mit jeweils vielen separaten Wasm-Modulen), die denselben Prozess teilen, kann ich mir nicht vorstellen, dass wir zu viel mehr brauchen. Ich denke, Memory unterscheidet sich von einfachen alten ArrayBuffer s darin, dass Apps nicht natürlich Tausende erstellen wollen.

Was sind die Nachteile?
Kompilierung und Instanziierung zu einer versprochenen Aktion zu kombinieren?

Wie ich oben erwähnt habe, ist das Hinzufügen einer Promise<Instance> eval(bytecode, importObj) API in Ordnung, aber jetzt bringt es den Entwickler in eine schwierige Lage, da er sich jetzt zwischen einem Perf-Boost auf einigen Plattformen oder der Möglichkeit entscheiden muss, seinen kompilierten Code zwischenzuspeichern alle Plattformen. Es scheint, dass wir eine Lösung brauchen, die sich in das Caching integrieren lässt, und das habe ich oben mit der expliziten Cache API gedanklich gemacht.

Neue Idee: Was wäre, wenn wir eine asynchrone Version von new Instance hinzufügen, sagen wir WebAssembly.instantiate und wie bei WebAssembly.compile sagen wir, dass jeder die asynchrone Version verwenden soll? Dies ist etwas, was ich _jedenfalls_ in Betracht gezogen habe, da die Instanziierung einige ms dauern kann, wenn Patching verwendet wird. Dann sagen wir in der Spezifikation, dass die Engine entweder in compile oder in instantiate teure Arbeit leisten kann (oder nicht, wenn eine Engine träge Validierung/Kompilierung durchführt!).

Bleibt immer noch die Frage, was zu tun ist, wenn ein compile d Module in IDB gespeichert ist, aber das ist nur eine schwierige Frage, wenn es _jedenfalls_ mehrere Codegen-Modi gibt. Eine Idee ist, dass Module s, die in IDB gespeichert oder von IDB abgerufen werden, ein Handle zu ihrem IDB-Eintrag halten und diesem Eintrag neuen kompilierten Code hinzufügen. Auf diese Weise würde der IDB-Eintrag träge eine oder mehrere kompilierte Versionen seines Moduls ansammeln und in der Lage sein, die während der Instanziierung benötigte bereitzustellen.

Der IDB-Teil ist ein bisschen mehr Arbeit, aber das scheint in Bezug auf die Leistung ziemlich nahe am Ideal zu sein. WDYT?

Ich denke, das Hinzufügen von async instantiate macht Sinn, aber ich würde auch einen Memory Parameter zu compile hinzufügen. Wenn Sie instantiate einen anderen Speicher übergeben, können Sie sich neu kompilieren lassen, ansonsten haben Sie den Speicher beim Kompilieren bereits "gebunden".

Ich habe noch nicht genug über das Caching nachgedacht, um eine vollständige Meinung zu haben.

@lukewagner

Ich bin kein Experte für ASLR, aber iiuc, selbst wenn wir keine konservative Grenze haben (dh wenn wir Ihnen erlauben, die Zuweisung fortzusetzen, bis mmap fehlschlägt, weil der Kernel seine maximale Anzahl von Adressbereichen erreicht hat). , würde nur ein kleiner Bruchteil des gesamten adressierbaren 47-Bit-Raums verbraucht, so dass die Codeplatzierung über diesen 47-Bit-Raum weiterhin höchst zufällig wäre. Die Platzierung des IIUC- und ASLR-Codes ist auch nicht vollständig zufällig; gerade genug, um es schwer vorherzusagen, wo etwas sein wird.

ASLR betrifft sowohl Code als auch Daten. Der Punkt besteht darin, es für einen Angreifer teurer zu machen, sich in eine Datenstruktur einzuschleichen, ohne einen Zeiger darauf zu verfolgen. Wenn der Angreifer das Gedächtnis erschöpfen kann, hat er definitiv mehr Einfluss.

Nun, da es aus asm.js-Tagen ist, nur 1000. Dann wirft die Speicherzuweisung einfach aus. Vielleicht müssen wir das ändern, aber selbst mit vielen supermodularisierten Apps (mit jeweils vielen separaten Wasm-Modulen), die denselben Prozess teilen, kann ich mir nicht vorstellen, dass wir zu viel mehr brauchen. Ich denke, Speicher unterscheidet sich von einfachen alten ArrayBuffern darin, dass Apps nicht natürlich Tausende erstellen möchten.

1000 scheint eine sinnvolle Grenze zu sein. Ich werde mich bei den Sicherheitsleuten erkundigen.

Wie ich oben erwähnt habe, ein Versprechen hinzufügeneval(bytecode, importObj) API ist in Ordnung, aber jetzt bringt es den Entwickler in eine schwierige Lage, weil er jetzt zwischen einem Perf-Boost auf einigen Plattformen oder dem Zwischenspeichern seines kompilierten Codes auf allen Plattformen wählen muss. Es scheint, dass wir eine Lösung brauchen, die sich in das Caching integrieren lässt, und das habe ich oben mit der expliziten Cache-API gedanken.

Rechts. Ich sehe ein paar Möglichkeiten, wie eine solche API zum Laufen gebracht werden könnte. Eine kitschige, aber praktische API wäre, eval zu überladen:

  1. instancePromise = eval(bytecode, importObj)
  2. instancePromise = eval(module, importObj)

und dann hat Instanz einen Getter:

Modul = Instanz.Modul

Wo Modul ist Struktur klonbar.

Was denkst du über dies?

Neue Idee: Was wäre, wenn wir eine asynchrone Version einer neuen Instanz hinzufügen, sagen wir WebAssembly.instantiate, und wie bei WebAssembly.compile sagen wir, dass jeder die asynchrone Version verwenden soll? Dies ist etwas, was ich sowieso in Betracht gezogen habe, da die Instanziierung einige ms dauern kann, wenn Patching verwendet wird. Dann sagen wir in der Spezifikation, dass die Engine entweder beim Kompilieren oder Instanziieren teure Arbeit leisten kann (oder auch nicht, wenn eine Engine träge Validierung/Kompilierung durchführt!).

Bleibt immer noch die Frage, was zu tun ist, wenn ein kompiliertes Modul in IDB gespeichert wird, aber das ist nur eine schwierige Frage, wenn es sowieso mehrere Codegen-Modi gibt. Eine Idee ist, dass Module, die in IDB gespeichert oder von IDB abgerufen werden, ein Handle zu ihrem IDB-Eintrag halten und diesem Eintrag neuen kompilierten Code hinzufügen. Auf diese Weise würde der IDB-Eintrag träge eine oder mehrere kompilierte Versionen seines Moduls ansammeln und in der Lage sein, die während der Instanziierung benötigte bereitzustellen.

Der IDB-Teil ist ein bisschen mehr Arbeit, aber das scheint in Bezug auf die Leistung ziemlich nahe am Ideal zu sein. WDYT?

Faszinierend. Bezogen auf meine obige Idee:

Pro: Ihre ist eine leicht verständliche Abstraktion, die konzeptionell dem ähnelt, was wir jetzt sagen.
Nachteil: Ihr Vorschlag führt nicht zu so viel Synergie zwischen dem, was der Benutzer tut, und dem, was die Engine tut, wie es mein Vorschlag zulässt.

Es gibt drei Bereiche, in denen Ihr Vorschlag dem Benutzer nicht so viel Kontrolle gibt wie bei mir:

  1. Die teure Arbeit kann an einem von zwei Orten stattfinden, sodass der Benutzer damit rechnen muss, dass einer von beiden teuer ist. Wir werden wahrscheinlich Webinhalte haben, die sich schlecht verhalten, wenn einer von ihnen teuer ist, weil er auf Fälle abgestimmt wurde, in denen er zufällig billig war. Mein Vorschlag hat einen Punkt, an dem teure Dinge passieren, was zu mehr Einheitlichkeit zwischen den Implementierungen führt.
  2. Es gibt keinen eindeutig garantierten Pfad für alle Versionen des kompilierten Codes, die zwischengespeichert werden sollen. Auf der anderen Seite bedeutet meine Verwendung des Threads des Moduls durch die API, dass die VM das Modul jedes Mal mit mehr Material aufbauen kann, während der Benutzer den Cache weiterhin verwalten kann. Wenn wir also beim ersten Mal 4 GB speichern, werden wir dies zwischenspeichern, aber wenn wir beim zweiten Mal keine 4 GB schaffen, können wir möglicherweise beide zwischenspeichern (wenn der Benutzer instance.module nach jeder Kompilierung zwischenspeichert).
  3. Ungewöhnliche Eckfälle im Browser oder andere Probleme können manchmal zu einer doppelten Kompilierung in Ihrem Schema führen, da wir eine Sache im Kompilierungsschritt kompilieren würden, dann aber feststellen, dass wir im Instanziierungsschritt eine andere Sache benötigen. Meine Version erfordert nie eine doppelte Kompilierung.

Also mir gefällt meins besser. Trotzdem denke ich, dass Ihr Vorschlag eine Weiterentwicklung ist, also klingt er definitiv gut für mich.

Dieses Problem hängt davon ab, wie oft die Fragmentierung die Zuweisung schnell macht
Speicher (übrigens 4 GB + maximal unterstützter Offset oder 8 GB) schlägt fehl. Wenn die
wahrscheinlich viel weniger als 1% ist, dann ist es möglicherweise nicht völlig unvernünftig,
muss das eine OOM-Situation sein.

In einem Szenario, in dem der Benutzer im Internet surft und viele
kleine WASM-Module in schneller Folge, vermutlich sind nicht alle live dabei
wenn. In diesem Fall würde ein kleiner Cache mit reservierten 4-GB-Chunks die
Ausgabe.

Eine andere mögliche Strategie besteht darin, eine Version des Codes mit . zu generieren
Grenzüberprüfungen, und wenn schneller Speicher verfügbar ist, einfach die Grenzen überschreiben
checkt mit nops. Das ist hässlich, aber das ist verdammt viel schneller als ein
neu kompilieren und weniger Platz als zwei Kompilierungen.

Am Do, 27.10.2016 um 21:03 Uhr, pizlonator [email protected]
schrieb:

@mtrofin https://github.com/mtrofin

Modulautor muss 4 GB als min./max. Speicher benötigen

Das ist nicht wirklich praktikabel, da viele Geräte keine 4 GB physisch haben
Erinnerung. Außerdem ist das schwer zu spezifizieren.

Eine Variante von compilieren (zumindest async, vielleicht auch sync), die ein
Instanz akzeptiert nur schnellen Speicher.

Ich glaube nicht, dass wir doppelte Kompilierungen wollen.


Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/WebAssembly/design/issues/838#issuecomment -256738329,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/ALnq1F6CYUaq0unla0H6RYivUC8jfxIAks5q4PWIgaJpZM4Kh1gM
.

Es ist nicht nur ASLR: es ist auch Pagetable / Allocator / etc. Verschmutzung. Wir alle müssen mit unseren Sicherheitsleuten _sowie_ mit unseren Kernel-/Systemleuten sprechen. Oder wir können den Entwicklern die Grenzen, die jede Engine dem "schnellen" Memory auferlegt, im Voraus erklären und es in der API idiomatisch machen, damit es schwer ist, es falsch zu verwenden.

Es gibt all diese Krücken, die wir verwenden können, wie Nop oder Double Compilation, aber warum überhaupt Krücken?

@jfbastien Ich glaube nicht, dass PROT_NONE Speicher Seitentabelleneinträge kostet; Ich denke, es gibt eine separate Datenstruktur, die Zuordnungen enthält, aus denen die Seitentabelle träge gefüllt wird.

@pizlonator Ich mag diese Idee und ich kann sehen, dass wir jeden dazu ermutigen, dies standardmäßig in Tutorials, Toolchains usw. zu verwenden. Es ist auch prägnanter und einfacher zu lehren, wenn Sie Module einfach ignorieren können. Dies könnte auch die Bedenken von @s3ththompson adressieren , die Verwendung der Synchronisierungs-APIs zu verhindern, indem die schönste API zur asynchronen gemacht wird.

Ich denke jedoch, wir sollten WebAssembly.compile und den Module Konstruktor nicht wegnehmen: Ich stelle mir Szenarien vor, in denen Sie einen "Codeserver" haben (der ursprungsübergreifendes Code-Caching über IDB + postMessage bereitstellt). new Module ), müssten wir new Instance behalten

Wenn Sie sich also darauf einig sind, läuft dies auf einen rein additiven Vorschlag der beiden WebAssembly.eval Ihnen erwähnten

Eine Optimierung jedoch: Ich denke, wir sollten keinen module Getter haben, da dies erfordern würde, dass die Instance einige interne Daten (nämlich Bytecode) für die Lebensdauer der Instance ; im Moment können Module normalerweise direkt nach der Instanziierung mit GC bearbeitet werden. Dies würde entweder eine Dateneigenschaft vorschlagen (die der Benutzer entfernen kann, obwohl er dies wahrscheinlich vergessen wird) oder vielleicht eine dritte Version von eval , die ein {instance, module} Paar zurückgibt...

Eine asynchrone One-Step-API als empfohlener Fall für die typische monolithische App ist als empfohlenes Muster sinnvoll.

Mit @lukewagner vereinbart, dass sowohl der Fall der All-Sync (Inline-Kompilierung), der durch das neue Modul als auch die neue Instanz abgedeckt wird, nützlich ist.
Auch der Hintergrundkompilierungsserver (async) mit Synchronisierungsinstanz scheint nützlich zu sein.

Das Hinzufügen der beiden vorgeschlagenen Evaluierungsvarianten scheint ein guter Weg, dies einzuführen.

Allerdings mag ich den Namen nicht, weil er in den Köpfen der (Sicherheits-)Leute mit js eval verschmolzen wird (dem er in gewisser Weise ähnelt, aber nicht in Bezug auf die Bereichserfassung).
Wie wäre es mit WebAssembly.instantiate?

Hah, guter Punkt, eval hat ein bisschen eine Wiederholung . +1 bis WebAssembly.instantiate .

Was wäre die Richtlinie für den Entwickler, wenn das asynchrone Instanziat verwendet werden soll?

@mtrofin Um WebAssembly.instantiate standardmäßig zu verwenden, es sei denn, sie hatten ein spezielles Code-Sharing-/Lade-Schema, das das Kompilieren von Module s unabhängig von einer bestimmten Verwendung erforderte.

@lukewagner Das scheint vernünftig.

Hah, guter Punkt, eval hat ein bisschen eine Wiederholung. +1 für WebAssembly.instantiate.

Einverstanden.

Wenn man sich also darauf einig ist, läuft dies auf einen rein additiven Vorschlag der beiden von Ihnen erwähnten WebAssembly.eval-Überladungen hinaus. Jawohl?

So hört es sich an.

Ich denke, wir sollten keinen Modul-Getter haben, da dies erfordern würde, dass die Instanz einige interne Daten (nämlich Bytecode) für die Lebensdauer der Instanz bereithält; derzeit Module können normalerweise sofort nach der Instanziierung mit GC bearbeitet werden. Dies würde entweder eine data-Eigenschaft vorschlagen (die der Benutzer entfernen kann, obwohl er dies wahrscheinlich vergessen wird) oder vielleicht eine dritte Version von eval, die ein {instance, module}-Paar zurückgibt...

Sicher fühlt sich eine Dateneigenschaft besser an. Oder lassen Sie WebAssembly.instantiate immer eine Instanz, ein Modulpaar zurückgeben.

Ist das richtig: Angenommen, Sie haben WebAssembly.instantiate mit dem Ziel, eine Fast-Memory-Modulvariante zu erhalten. Sie erhalten jetzt das Modul und strukturieren es. Nun muss dieses Modul mit Memory -es instanziiert werden, das Fastmemory unterstützt.

@pizlonator Ja, ich kann es auf verschiedene Weise in meinem Kopf vernichten. Ich denke, ich mag es, das Paar etwas besser zurückzugeben, da dies wahrscheinlich dazu führt, dass weniger Leute versehentlich ein unbenutztes Module .

@mtrofin Eine Neukompilierung kann immer noch erforderlich sein, wenn Sie Module aus einem instantiate Aufruf und instantiate bei neuen Importen entfernen; Ich denke, der Sinn dieser API-Ergänzung ist, dass dies nicht der übliche Fall ist und nur dann passiert, wenn es grundlegend notwendig ist (dh Sie haben 1 Modul, das auf zwei Arten von Speichern zugreift).

Dieser Thread wird lang, sieht so aus, als ob er konvergiert, aber um 100% sicher zu sein, müssen wir den Code schreiben, von dem wir erwarten, dass er von verschiedenen Benutzern geschrieben wird:

  1. Asynchrone Instanziierung eines einzelnen Moduls.
  2. Asynchrone Instanziierung eines Moduls mit gemeinsamer Speichernutzung mit anderen Modulen.
  3. Synchrone Instanziierung eines einzelnen Moduls (ich glaube nicht, dass synchrone Multi-Module sinnvoll sind?).
  4. Caching für all dies (sowohl Einlegen in den Cache als auch Abrufen und Instanziieren mit Speicher).
  5. Aktualisierung eines einzelnen .wasm Moduls und zwischengespeicherte Lasten der anderen Module.

Noch etwas? Es hört sich so an, als hätte @lukewagner Ideen zu Importen, die ich nicht ganz verstehe.

Das bedeutet, dass nachfolgende Verwendungen dieses Moduls asynchron instanziieren müssen oder riskieren, den UI-Thread mit einer überraschend langen synchronen Instanziierung zu blockieren.

@jfbastien Ich möchte für jedes Snippet, das wir von Entwicklern erwarten, verstehen, was sie dazu motivieren würde, diesen bestimmten Weg zu gehen, und welche Informationen der Entwickler zur Verfügung haben muss, um eine Entscheidung zu treffen.

@mtrofin Richtig, bei einem Module m Sie WebAssembly.instantiate(m) aufrufen, was asynchron ist. Sie _könnten_ new Instance(m) anrufen und es könnte teuer werden, aber das ist nicht anders als new Module(m) .

@jfbastien Angenommen, wenn Sie "asynchrone Instanziierung" sagen, meinen Sie "asynchrone Kompilierung und Instanziierung", hier ist die Kurzversion:

  1. WebAssembly.instantiate(bytecode, imports)
  2. WebAssembly.instantiate(bytecode, imports) , wobei imports den gemeinsamen Speicher einschließt
  3. new Instance(new Module(bytecode), imports)
  4. In allen Fällen können Sie ein Module , dann put das in einem IDBObjectStore . Später get a Module m zurück und rufen WebAssembly.instantiate(m, imports) .
  5. Nichts wirklich Besonderes hier: Sie WebAssembly.instantiate ein Modul aus Bytecode und instantiate den Rest aus den Module s aus IDB.

Sollten wir empfehlen, die Synchronisierungsinstanz zu verwenden, wenn Sie der Meinung sind, dass Sie die Synchronisierungskompilierung verwenden können, und die asynchrone Instanziierung, wenn Sie der Meinung sind, dass Sie die asynchrone Kompilierung verwenden sollten?

Abgesehen davon befürchte ich, dass der Entwickler jetzt vor einem komplexeren System stehen würde: mehr Auswahlmöglichkeiten, die Optimierungen hervorbringen, die wir vornehmen möchten, und ich bin nicht sicher, ob der Entwickler die richtigen Informationen zur Verfügung hat, um die Kompromisse einzugehen. Gibt es aus Sicht des Entwicklers eine kleinere Gruppe von Bedenken, die ihnen wichtig sind und die sie gerne äußern würden? Wir sprachen an einer Stelle darüber, dass Entwickler eine "Optimierung auf Kosten präziser Fehlerpunkte" haben (dies war das Hochziehen von Grenzüberprüfungen). Wäre eine Alternative ein "Optimieren"-Flag?

@mtrofin 99% von dem, was Entwickler schreiben (oder von der Toolchain für sie generiert haben) wären WebAssembly.instantiate . Sie würden Synchronisierungs-APIs nur für spezielle "Ich schreibe ein JIT in wasm" und WebAssembly.compile wenn Sie ein Code-Sharing-System schreiben, daher würde ich denken, dass die Tutorials "Erste Schritte" ausschließlich WebAssembly.instantiate .

@lukewagner Mir ist
Auf diese Weise können Sie zur Kompilierzeit Hinweise auf den Speicher geben.
Wenn Sie später mit verschiedenen Importen, insbesondere synchron, erneut instanziieren, kann es zu einem Schluckauf kommen.

Also Zusammenfassung der Änderungen (nur damit ich klar bin):

  • Add WebAssembly.instantiate(bytes, imports) gibt das Versprechen von {instance:, module:} zurück
  • Add WebAssembly.instantiate(module, imports) gibt das Versprechen von {instance:, module:} zurück
  • Wechseln Sie zu einem neuen Modul (Bytes , Importe ) gibt das Modul zurück
  • Ändern Sie zu WebAssembly.compile(bytes , imports ) gibt das Versprechen der Instanz zurück

Geben Sie irgendwo die Erwartung an, dass die Instanziierung schnell sein wird, wenn die Importe von der Kompilierung mit der Instanziierung übereinstimmen.

WDYT?

Oh oops, ich wollte die Importe als Argument auf Instance . Ich bin nicht überzeugt, dass es für Module oder compile notwendig ist. [Bearbeiten: denn wenn du sie hättest, würdest du einfach instantiate anrufen]

Das würde also bedeuten, dass Sie für den End-to-End-Async-Fall wissen können, dass Sie an einen 4 GB-Hack-Speicher binden, aber nicht für einen JIT-Filterkernel oder ein im Hintergrund kompiliertes Element (es sei denn, Sie erstellen auch ein Throw- entfernte Instanz)?

+1 zur Konzentration der Anleitung auf das asynchrone Paar von Kompilieren und Instanziieren - macht die Nachricht einfach und verbirgt die Komplexität des Entscheidungsproblems vor dem Entwickler.

Ja, ich denke, wir sind uns alle einig, dass wir die Leute darauf hinweisen würden:
Erstes Mal:
WebAssembly.instantiate(bytes, imports) -> Versprechen von {module, instance} (Cache-Modul an indexeddb)
Zweites Mal:
WebAssembly.instantiate(module, imports) -> Versprechen von {module, instance}

Gibt es Einwände dagegen, dass dies das Hauptmuster ist?

Ich bin hin- und hergerissen über Importe für Kompilieren / neues Modul. Das scheint ein nützlicher Hinweis zu sein.
Ich wäre jedoch offen dafür, es als Möglichkeit zu erwähnen und das Hinzufügen dieses Arguments (es könnte optional sein) zu Post-MVP zu verschieben.

Die Gedanken?

@mtrofin (Nun, technisch gesehen nur instantiate .)

@lukewagner (ich glaube, das meinte @mtrofin )

@lukewagner , @flagxor OK, aber wir behalten die asynchrone Kompilierungs-API, oder?

Wie wäre es mit diesem Szenario: Sie erhalten eine Anwendung wie PhotoShop mit Tonnen von Plugins. Jedes Plugin ist ein wasm-Modul. Sie starten die Haupt-App und schaffen es, die magische Speichergröße zuzuweisen, die Fastmemory auslöst (scheint für dieses Szenario vernünftig - eine App, speicherhungrig).

Sie möchten eine Reihe von Plugins parallel kompilieren, damit Sie einige Arbeiter entlassen, um dies zu tun. Sie können diesen Kompilierungen nicht den tatsächlich verwendeten Speicher übergeben (richtig?). Abhängig von den Voreinstellungen erhalten Sie also eine Slowmemory-Kompilierung für die Plugins, die dann von einer kostspieligen Reihe asynchroner Neukompilierungen für den Fastmemory gefolgt wird, wenn die Plugins mit der App verbunden werden.

Wenn wir dieses Szenario kaufen, scheint es sinnvoll zu sein, einen Speicherdeskriptor (um klar zu sein, ohne tatsächlichen Sicherungsspeicher) an die Kompilierungs-API zu übergeben.

Ja, es sollte möglich (sogar erwünscht) sein, Memory an die Kompilierung zu übergeben.

@mtrofin Richtig, compile für fortgeschrittene Anwendungen. Ich nehme an, dass dieses Plugin-Beispiel ein gültiger Fall ist, in dem Sie _kompilieren_, _und_ ein Memory , aber (noch) nicht instanziieren möchten.

@pizlonator Übrigens , ich wollte schon früher fragen, vorausgesetzt, der Hack "Werfen, wenn mehr als 1000 4-GB-Karten pro Prozess" ist ausreichend, um die ASLR / Sicherheitsbedenken auszuräumen, besteht _noch_ aufgrund der Plattform ein Bedarf für Slow-Mode / Fast-Mode? Kontingentbeschränkungen für virtuelle Adressen? Denn wenn nicht, wäre es sicherlich schön, wenn dies nicht einmal für fortgeschrittene Benutzer ein Leistungsaspekt wäre. (Die instantiate APIs scheinen natürlich aus den anderen Gründen, die wir erwähnt haben, nützlich zu sein.)

Es gibt auch Anwendungen, die von einer Speichergröße profitieren könnten, die eine Zweierpotenz plus einen Überlaufbereich ist, in der die Anwendung bereits Zeiger maskiert, um das Tagging zu entfernen, sodass die hohen Bits maskiert werden können, um die Überprüfung von Grenzen zu vermeiden. Diese Anwendungen müssen über die Speicherzuweisungsgröße nachdenken, die sie vor der Kompilierung erhalten können, indem sie entweder globale Konstanten ändern, die zum Maskieren verwendet werden, oder geeignete Konstanten beim Dekomprimieren in wasm einbacken.

Es gibt auch die Puffer-bei-Null-Optimierung, die einige Laufzeiten möglicherweise nutzen möchten, und es wird nur einen solchen Puffer pro Prozess geben.

Es würde die Plattform auch benutzerfreundlicher machen, wenn sie vor dem Kompilieren der Anwendung über den erforderlichen Speicher und den verfügbaren Speicher nachdenken könnte. Zum Beispiel, um dem Browser oder der App zu ermöglichen, den Benutzer darüber zu informieren, dass er einige Registerkarten schließen muss, um die Anwendung auszuführen, oder um sie ohne eingeschränkte Funktionalität auszuführen.

Ein Webbrowser möchte möglicherweise einen dedizierten App-Modus haben, der eine Option für Benutzer ist, in denen sie möglicherweise auf einem begrenzten Gerät ausgeführt werden und den gesamten Speicher und die Leistung benötigen, die sie erhalten können, nur um die eine Anwendung gut auszuführen. Dazu muss es frühzeitig über die Anforderungen nachdenken können.

Der Speicher sollte vor der Kompilierung nicht allokiert werden müssen, sondern eine begründete Reservierung vorgenommen werden. Auf einem begrenzten Gerät kann allein die Kompilierung viel Speicher beanspruchen, sodass selbst eine große VM-Zuweisung ein Showstopper sein kann.

Das sind keine neuen Themen, die seit Jahren diskutiert werden. Die Verwaltung der Speicherressourcen ist erforderlich und muss mit der Codegenerierung koordiniert werden.

@lukewagner Ich denke schon, denn wenn wir uns auf 1000 Module beschränken, dann würde ich mir Sorgen machen, dass die Toleranzen nicht groß genug sind.

  • Ich würde mir Sorgen machen, dass ein Angriff auftaucht, bei dem diese Decke gesenkt werden muss.
  • Ich würde mir Sorgen machen, dass Optimierungen an anderer Stelle im Stack die Menge des uns zur Verfügung stehenden virtuellen Adressraums reduzieren, was uns dann erfordern würde, neu zu bewerten, ob die Obergrenze niedrig genug ist.
  • Ich würde mir Sorgen machen, Programmierstile zu verhindern, die absichtlich Tausende von Modulen erstellen. Ich weiß zum Beispiel, dass die meisten JavaScriptCore-Framework-Clients eine VM erstellen, ein wenig Arbeit erledigen und sie dann zerstören. Wenn WebAssembly von JS genauso verwendet wird wie JSC von Objective-C, dann müsste der GC wissen, damit es auf 64-Bit-Systemen funktioniert, wenn Sie 1000 Speicher zuweisen - auch wenn jeder klein ist - dann müssen Sie dies tun GC für den Fall, dass die nächste Zuweisung gelingen sollte, da diese 1000 Speicher jetzt nicht erreichbar sind. Die Möglichkeit, Nicht-4-GB-Hack-Speicher zuzuweisen, nachdem beispielsweise bereits 10 4 GB-Hack-Speicher live sind, würde bedeuten, dass der GC seine Heuristik nicht sehr ändern müsste. Es müsste kein GC durchgeführt werden, wenn Sie das 1001. Modul in Ihrer instantiate->run->die-Schleife zuweisen. Dies wäre ein Vorteil für Muster, die einen winzigen Speicher verwenden. Alles unter 1 MB, und es macht Sinn, 1000 davon zu haben. Ich kann mir vorstellen, dass Leute in 64 KB nützliche Dinge tun.
  • Ich würde mir Sorgen machen, dass dies für andere JavaScript-Kontexte weniger nützlich ist. Ich möchte die Tür für JSC-C-API- und Objective-C-API-Clients offen lassen, damit sie über ihren JS-Code auf die WebAssembly-API zugreifen können. Diese Kunden würden wahrscheinlich eine kleine Begrenzung der Anzahl der 4 GB-Speicher bevorzugen, die wir zuweisen. Es ist verlockend, dieses Kontingent in einem solchen Kontext sogar konfigurierbar zu machen.

Mir gefällt, dass die verbesserte API die Notwendigkeit einer künstlichen Obergrenze für die Anzahl der Speicher, der Neukompilierung oder anderer unerwünschter Dinge beseitigt. Ich mag keine künstlichen Decken, es sei denn, die Toleranzen sind sehr groß, und ich denke, das ist hier nicht der Fall.

@pizlonator Fair genug, und es ist eine sauberere / einfachere API, daher denke ich, dass es in Ordnung ist, hinzuzufügen.

Warum ich mich nicht mit den von Ihnen erwähnten Punkten befasse (derzeit):

  • Es kann durchaus sein, dass die Grenze erhöht werden muss; das ist leicht.
  • Bei jeder vernünftigen Grenze wird nur ein kleiner Bruchteil des gesamten 64-Bit-Adressraums verwendet, daher ist mir nicht bewusst, was dieser Angriffsvektor hier ist; Bestimmter Inhalt hat viele Möglichkeiten, sich selbst zu OOM
  • Wir heben die GC-Heuristiken entsprechend der Reservierungsgröße an, und somit führt das Durchlaufen von Memory s nur zu aggressiveren GC. Mehr GC ist nicht großartig, aber ich bin mir nicht sicher, ob dies ein übliches Muster sein wird.

Aber wer weiß, was wir in Zukunft sehen werden, also denke ich, dass es nützlich ist, die Flexibilität jetzt eingebaut zu haben.

Ich halte es für eine schlechte Idee, zu viele Implementierungsdetails in der js-API (insbesondere architektur- / plattformspezifische Details) aufzudecken. Ich denke, es sollte für Implementierungen ausreichen, ihre eigenen internen Grenzen für schnellen Speicher zu haben.

Eine asynchrone Instanziierungsfunktion zu haben scheint vernünftig, aber ich denke, wir sollten etwas anderes verwenden, wenn wir es verwenden möchten, um Compilerhinweise zu geben. Zum Beispiel könnten wir Module so erweitern, dass sie einen Flags-Abschnitt haben, der eine Optimierung für Singleton, Optimierung für viele Instanzen, Optimierung für Ladezeit, Optimierung für vorhersehbare Leistung usw. anfordert. Was (wenn überhaupt) Engines tun, hängt natürlich vollständig von der Implementierung ab. aber es gibt Entwicklern einen Drehknopf, und der Wettbewerb wird die Browser-Anbieter ehrlich halten.

Am Freitag, 28. Oktober 2016 um 02:15 Uhr, JF Bastien [email protected]
schrieb:

Ja, es sollte möglich (sogar ermutigt) sein, die Erinnerung an die weiterzugeben
Zusammenstellung.

Ich denke, davon sollte eher zugunsten von nicht abgeraten werden
Importieren/Exportieren von Erinnerungen überhaupt. Immerhin, wenn ein Modul nicht importiert
oder Exportspeicher, Speicher kann zum Zeitpunkt der Kompilierung reserviert werden. ich weiß wir
in der Lage sein, effizient mit dem ausgeklügelten Modul-Fu umzugehen, das manche
Anwendungen tun wollen, aber ich erwarte, dass monolithische WASM-Apps dies tun werden
häufiger sein, als wir erwarten. Vielleicht bin ich hier in der Minderheit, aber
Ich würde eher weniger Module mit weniger dynamischer Bindung sehen.

Sie erhalten dies, weil Sie einen Kommentar abgegeben haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/WebAssembly/design/issues/838#issuecomment -256805006,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/ALnq1KXrUWaegRZwEhznmT1YcyI33IN9ks5q4T6TgaJpZM4Kh1gM
.

Ich stimme Ihnen voll und ganz zu, ich denke, dass monolithische Apps der primäre Anwendungsfall sein werden, zumindest für die unmittelbaren Jahre nach MVP. Sich Sorgen darüber zu machen, was passiert, wenn Sie Zehntausende von Modulen geladen haben, bedeutet viel über ein Wasm-Ökosystem, das noch nicht existiert.

Was bleibt also zu tun, um dieses Problem zu lösen? Eine Sache wäre, http://webassembly.org/getting-started/js-api zu aktualisieren, was ich tun kann. Eine andere wäre, dass binaryen dies standardmäßig ausgibt (klingt gut @kripken?). @titzer implementiert Canary WebAssembly.instantiate ?

Noch etwas?

@lukewagner :

@kripken Richtig, Wechsel zu WebAssembly.instantiate . Wir entfernen den alten Weg nicht, aber der neue Weg ist effizienter und soll standardmäßig verwendet werden.

Wir könnten die Promise-basierte API beim Generieren von HTML verwenden, aber viele Benutzer generieren JS und das automatische Hinzufügen von asynchronen Schritten dort ist nicht trivial. Wir können dies jedoch für Menschen dokumentieren. Aber ich denke, wir sollten das alles erst tun, wenn dies in allen Browsern landet.

@kripken Ich bin mir nicht sicher, ob ich das verstehe: Der aktuelle Ansatz besteht darin, überhaupt keine asynchrone API zu verwenden?

Ja . Sehen Sie sich dieses Problem an, um asynchrone Dinge hinzuzufügen.

@kripken Node hätte Arbeitsversprechen, nehme ich an. Sogar Shells haben eine Möglichkeit, die Promise-Queue explizit zu leeren (und damit alle Auflösungen synchron auszuführen); in SM ist es drainJobQueue() und in V8 höre ich ein %RunMicroTasks() . Es scheint, als könnten Sie einfach WebAssembly.instantiate testen und es standardmäßig verwenden, wenn es vorhanden ist.

Sicher, aber zuerst könnte die Promise-Unterstützung in der neuesten node.js enthalten sein, aber nicht in den häufig verwendeten Versionen (z. B. Standardversion in Linux-Distributionen). Und zweitens ist das größere Problem, dass wir bei der Ausgabe von HTML die Kontrolle darüber haben, wie die Seite geladen wird (emcc gibt den Ladecode für den Benutzer aus), während bei der Ausgabe von JS davon ausgegangen wird, dass JS nur linear ausgeführt wird und die Benutzer davon abhängen darauf, zB können sie direkt nach JS ein weiteres script-Tag haben. In diesem Fall schreibt der Benutzer seinen eigenen Ladecode.

Als Ergebnis beider können wir, wie bereits erwähnt, Promise-APIs beim Ausgeben von HTML verwenden (dann befinden Sie sich definitiv nicht in der Shell und haben die Kontrolle über das Laden), aber nicht beim Ausgeben von JS. Dort können wir es nur dokumentieren.

Verfügt Node über Versionen, die WebAssembly unterstützen, aber nicht Promise? Interessieren sich die Node-Mitarbeiter für diese Versionen?

Ich verstehe die geradlinige JS-Sache nicht. Wenn Sie immer ein Versprechen zurückgeben, funktioniert das nicht einfach (Benutzer des Codes müssen das Versprechen konsumieren)?

Ich kenne die Antwort auf die erste Frage nicht, aber ein Polyfill könnte wasm auf einer Knotenversion ausführen lassen, die keine Versprechen hat. Obwohl es möglich sein könnte, auch Versprechen zu polyfillen, da node seit einiger Zeit eine Version von setTimeout hat, denke ich, bin mir aber auch nicht sicher.

Zum Thema Straightline: emcc gibt JS aus, das die Laufzeit einrichtet und eine Verbindung zum kompilierten Code herstellt. Einige JS in einem Skript-Tag direkt danach könnten den kompilierten Code aufrufen, zB mit ccall . Mit anderen Worten, die JS-Ausgabe von emcc ist kein Versprechen, daher bin ich mir nicht sicher, was Sie mit "ein Versprechen zurückgeben" meinen - wer würde es zurückgeben und wer würde es erhalten? Aber wie bereits erwähnt, ist der empfohlene Pfad, dass emcc HTML ausgibt. In diesem Fall können wir asynchronen Ladecode erstellen. Es ist nur so, dass einige Benutzer es vorziehen, das Laden direkter zu steuern. Wir müssen sie ermutigen, das asynchrone Wasm-Zeug zu verwenden, wenn es besser ist.

IMO Node mit WebAssembly aber ohne Promises ist keine Designbeschränkung, über die wir uns Sorgen machen sollten. Ein Polyfill für WebAssembly ist in diesem Zusammenhang ziemlich albern.

Sie beschreiben, was der Code heute macht. Ich verstehe es nicht ganz, aber ich möchte ein Backup machen: Ich möchte, dass der Code auf Webseiten die Promise-API verwendet. Emscripten ist ein Hersteller eines solchen Codes. Ich verstehe nicht, was es daran hindert, Code auszugeben, der Versprechen verwendet. Ich bin völlig in Ordnung, wenn Sie sagen "das ist eine bedeutende Arbeit, weil es heute nicht so funktioniert, aber wir werden es schaffen". Aber aus unserer Diskussion bin ich mir nicht sicher, ob Sie das sagen.

Geht es bei dem von Ihnen angesprochenen

Warum ist ein Polyfill am Knoten albern? Scheint in diesem Zusammenhang immer noch nützlich zu sein, wenn auch weniger als in anderen Fällen :)

Nochmal: Emscripten das Versprechen API verwenden , wenn es HTML emittiert. Und das ist der empfohlene Weg. Die Antwort auf Ihre Frage lautet also "ja". Es ist keine bedeutende Arbeit. Es ist in dieser Ausgabe skizziert, die sich ja auf das Caching konzentriert, aber ich habe Anmerkungen aus der (alten) Offline-Diskussion hinzugefügt, dass wir dabei auch eine Reihe anderer asynchroner Optimierungen vornehmen sollten, da dies möglich ist und dies trivial ist.

Berücksichtigt das Ihre Bedenken?

Ich sage nur, dass, wenn Emscripten JS ausgibt - der weniger empfohlene Pfad - die Garantien für diese Ausgabe nicht mit dem Code übereinstimmen, der intern asynchrone Magie ausführt. Wir würden den Code der Leute brechen, was wir nicht wollen. Wie ich schon sagte, kann jemand JS schreiben, das synchron direkt nach dem JS läuft, das davon ausgeht, dass es bereit ist. Daher können wir in diesem Fall keine Versprechungen verwenden. Stell dir das vor:

````

````

doSomething benötigt Module.my_func zu existieren. Wenn output.js gerade ein Versprechen zurückgegeben hat, existiert es noch nicht. Das wäre also eine bahnbrechende Veränderung.

Macht das jetzt Sinn?

Warum ist ein Polyfill am Knoten albern? Scheint in diesem Zusammenhang immer noch nützlich zu sein, wenn auch weniger als in anderen Fällen :)

Ein Polyfill zu Wasm ist nicht albern. Es ist albern, Node-Installationen zu bedienen, die kein Wasm haben, es polyfillen und kein Versprechen haben, aber das nicht polyfill. Es ist halbherzig. Sie sollen die andere Hälfte des Arsches bekommen 😁

Nochmals: Emscripten verwendet die Promise-API, wenn es HTML ausgibt. Und das ist der empfohlene Weg. Die Antwort auf Ihre Frage lautet also "ja". Es ist keine bedeutende Arbeit. Es ist in dieser Ausgabe skizziert, die sich ja auf das Caching konzentriert, aber ich habe Anmerkungen aus der (alten) Offline-Diskussion hinzugefügt, dass wir dabei auch eine Reihe anderer asynchroner Optimierungen vornehmen sollten, da dies möglich ist und dies trivial ist.

Berücksichtigt das Ihre Bedenken?

Ok das ist gut! Solange die meisten Webseiten die Versprechen-API verwenden, bin ich glücklich.

[schnipsen]

Macht das jetzt Sinn?

Jawohl. Danke fürs Erklären.

Aus meiner Sicht ist Emscripten jedoch nicht mehr nur im Geschäft mit der Emission von JS! Ihr Beispiel macht Sinn für Ye Olden Codes, aber neue Sachen sollten IMO Versprechen annehmen.

Übrigens, ich habe mir die Umstellung von webassembly.org/demo angesehen, um instantiate und es ist ein bisschen schwierig, da das aktuelle synchrone new Instance in einem Kontext auftritt, der ein synchrones Ergebnis möchte. Sobald wir also Binaryen so aktualisiert haben, dass es standardmäßig instantiate ausgibt, wäre es schön, die AngryBots-Demo von Grund auf neu zu erstellen.

Ja, aber beachten Sie, dass ein Neuaufbau von Grund auf nicht ausreicht - ich glaube, Unity verwendet seinen eigenen Lade- und HTML-Code. Also müssen wir dieses Problem wie oben erwähnt dokumentieren und kommunizieren, damit sie die notwendigen Dinge tun können (oder wir sie vielleicht dazu bringen können, emcc den HTML-Code ausgeben zu lassen, aber ich weiß nicht, ob das für sie machbar ist).

Ja, aber beachten Sie, dass ein Neuaufbau von Grund auf nicht ausreicht - ich glaube, Unity verwendet seinen eigenen Lade- und HTML-Code. Also müssen wir dieses Problem wie oben erwähnt dokumentieren und kommunizieren, damit sie die notwendigen Dinge tun können (oder wir sie vielleicht dazu bringen können, emcc den HTML-Code ausgeben zu lassen, aber ich weiß nicht, ob das für sie machbar ist).

In Anbetracht der möglichen Nachteile von nicht die Verwendung von WebAssembly.instantiate API, ich denke , es lohnt sich zu fragen , sie verwenden es zu berücksichtigen.

Sind diese Nachteile dokumentiert? Eine klare Anleitung dazu entweder auf der Hauptseite von wasm oder auf der

Ich habe diese lange Ausgabe gerade selbst überflogen, und mir sind die Nachteile noch nicht klar, also möchte ich das auch lesen :)

@kripken Die Herausforderung beim aktuellen Code besteht darin, dass binaryen/emscripten nur die notwendigen Importe (benötigt als Argumente für instantiate ) bereitstellen, während gleichzeitig die Exporte synchron benötigt werden. Wenn die Importe "im Voraus" verfügbar gemacht werden können, dann ist es ziemlich trivial, ein WebAssembly.instantiate an das Ende des asynchronen XHR zu heften (wie ich es in der aktuellen Demo mit dem asynchronen compile ). Ich glaube also nicht, dass dies von Unitys Seite viel Arbeit erfordern wird. Außerdem ist unser derzeitiger AngryBots-Wasm-Build nach gesehen habe, suboptimal und muss sowieso aufgefrischt werden.

Oh, ich habe nicht verstanden, dass es hier entscheidend ist, die Importe verfügbar zu haben, bevor wasm XHR überhaupt beginnt. Das ist dann viel kniffliger. Also das meiste was ich vorher gesagt habe ist falsch.

Damit wir die Importe haben, müssen wir den gesamten JS-Kleber heruntergeladen, geparst und ausgeführt haben. Wenn wir all das tun, bevor wasm XHR überhaupt beginnt, dann ist es ein ganz anderes Ladeschema und eine Reihe von Kompromissen als jetzt. Insbesondere bei kleinen bis mittleren Projekten wäre dies vielleicht nicht einmal eine Beschleunigung, ich denke, das müssten wir messen - wenn wir es nicht schon getan haben?

Dies wäre für Unity nicht einfach. Es würde erhebliche Änderungen am Code erfordern, den emcc ausgibt, damit der JS-Glue ausgeführt werden kann, bevor der kompilierte Code fertig ist.

Vielleicht möchten wir ein neues Modell von ausgegebenem JS in Betracht ziehen, eine JS-Datei für die Importe, eine JS-Datei für den Rest? Und es wäre Opt-In, damit wir niemanden brechen würden. Jedenfalls gibt es viele Überlegungen, und ohne Messungen ist es schwer zu erraten, was optimal ist.

@kripken Nicht vor dem XHR, sondern nachdem es abgeschlossen ist und einige Zeit bevor wir mit der Ausführung des Skripts beginnen, das den synchronen Zugriff auf das

Hmm, ich glaube, ich verstehe das immer noch nicht ganz, sorry (insbesondere verstehe ich nicht, warum die Asynchronität mit dem Abrufen der Importe interagiert - könnte der Vorteil sein, dass die Importe nicht synchron funktionieren?). Aber es scheint, dass die oben erwähnten Probleme immer noch relevant sind, auch wenn dies nach Abschluss von XHR geschieht - das heißt, wir haben jetzt ein einziges Skript, das die Importe generiert und auch die Exporte empfängt. Diese Aufteilung kann zu Kompromissen führen - wir müssen nur messen, wenn wir Zeit haben.

Das Einfügen des Exports verwendenden Codes in einen Callback funktioniert leider nicht nur aus den oben genannten Gründen, sondern wir könnten im Rahmen dieser Messungen untersuchen, indem wir einen neuen Kompilierungsmodus hinzufügen.

Die Signatur von instantiate ist WebAssembly.instantiate(bytes, importObj) , also müssen Sie importObj um das asynchrone Kompilieren+Instanziieren zu starten.

Im Offline- Gespräch beeindruckte

  1. Bereitstellung des Speichers beim Kompilieren.
  2. Bereitstellung aller Importe beim Kompilieren (Übermenge der vorherigen).
  3. Dies alles asynchron.

Angesichts der aktuellen Funktionsweise der Toolchain ist das Ausführen von 2 + 3, wie oben beschrieben, schwierig, da es bestehende Benutzer zerstört. Wir können es tun, wollen es aber möglicherweise nicht standardmäßig, zumindest nicht anfangs - wir müssten Community-Mitglieder usw. konsultieren durch Hinzufügen einer indirekten Ebene für Importe oder Exporte).

Aber einige andere Optionen sind trivialerweise einfach zu verwenden:

  • 1 + 3 würde eine neue API wie instantiate(bytes, Memory) -> Promise erfordern. Der Grund dafür, dass dies einfach zu verwenden ist, ist, dass der Speicher ohnehin früh erstellt werden kann (während fast alle anderen Importe JS-Funktionen sind, die wir nicht früh haben können).
  • 2 allein, ohne 3, dh new Instance(bytes, imports) . Das heißt, synchrone Kompilierung von Binärdaten + Importen. Der Grund dafür, dass dies einfach zu verwenden ist, ist, dass unser aktueller Code Folgendes tut: instance = new WebAssembly.Instance(new WebAssembly.Module(getBinary()), imports) also würden wir das einfach in einen API-Aufruf falten.

Ich denke, die letzte Option macht Sinn. Es bedeutet im Grunde, eine Synchronisierungsversion der neuen API hinzuzufügen, wodurch die Dinge mit unserer bestehenden synchronen Kompilierungsoption symmetrischer werden. Und ich sehe keinen Grund, die neue Optimierung der Kenntnis des Speichers zur Kompilierzeit an die asynchrone Kompilierung zu binden? (Async ist großartig, aber ein separates Problem?)

Ich schlage Metaobjekte vor, die die Importe (den Speicher) beschreiben, um die Einschränkung zu durchbrechen, dass die Importe vor dem Kompilieren zugewiesen werden müssen . Dann könnte die Methode instantiate einen Fehler auslösen, wenn der bereitgestellte Speicher nicht mit dem erwarteten übereinstimmt und die Neukompilierung nicht verzögert wird.

Es wäre auch sehr nützlich, vor der Zuweisung des linearen Speichers auf einem speicherbeschränkten Gerät kompilieren zu können, auch um die Kompilierung für Speicher, der im linearen Adressraum bei Null zugewiesen ist, und möglicherweise für Speicher mit Schutzzonen optimieren zu können, und Optimieren Sie für den Speicher mit einer festen maximalen Größe, die eine Potenz von zwei plus einer Überlaufzone sein kann. Dieses Metaobjekt könnte auch von einem wasm-Benutzer-Decoder/Re-Writer verwendet werden, um für die ausgehandelten Speichereigenschaften optimierten Code auszugeben.

Dies wurde zu Beginn dieses Projekts vor vielen Jahren als notwendig vorgeschlagen!

@kripken Obwohl die Synchronisierungs-APIs meiner Meinung nach technisch notwendig sind, sollten sie definitiv der nicht empfohlene Pfad sein, sonst werden wir jede Menge unnötigen Main-Thread-Junk einführen, eine Regression im Vergleich zu asm.js +

Ich wollte ein besseres Beispiel dafür liefern, warum wir die synchrone Kompilierung/Instanziierung explizit ablehnen (oder, wie ich in der Vergangenheit argumentiert habe, sogar verbieten) müssen. Hoffentlich bietet dies zusätzliches Diskussionsstoff.

Lassen Sie uns herauszoomen und für eine Sekunde über die Benutzererfahrung sprechen.


Hier ist ein Vergleich zwischen dem asynchronen Laden der AngryBots-Demo (über asm.js), links und synchron (über WebAssembly in V8), rechts.

comparison

Die synchrone Kompilierung bietet eine schreckliche Benutzererfahrung und unterbricht Fortschrittsbalken oder andere visuelle Hinweise auf das Laden von Anwendungen.

Was auch immer der Aufwand ist, ich denke, wir müssen zusammenarbeiten, um sicherzustellen, dass Emscripten, Unity und andere Tools standardmäßig asynchrones Laden von WebAssembly exportieren. cc @jechter @juj Nicht nur das, ich denke, wir müssen es den Entwicklern unerschwinglich machen, in die Falle zu tappen, synchronen

Wir müssen WebAssembly.compile & WebAssembly.instantiate ausdrücklich ermutigen und new WebAssembly.Module & new WebAssembly.Instance entmutigen.


Lassen Sie uns etwas tiefer eintauchen und sehen, wie schlecht der Fortschrittsbalken auf der rechten Seite für WebAssembly ist.

Hier ist ein Trace, der mit dem DevTools-Leistungsfenster erfasst wurde:

screen shot 2016-12-28 at 1 26 59 pm

Auf meinem MacBook Air dauert es 30 Sekunden, bis überhaupt ein Fortschrittsbalken angezeigt wird.

Was braucht diese Zeit? Lassen Sie uns in die ~20s hineinzoomen, nachdem das wasm-Modul heruntergeladen wurde, wenn der Hauptthread vollständig gesperrt ist:

screen shot 2016-12-28 at 2 18 36 pm

Die Kompilierung dauert ~20s und die Instanziierung ~2s.

Beachten Sie, dass der Unity-Loader in diesem Fall bereits das asynchrone WebAssembly.compile aufruft, wenn es unterstützt wird. Die 20er liegen also daran, dass V8 immer noch eine synchrone Kompilierung unter der Haube durchführt. cc @titzer @flagxor das ist etwas, was V8 wirklich reparieren muss.

Der Fehler bei der 2s-Instanziierung ist auf den Unity-Loader-Code zurückzuführen, der das synchrone new WebAssembly.Instance aufruft. Dies ist, was wir sowohl im Unity-Code als auch in Emscripten wirklich beheben müssen.


Erwähnenswert finde ich auch, dass dies nicht _nur_ das Risiko ist, dass sich ein Entwickler in den Fuß geschossen hat oder nicht. Die durchschnittliche Webseite enthält Dutzende von Skripten von Drittanbietern: Diese Skripte haben alle das Potenzial, WebAssembly mit Top-Dokumenten zu enthalten.

(Wenn Sie die Spur genauer untersuchen möchten, können Sie die vollständige Zeitleiste hier anzeigen: https://chromedevtools.github.io/timeline-viewer/?loadTimelineFromURL=https://www.dropbox.com/s/ noqjwql0pq6c1wy/wasm?dl=0)

Ich stimme 100% zu, dass Async großartig ist :) Aber ich sage, es ist orthogonal zur Optimierung, den Speicher gleichzeitig mit dem Kompilieren des Moduls zu erhalten. Und dass wir nicht trivialerweise asynchron + diese Optimierung erreichen können - wir können diese Kombination erhalten, aber entweder auf Kosten eines gewissen Overheads oder auf Kosten von Zeit, wenn wir ein neues Flag dafür einführen und einen Weg finden, es als Standard einzustufen .

Wenn wir also diese Speicher-/Kompilierungsoptimierung jetzt standardmäßig aktiviert und mit voller Effizienz möchten, ist es ein Problem, sie auch an die Asynchronität zu binden. Aber wenn es entweder nicht dringend ist, oder wir damit einverstanden sind, dass es nicht standardmäßig aktiviert ist, oder wir mit etwas neuem Overhead einverstanden sind, dann ist das kein Problem.

Da Entwickler sich für wasm entscheiden, ist es meiner Meinung nach sinnvoll, die Gelegenheit zu nutzen, die Struktur der obersten Ebene mit einem neuen Standard ein wenig zu ändern (mit einer Möglichkeit, bei Bedarf das alte Verhalten zu aktivieren). Personen, die Synchronisierung verwenden

@kripken Ich denke, Sie kommen zu anderen Schlussfolgerungen als ich, @lukewagner und @s3ththompson , denn für Sie ist es am wichtigsten, bestehenden Entwicklern einen nahtlosen Übergang von asm.js zu bieten.

Ist das richtig?

Ich stimme zu, dass dies ein berechtigtes Anliegen ist. Ich gewichte es niedriger, weil wir IMO mit WebAssembly viel mehr Entwickler einbringen als asm.js. Ich möchte es vermeiden, Early Adopters zu verbrennen, aber bestehende IIUC-Entwickler sind ziemlich flexibel und wollen asynchrone Kompilierung.

Wenn wir davon ausgehen, dass sie eine asynchrone Kompilierung wünschen und bereit sind, einige zu refaktorisieren, um sie zu erhalten, bleibt nur noch Speicher zur Kompilierungszeit übrig, was diese API bietet. Es ist sehr wünschenswert, weil es eine Falle vermeidet.

Sie haben vorhin Ihre Besorgnis über den Arbeitsaufwand auf der Emscripten-Seite geäußert, um dies zu unterstützen. Glaubst du immer noch, dass das ein Problem ist?

FWIW, Unity 5.6 verwendet WebAssembly.instantiate.

jonas

Am 28. Dezember 2016 um 23:42 Uhr schrieb Seth Thompson [email protected] :

Ich wollte ein besseres Beispiel dafür liefern, warum wir die synchrone Kompilierung/Instanziierung explizit ablehnen (oder, wie ich in der Vergangenheit argumentiert habe, sogar verbieten) müssen. Hoffentlich bietet dies zusätzliches Diskussionsstoff.

Lassen Sie uns herauszoomen und für eine Sekunde über die Benutzererfahrung sprechen.

Hier ist ein Vergleich zwischen dem Laden der AngryBots-Demo mit asynchronen Aufrufen (über asm.js) links und synchronen Aufrufen (aktuelle WebAssembly-Implementierung) rechts.

Die synchrone Kompilierung bietet eine schreckliche Benutzererfahrung und unterbricht Fortschrittsbalken oder andere visuelle Hinweise auf das Laden von Anwendungen.

Was auch immer der Aufwand ist, ich denke, wir müssen zusammenarbeiten, um sicherzustellen, dass Emscripten, Unity und andere Tools standardmäßig asynchrones Laden von WebAssembly exportieren. cc @jechter @juj Nicht nur das, ich denke, wir müssen es den Entwicklern unerschwinglich machen, in die Falle zu tappen, synchronen

Wir müssen WebAssembly.compile & WebAssembly.instantiate ausdrücklich ermutigen und von neuem WebAssembly.Module & neuer WebAssembly.Instance abraten.

Lassen Sie uns etwas tiefer eintauchen und sehen, wie schlecht der Fortschrittsbalken auf der rechten Seite für WebAssembly ist.

Hier ist ein Trace, der mit dem DevTools-Leistungsfenster erfasst wurde:

Auf meinem MacBook Air dauert es 30 Sekunden, bis überhaupt ein Fortschrittsbalken angezeigt wird.

Was braucht diese Zeit? Lassen Sie uns in die ~20s hineinzoomen, nachdem das wasm-Modul heruntergeladen wurde, wenn der Hauptthread vollständig gesperrt ist:

Die Kompilierung dauert ~20s und die Instanziierung ~2s.

Beachten Sie, dass der Unity-Loader in diesem Fall bereits die asynchrone WebAssembly.compile aufruft, wenn sie unterstützt wird. Die 20er liegen also daran, dass V8 immer noch eine synchrone Kompilierung unter der Haube durchführt. cc @titzer @flagxor das ist etwas, was V8 wirklich reparieren muss.

Der Fehler bei der 2s-Instanziierung ist auf den Unity-Loader-Code zurückzuführen, der die synchrone neue WebAssembly.Instance aufruft. Dies ist, was wir sowohl im Unity-Code als auch in Emscripten wirklich beheben müssen.

Erwähnenswert finde ich auch, dass hier nicht nur die Gefahr besteht, dass sich ein Entwickler selbst in den Fuß schießt oder nicht. Die durchschnittliche Webseite enthält Dutzende von Skripten von Drittanbietern: Diese Skripte haben alle das Potenzial, WebAssembly mit Top-Dokumenten zu enthalten.

(Wenn Sie die Spur genauer untersuchen möchten, können Sie die vollständige Zeitleiste hier anzeigen: https://chromedevtools.github.io/timeline-viewer/?loadTimelineFromURL=https://www.dropbox.com/s/ noqjwql0pq6c1wy/wasm?dl=0)


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail, zeigen Sie sie auf GitHub an oder schalten Sie den Thread stumm.

@jfbastien Vielleicht verstehe ich nicht, was du mit "Schlussfolgerungen" meinst. Ich präsentiere hier meistens nur Optionen. Ich selbst habe keine Schlussfolgerung.

Wenn wir jetzt die Option Memory-at-Compile-Time wünschen, habe ich ein paar Optionen vorgeschlagen, mit denen wir es so schnell wie möglich haben können, indem wir unsere aktuelle Synchronisierungskompilierung trivial modifizieren.

Oder wenn wir diese Option jetzt asynchron wünschen, habe ich eine Option vorgeschlagen ("1 + 3", dh nicht alle Importe zur Kompilierzeit erhalten, nur den Speicher), die uns auch so schnell wie möglich zur Verfügung stellen kann.

Oder wenn wir uns jetzt auf eine asynchrone Version dieser Option mit der aktuellen API (nicht "1 + 3") konzentrieren möchten, können wir dies auch erreichen, es bedarf nur einiger sorgfältiger Planung, da dies eine bahnbrechende Änderung wäre. Ich glaube nicht, dass es für uns hier eine Option ist, bestehende Benutzer zu sperren, also müssten wir die Community konsultieren. Möglicherweise gibt es wenig Bedenken und wir können es einfach tun. In diesem Fall hängt der Aufwand davon ab, wie viel Overhead wir zu tolerieren bereit sind - wenn wir überhaupt keinen Overhead akzeptieren können, kann es viel sein. Oder möglicherweise gibt es Bedenken, was meine persönliche Intuition ist - jede grundlegende Änderung muss schrittweise vorgenommen werden - in diesem Fall dauert es länger, wir würden wahrscheinlich mit einer neuen Option beginnen und schließlich planen, sie zum Standard zu machen.

Wieder keine Schlussfolgerungen von mir. Alle oben genannten Optionen sind. Es hängt wirklich davon ab, was Ihnen mehr am Herzen liegt: Memory-at-Compile-time oder async oder beides; und das Problem, neuen Overhead zu tolerieren vs. nicht; und ob Sie dies dringend wünschen oder warten können usw. usw. Auf der emscripten-Seite helfe ich Ihnen gerne bei jedem der oben genannten Punkte, wie auch immer die Leute entscheiden.

Oder wenn wir uns jetzt auf eine asynchrone Version dieser Option mit der aktuellen API (nicht "1 + 3") konzentrieren möchten, können wir dies auch erreichen, es bedarf nur einiger sorgfältiger Planung, da dies eine bahnbrechende Änderung wäre. Ich glaube nicht, dass es für uns hier eine Option ist, bestehende Benutzer zu sperren, also müssten wir die Community konsultieren. Möglicherweise gibt es wenig Bedenken und wir können es einfach tun. In diesem Fall hängt der Aufwand davon ab, wie viel Overhead wir zu tolerieren bereit sind - wenn wir überhaupt keinen Overhead akzeptieren können, kann es viel sein. Oder möglicherweise gibt es Bedenken, was meine persönliche Intuition ist - jede grundlegende Änderung muss schrittweise vorgenommen werden - in diesem Fall dauert es länger, wir würden wahrscheinlich mit einer neuen Option beginnen und schließlich planen, sie zum Standard zu machen.

Um sicher zu gehen, verstehe ich:

  1. Wie hoch sind die Gemeinkosten?

    • Sind diese Overheads inhärent mit Async + Memory-at-Compile-Time oder sind es Implementierungsbeschränkungen, die später behoben werden können? IMO, wenn sie A + M inhärent sind, sollten wir die API so schnell wie möglich reparieren.

    • IIUC die Gemeinkosten sind entweder bei Importen oder Exporten eine vorübergehende Sache und sind nicht A+M inhärent, ist das richtig? Könnten Sie das etwas genauer ausführen?

  2. Von welchen Benutzern sprechen wir über das Brechen? asm.js-Benutzer, die denselben Code auch für wasm verwenden möchten?

Anders ausgedrückt: Was wäre der ideale Endzustand für WebAssembly, wenn wir unendlich viel Zeit hätten und kein "Vermächtnis"? Ich denke, wir haben für diesen idealen Endzustand konzipiert, und Sie weisen auf Hindernisse auf dem Weg hin, aber ich bin mir immer noch nicht sicher, ob das der Fall ist. Wenn ja, bin ich mir nicht sicher, ob ich über genügend Informationen verfüge, um herauszufinden, ob WebAssembly eine Notlösung braucht, um den Übergang zu erleichtern, oder ob eine vorübergehende Belastung für eine Untergruppe von Emscripten-Benutzern (und welche Benutzer) akzeptabel ist.

Zu den Overheads in einigen der Optionen: Wie oben beschrieben, müssen wir die Importe derzeit synchron senden und die Exporte empfangen, und wir haben die JS-Importe erst, wenn die JS fertig ist (aber wir haben den Speicher !). Eine Möglichkeit, dies zu umgehen, besteht darin, den Importen oder Exporten eine Umleitungsebene hinzuzufügen. Zum Beispiel könnten wir sehr früh Thunks anstelle der echten Importe bereitstellen (im HTML, vor dem JS), also scheinen wir sie synchron bereitzustellen (aber es sind nur die Thunks, die noch vor dem JS trivial zu erstellen sind). . Später, wenn wir das JS haben, können wir die Thunks so aktualisieren, dass sie auf die richtige Stelle zeigen. Dies würde einen zusätzlichen JS-Aufruf und eine JS-Objektsuche für jeden Importaufruf verursachen. Auch etwas Overhead in Codegröße und Start.

Über das Brechen von Benutzern: Wir möchten, dass bestehende emscripten-Benutzer in der Lage sind, eine Flagge zu zeigen und wasm zu

dass wir die JS-Importe erst haben, wenn die JS fertig ist (aber wir haben den Speicher!)

Das scheint nebensächlich und nichts, was wir mit einer API für immer in die Zeit einbacken sollten. Darüber hinaus müssen wir aus den von @s3ththompson erklärten Gründen, selbst wenn wir das sowieso asynchron @jfbastien zu, dass wir hier unsere ideale

Über das Brechen von Benutzern: Wenn wir einen einfachen Migrationspfad anbieten (wie ein "Onload" -Ereignis/-Versprechen), sollte es für Benutzer einfach sein, zu migrieren, wobei die Karotte alle wichtigen Vorteile darstellt. Ich glaube nicht, dass wir Beweise dafür haben, dass die exakte Quell-Backkompatibilität von asm.js eine zwingende Voraussetzung für die Standardkonfiguration ist; Wir haben viele Beispiele für das Gegenteil: Benutzer, die bereit sind, alles zu tun, um die optimale Leistung zu erzielen, denn das ist natürlich der springende Punkt.

Im Offline-Gespräch mit @lukewagner denken wir, dass es am besten ist, Instanziieren für wasm in der empfohlenen Standardeinstellung in emscripten zu aktivieren, indem eine Indirektionsebene hinzugefügt wird (wie oben beschrieben, aber bei den Exporten). Es wird wahrscheinlich für die meisten Anwendungsfälle einen vernachlässigbaren Overhead haben. Das bringt uns also die Vorteile, die wir uns hier alle wünschen.

Irgendwann können wir diesen kleinen Overhead loswerden, aber es wird eine bahnbrechende Änderung sein, die eine Menge mehr Planung und Diskussionen über Mailinglisten usw. Da wir jedoch denken, dass der Overhead der Indirektionsschicht sehr gering ist, ist dieser Teil nicht dringend.

@krippen super ! Es wäre hilfreich, ein Diagramm zu haben, das verschiedene Instanzen / JS / Importe / Exporte / Speicher / Tabellen zeigt. Möchten Sie diese Diskussion woanders hin verschieben (Emscripten / Binaryen Repo)? Ich habe ein mentales Bild davon, wie C++-Code meiner Meinung nach organisiert werden sollte, aber es ist jetzt ziemlich offensichtlich, dass Sie nicht das gleiche Bild haben! Sie haben dort mehr Erfahrung, also würde ich gerne daraus lernen und helfen, wo ich kann.

IIUC Emscripten verwendet dies jetzt standardmäßig. Schließen.

Beachten Sie für die Nachverfolgung von comment , dass synchrone Kompilierung und Instanziierung nützlich sind. Insbesondere bin ich kürzlich in eine Situation geraten, in der ich ein WebAssembly-Modul in Node.js ( v7.7.2 ) synchron laden und kompilieren wollte. Wenn nur APIs verfügbar sind, die Versprechen zurückgeben, bedeutet dies, dass ich keinen synchronen Export bereitstellen könnte.

Denken Sie bei der Entscheidung, ob Sie entweder asynchrone, synchronisierte oder beide APIs bereitstellen möchten, daran, dass ein Browserkontext nicht die einzige Umgebung ist, in der Benutzer WebAssembly verwenden möchten. Eine Reihe von Leuten, mich eingeschlossen, interessieren sich für das Potenzial von WebAssembly als effizientes Kompilierungsziel in Kombination mit der Node.js-Laufzeit, ähnlich der JVM. Während asynchroner Import/Export in Node.js landen kann , wird der synchrone Import und Export auf absehbare Zeit das vorherrschende Muster bleiben. In diesem Fall ist die Möglichkeit, ein WebAssembly-Modul zu laden und dieses Modul synchron zu kompilieren und zu instanziieren, sehr nützlich.

@kgryte Ich hätte klarstellen sollen, dass sich mein Kommentar hauptsächlich auf den Browser als Ausführungskontext bezog. Wir sind auf einer API-Oberfläche gelandet, die immer noch die synchronen APIs verfügbar macht. Browser können Modulen, die an die synchronen APIs übergeben werden, eine Größenbeschränkung auferlegen (Chrome tut dies beispielsweise bereits), aber diese Beschränkung ist vom Einbetter konfigurierbar und sollte nicht für Node.

@s3ththompson Danke für die Klarstellung.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

jfbastien picture jfbastien  ·  6Kommentare

nikhedonia picture nikhedonia  ·  7Kommentare

artem-v-shamsutdinov picture artem-v-shamsutdinov  ·  6Kommentare

bobOnGitHub picture bobOnGitHub  ·  6Kommentare

frehberg picture frehberg  ·  6Kommentare