Design: Bitte unterstützen Sie willkürliche Labels und Gotos.

Erstellt am 8. Sept. 2016  ·  159Kommentare  ·  Quelle: WebAssembly/design

Ich möchte darauf hinweisen, dass ich nicht an der Webassemblierung beteiligt war,
und ich betreue keine großen oder weit verbreiteten Compiler (nur meine eigenen)
spielzeugartige Sprache, kleinere Beiträge zum QBE-Compiler-Backend und ein
Praktikum im Compiler-Team von IBM), aber am Ende wurde ich ein bisschen wütend, und
wurde ermutigt, weiter zu teilen.

Also, während es mir ein bisschen unangenehm ist, einzuspringen und größere Änderungen vorzuschlagen
zu einem Projekt, an dem ich nicht gearbeitet habe... hier geht es:

Meine Beschwerden:

Wenn ich einen Compiler schreibe, würde ich als erstes mit dem High-Level anfangen
Struktur -- Schleifen, if-Anweisungen usw. -- überprüft sie auf Semantik,
Typprüfung durchführen und so weiter. Das zweite, was ich mit ihnen mache, ist sie einfach zu werfen
aus und auf Basisblöcke und möglicherweise auf SSA-Form abflachen. In einigen anderen Teilen
der Compilerwelt ist ein beliebtes Format der Fortsetzungsübergabestil. Ich bin nicht
ein Experte für das Kompilieren mit Fortsetzungs-Passing-Stil, aber es scheint auch nicht so zu sein
eine gute Passform für die Schleifen und bereichsbezogenen Blöcke sein, die die Webassembly zu haben scheint
umarmt.

Ich möchte argumentieren, dass ein flacheres, auf Goto basierendes Format viel nützlicher wäre als
ein Ziel für Compiler-Entwickler und würde die
Schreiben eines brauchbaren Polyfill.

Ich persönlich bin auch kein großer Fan von verschachtelten komplexen Ausdrücken. Sie sind ein bisschen
umständlicher zu konsumieren, besonders wenn innere Knoten Nebenwirkungen haben können, aber ich
lehnen Sie sie als Compiler-Implementierer nicht stark ab – Die Web-Assembly
JIT kann sie verbrauchen, ich kann sie ignorieren und die Anweisungen generieren, die zuordnen
zu meinem IR. Sie bringen mich nicht dazu, Tische umzudrehen.

Das größere Problem sind Schleifen, Blöcke und andere syntaktische Elemente
dass Sie als optimierender Compiler-Autor sehr bemüht sind, sich als
Graph mit Verzweigungen, die Kanten darstellen; Die expliziten Kontrollflusskonstrukte
sind ein Hindernis. Rekonstruieren Sie sie aus dem Diagramm, wenn Sie es tatsächlich getan haben
die gewünschten Optimierungen sind sicherlich möglich, aber es ist eine ganze Menge
Komplexität, um ein komplexeres Format zu umgehen. Und das nervt mich: Sowohl die
Produzent und Konsument arbeiten an völlig erfundenen Problemen
was durch einfaches Weglassen komplexer Kontrollflusskonstrukte vermieden werden würde
aus der Webassembly.

Darüber hinaus führt das Bestehen auf höherstufigen Konstrukten zu einigen
pathologische Fälle. Zum Beispiel endet Duffs Gerät mit einem schrecklichen Netz
Assembly-Ausgabe, wie man sie beim Herumspielen in
Das Umgekehrte gilt jedoch nicht: Alles, was ausgedrückt werden kann
in Webassembler kann in einigen trivial in ein Äquivalent umgewandelt werden
unstrukturiertes, goto-basiertes Format.

Daher möchte ich zumindest vorschlagen, dass das Webassembly-Team Folgendes hinzufügt
Unterstützung für beliebige Labels und Gotos. Wenn sie sich dafür entscheiden, das höhere zu behalten
Levelkonstrukte wäre es ein bisschen verschwenderische Komplexität, aber immerhin
Compiler-Autoren wie ich könnten sie ignorieren und eine Ausgabe generieren
direkt.

Polyfilling:

Eine der Bedenken, die ich bei dieser Diskussion gehört habe, ist, dass die Schleife
und eine blockbasierte Struktur ermöglicht ein einfacheres Polyfilling der Bahnanordnung.
Das ist zwar nicht ganz falsch, aber ich denke, dass eine einfache Polyfill-Lösung
für Labels und Gotos ist möglich. Wobei es vielleicht nicht ganz so optimal ist,
Ich denke, dass es im Bytecode der Reihe nach ein bisschen Hässlichkeit wert ist
um zu vermeiden, dass ein neues Tool mit eingebauten technischen Schulden gestartet wird.

Wenn wir eine LLVM (oder QBE) ähnliche Syntax für die Web-Assembly annehmen, dann etwas Code
das sieht aus wie:

int f(int x) {
    if (x == 42)
        return 123;
    else
        return 666;
}

könnte kompilieren zu:

 func @f(%x : i32) {
    %1 = test %x 42
jmp %1 iftrue iffalse

 L0:
    %r =i 123
jmp LRet
 L1:
    %r =i 666
jmp LRet
 Lret:
    ret %r
 }

Dies könnte in Javascript polygefüllt werden, das wie folgt aussieht:

function f(x) {
    var __label = L0;
    var __ret;

    while (__label != LRet) {
        switch (__label) {
        case L0:
            var _v1 = (x == 42)
            if (_v1) {__lablel = L1;} else {label = L2;}
            break;
        case L1:
            __ret = 123
            __label = LRet
            break;
        case L2;
            __ret = 666
            __label = LRet
            break;
        default:
            assert(false);
            break;
    }
}

Ist es hässlich? Ja. Ist es wichtig? Hoffentlich, wenn die Webassembly abhebt,
nicht für lange.

Und wenn nicht:

Nun, wenn ich jemals dazu komme, auf Webassemblys abzuzielen, würde ich wohl Code generieren
Verwenden Sie den Ansatz, den ich im Polyfill erwähnt habe, und tun Sie mein Bestes, um alle zu ignorieren
die High-Level-Konstrukte, in der Hoffnung, dass die Compiler schlau genug sind, um
fang an diesem Muster an.

Aber es wäre schön, wenn wir nicht beide Seiten der Codegenerierung haben müssten
das angegebene Format umgehen.

control flow

Hilfreichster Kommentar

Die kommende Version von Go 1.11 wird experimentelle Unterstützung für WebAssembly bieten. Dies beinhaltet die volle Unterstützung aller Funktionen von Go, einschließlich Goroutinen, Kanälen usw. Allerdings ist die Leistung der generierten WebAssembly derzeit nicht so gut.

Dies liegt hauptsächlich an der fehlenden goto-Anweisung. Ohne die goto-Anweisung mussten wir in jeder Funktion auf eine Schleife und eine Sprungtabelle auf oberster Ebene zurückgreifen. Die Verwendung des Relooper-Algorithmus ist für uns keine Option, da wir beim Umschalten zwischen Goroutinen in der Lage sein müssen, die Ausführung an verschiedenen Stellen einer Funktion wieder aufzunehmen. Der Relooper kann dabei nicht helfen, sondern nur eine Goto-Anweisung.

Es ist großartig, dass WebAssembly so weit gekommen ist, dass es eine Sprache wie Go unterstützen kann. Aber um wirklich die Assemblierung des Webs zu sein, sollte WebAssembly genauso leistungsfähig sein wie andere Assemblersprachen. Go verfügt über einen fortschrittlichen Compiler, der eine sehr effiziente Assemblierung für eine Reihe anderer Plattformen ausgeben kann. Aus diesem Grund möchte ich argumentieren, dass es hauptsächlich eine Einschränkung von WebAssembly und nicht des Go-Compilers ist, dass es nicht möglich ist, mit diesem Compiler auch effiziente Assemblys für das Web auszugeben.

Alle 159 Kommentare

@oridb Wasm ist etwas für den Verbraucher optimiert, um schnell in das SSA-Formular konvertieren zu können, und die Struktur hilft hier für gängige Codemuster, sodass die Struktur für den Verbraucher nicht unbedingt eine Belastung darstellt. Ich stimme Ihrer Behauptung nicht zu, dass "beide Seiten der Codegenerierung um das angegebene Format herum arbeiten". Bei Wasm geht es in erster Linie um einen schlanken und schnellen Verbraucher, und wenn Sie Vorschläge haben, um ihn schlanker und schneller zu machen, könnte das konstruktiv sein.

Blöcke, die in eine DAG eingeordnet werden können, können in den wasm-Blöcken und -Zweigen ausgedrückt werden, wie in Ihrem Beispiel. Die Switch-Schleife ist der Stil, der bei Bedarf verwendet wird, und vielleicht können Verbraucher hier etwas Jump-Threading durchführen. Vielleicht werfen Sie einen Blick auf Binaryen, das einen Großteil der Arbeit für Ihr Compiler-Backend erledigen könnte.

Es gab andere Anfragen nach einer allgemeineren CFG-Unterstützung und einige andere Ansätze, die Schleifen verwenden, wurden erwähnt, aber vielleicht liegt der Fokus derzeit woanders.

Ich glaube nicht, dass es Pläne gibt, den 'Continuation Passing Style' explizit in der Codierung zu unterstützen, aber es wurden Blöcke und Schleifen erwähnt, die Argumente (genau wie ein Lambda) ausgeben und mehrere Werte (mehrere Lambda-Argumente) unterstützen und a . hinzufügen pick Operator, um das Referenzieren von Definitionen (die Lambda-Argumente) zu erleichtern.

die Struktur hilft hier bei gängigen Codemustern

Ich sehe kein allgemeines Codemuster, das in Bezug auf Verzweigungen zu beliebigen Labels einfacher darzustellen ist, im Vergleich zu den eingeschränkten Schleifen und Blöcken, die die Webassembly erzwingt. Ich könnte einen kleinen Vorteil sehen, wenn versucht würde, den Code dem Eingabecode für bestimmte Sprachklassen sehr ähnlich zu machen, aber das scheint kein Ziel zu sein – und die Konstrukte sind ein bisschen kahl, wenn sie dafür da wären

Blöcke, die in eine DAG eingeordnet werden können, können in den wasm-Blöcken und -Zweigen ausgedrückt werden, wie in Ihrem Beispiel.

Ja, das können sie sein. Ich würde es jedoch vorziehen, keine zusätzliche Arbeit hinzuzufügen, um zu bestimmen, welche auf diese Weise dargestellt werden können und welche zusätzliche Arbeit erfordern. Realistischerweise würde ich die zusätzliche Analyse überspringen und immer nur das Switch-Loop-Formular generieren.

Auch hier ist mein Argument nicht, dass Schleifen und Blöcke die Dinge unmöglich machen; Alles, was sie tun können, ist für eine Maschine einfacher und einfacher, mit goto, goto_if und beliebigen, unstrukturierten Labels zu schreiben.

Vielleicht werfen Sie einen Blick auf Binaryen, das einen Großteil der Arbeit für Ihr Compiler-Backend erledigen könnte.

Ich habe bereits ein brauchbares Backend, mit dem ich ziemlich zufrieden bin, und plane, den gesamten Compiler vollständig in meiner eigenen Sprache zu booten. Ich würde lieber keine ziemlich große zusätzliche Abhängigkeit hinzufügen, nur um die erzwungene Verwendung von Schleifen/Blöcken zu umgehen. Wenn ich einfach Switch-Loops verwende, ist das Ausgeben des Codes ziemlich trivial. Wenn ich versuche, die in der Webassembly vorhandenen Funktionen tatsächlich effektiv zu nutzen, anstatt so zu tun, als ob sie nicht existierten, wird es viel unangenehmer.

Es gab andere Anfragen nach einer allgemeineren CFG-Unterstützung und einige andere Ansätze, die Schleifen verwenden, wurden erwähnt, aber vielleicht ist die Kraft derzeit woanders.

Ich bin immer noch nicht davon überzeugt, dass Schleifen irgendwelche Vorteile haben – alles, was mit einer Schleife dargestellt werden kann, kann mit einem Goto und Label dargestellt werden, und es gibt schnelle und bekannte Konvertierungen in SSA von flachen Befehlslisten.

Was CPS angeht, glaube ich nicht, dass es explizite Unterstützung geben muss - es ist in FP-Kreisen beliebt, weil es ziemlich einfach direkt in Assembler umgewandelt werden kann und SSA in Bezug auf die Argumentation ähnliche Vorteile bietet (http:// mlton.org/pipermail/mlton/2003-January/023054.html); Auch hier bin ich kein Experte, aber soweit ich mich erinnere, wird die Fortsetzung des Aufrufs auf ein Label, ein paar Moves und ein Goto reduziert.

@oridb 'es gibt schnelle und bekannte Konvertierungen in SSA von flachen Anweisungslisten'

Wäre interessant zu wissen, wie sie im Vergleich zu wasm SSA-Decodern abschneiden, das ist die wichtige Frage?

Wasm verwendet derzeit einen Wertestapel, und einige der Vorteile davon würden ohne die Struktur wegfallen, es würde die Decoderleistung beeinträchtigen. Ohne den Wertestapel hätte die SSA-Decodierung auch mehr Arbeit. Ich habe einen Registerbasiscode ausprobiert und die Decodierung war langsamer (nicht sicher, wie wichtig das ist).

Würden Sie den Wertestapel beibehalten oder ein registerbasiertes Design verwenden? Wenn der Wertestapel beibehalten wird, wird es vielleicht zu einem CIL-Klon, und vielleicht könnte die Leistung von wasm mit CIL verglichen werden, hat das jemand tatsächlich überprüft?

Würden Sie den Wertestapel beibehalten oder ein registerbasiertes Design verwenden?

Da habe ich eigentlich keine starken Gefühle. Ich würde mir vorstellen, dass die Kompaktheit der Kodierung eines der größten Bedenken ist; Ein Registerdesign kann dort nicht so gut abschneiden -- oder es kann sich herausstellen, dass es sich über gzip fantastisch komprimieren lässt. Ich weiß es eigentlich nicht aus dem Kopf.

Die Leistung ist ein weiteres Problem, obwohl ich vermute, dass sie angesichts der Fähigkeit, binäre Ausgaben zwischenzuspeichern, sowie der Tatsache, dass die Downloadzeit die Decodierung um Größenordnungen überwiegen kann, weniger wichtig sein könnte.

Wäre interessant zu wissen, wie sie im Vergleich zu wasm SSA-Decodern abschneiden, das ist die wichtige Frage?

Wenn Sie in SSA decodieren, bedeutet dies, dass Sie auch ein angemessenes Maß an Optimierung vornehmen. Es würde mich interessieren, wie signifikant die Decodierungsleistung überhaupt ist. Aber ja, das ist definitiv eine gute Frage.

Danke für Ihre Fragen und Bedenken.

Es ist erwähnenswert, dass viele der Designer und Implementierer von
WebAssembly hat nicht nur Erfahrung mit hochleistungsfähigen, industriellen JITs
für JavaScript (V8, SpiderMonkey, Chakra und JavaScriptCore), aber auch in
LLVM und andere Compiler. Ich persönlich habe zwei JITs für Java implementiert
bytecode und ich kann bestätigen, dass eine Stack-Maschine mit uneingeschränkten gotos
führt zu einer ziemlichen Komplexität beim Decodieren, Verifizieren und Konstruieren von a
Compiler-IR. Tatsächlich gibt es viele Muster, die in Java ausgedrückt werden können
Bytecode, der Hochleistungs-JITs verursacht, einschließlich C1 und C2 in
HotSpot, um einfach aufzugeben und den Code so zu delegieren, dass er nur im
Dolmetscher. Im Gegensatz dazu konstruiert man eine Compiler-IR aus etwas wie einem
AST von JavaScript oder einer anderen Sprache habe ich auch gemacht. Der
Die zusätzliche Struktur eines AST erleichtert einige dieser Arbeiten erheblich.

Das Design der Kontrollflusskonstrukte von WebAssembly vereinfacht Verbraucher durch
Ermöglicht eine schnelle, einfache Überprüfung, einfache Konvertierung in ein SSA-Formular in einem Durchgang
(sogar ein Graph-IR), effektive Single-Pass-JITs und (mit Postorder und den
Stapelmaschine) relativ einfache In-Place-Interpretation. Strukturiert
Kontrolle macht irreduzible Kontrollflussdiagramme unmöglich, wodurch
eine ganze Klasse fieser Eckkoffer für Decoder und Compiler. Es auch
schafft die Voraussetzungen für die Ausnahmebehandlung im WASM-Bytecode, für den V8
entwickelt bereits gemeinsam mit der Produktion einen Prototypen
Implementierung.

Wir haben sehr viele interne Diskussionen zwischen den Mitgliedern darüber geführt
Thema, da es für einen Bytecode eine Sache ist, die sich am meisten von
andere Ziele auf Maschinenebene. Es ist jedoch nicht anders als beim Targeting
eine Quellsprache wie JavaScript (was heutzutage viele Compiler tun) und
erfordert nur eine geringfügige Reorganisation von Blöcken, um eine Struktur zu erreichen. Dort
sind dafür bekannte Algorithmen und Werkzeuge. Wir möchten einige zur Verfügung stellen
bessere Anleitung für diejenigen Produzenten, die mit einem willkürlichen CFG beginnen, um
kommunizieren dies besser. Für Sprachen mit Ausrichtung auf WASM direkt von einem AST
(was V8 jetzt für asm.js-Code tut - direkt
Übersetzen eines JavaScript-AST in WASM-Bytecode), gibt es keine Umstrukturierung
Schritt notwendig. Wir gehen davon aus, dass dies für viele Sprachtools der Fall ist
über das gesamte Spektrum, die keine ausgeklügelten IRs enthalten.

Am Do, 8. September 2016 um 9:53 Uhr, Ori Bernstein [email protected]
schrieb:

Würden Sie den Wertestapel beibehalten oder ein registerbasiertes Design verwenden?

Da habe ich eigentlich keine starken Gefühle. würde ich mir vorstellen
Kompaktheit der Kodierung wäre eines der größten Bedenken; Wie du
erwähnt, Leistung ist eine andere.

Wäre interessant zu wissen, wie sie sich mit wasm SSA-Decodern vergleichen, das
ist die wichtige Frage?

Wenn Sie nach SSA dekodieren, bedeutet dies, dass Sie auch a
ein angemessenes Maß an Optimierung. Ich wäre neugierig zu vergleichen, wie
signifikante Decodierleistung steht an erster Stelle. Aber ja, das ist
auf jeden Fall eine gute Frage.


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/796#issuecomment -245521009,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/ALnq1Iz1nn4--NL32R9ev0JPKfEnDyvqks5qn77cgaJpZM4J3ofA
.

Danke @titzer , ich habe den Verdacht entwickelt, dass die Struktur von Wasm einen Zweck hat, der über die Ähnlichkeit mit asm.js hinausgeht. Ich frage mich jedoch: Java-Bytecode (und CIL) modellieren CFGs oder den Wertestapel nicht direkt, sie müssen vom JIT abgeleitet werden. Aber in Wasm (insbesondere wenn Blocksignaturen hinzugefügt werden) kann das JIT leicht herausfinden, was mit dem Wertestapel und dem Kontrollfluss vor sich geht. Könnte das die meisten der bösen Eckfälle vermeiden, an die Sie denken?

Es gibt diese nette Optimierung, die Interpreter verwenden, die auf einem irreduziblen Kontrollfluss beruht, um die Verzweigungsvorhersage zu verbessern ...

@oridb

Ich möchte argumentieren, dass ein flacheres, auf Goto basierendes Format viel nützlicher wäre als
ein Ziel für Compiler-Entwickler

Ich stimme zu, dass gotos für viele Compiler sehr nützlich sind. Aus diesem Grund können Sie mit Tools wie Binaryen beliebige CFGs mit gotos generieren , und sie können diese sehr schnell und effizient für Sie in WebAssembly umwandeln.

Es könnte hilfreich sein, sich WebAssembly als eine Sache vorzustellen , die für die @titzer betonte). Die meisten Compiler sollten WebAssembly wahrscheinlich nicht direkt generieren, sondern stattdessen ein Tool wie Binaryen verwenden, damit sie Gotos ausgeben können, eine Reihe von Optimierungen kostenlos erhalten und sich keine Gedanken über die Details des Binärformats auf niedriger Ebene von WebAssembly machen müssen (stattdessen Sie senden eine IR über eine einfache API).

In Bezug auf das Polyfilling mit dem while-switch-Muster, das Sie erwähnen: In emscripten haben wir so angefangen, bevor wir die "relooper"-Methode zum Nachbilden von Schleifen entwickelt haben. Das While-Switch-Muster ist im Durchschnitt etwa 4x langsamer (aber in einigen Fällen deutlich weniger oder mehr, zB sind kleine Schleifen empfindlicher). Ich stimme Ihnen zu, dass Jump-Threading-Optimierungen dies theoretisch beschleunigen könnten, die Leistung jedoch weniger vorhersehbar ist, da einige VMs dies besser machen als andere. Es ist auch in Bezug auf die Codegröße deutlich größer.

Es könnte hilfreich sein, sich WebAssembly als eine Sache vorzustellen , die für die @titzer betonte). Die meisten Compiler sollten WebAssembly wahrscheinlich nicht direkt generieren, sondern ein Tool wie Binaryen verwenden...

Ich bin immer noch nicht davon überzeugt, dass dieser Aspekt so wichtig sein wird – ich vermute wiederum, dass die Kosten für das Abrufen des Bytecodes die Verzögerung dominieren würden, die der Benutzer sieht, wobei die zweitgrößten Kosten die durchgeführten Optimierungen sind und nicht das Parsen und die Validierung. . Ich gehe auch davon aus / hoffe, dass der Bytecode weggeworfen wird und die kompilierte Ausgabe zwischengespeichert wird, was die Kompilierung effektiv zu einmaligen Kosten macht.

Aber wenn Sie die Nutzung von Webbrowsern optimieren, warum definieren Sie Webassembly nicht einfach als SSA, was meiner Meinung nach eher meinen Erwartungen entspricht und weniger Aufwand für die Konvertierung in SSA erfordert?

Sie können während des Downloads mit dem Parsen und Kompilieren beginnen, und einige VMs führen möglicherweise keine vollständige Kompilierung im Voraus durch (sie verwenden beispielsweise nur eine einfache Baseline). Daher können die Download- und Kompilierungszeiten kürzer als erwartet sein, und als Ergebnis können das Parsen und die Validierung einen erheblichen Faktor für die Gesamtverzögerung darstellen, die der Benutzer sieht.

In Bezug auf SSA-Darstellungen haben sie in der Regel große Codegrößen. SSA eignet sich hervorragend zum Optimieren von Code, jedoch nicht zum kompakten Serialisieren von Code.

@oridb Siehe den Kommentar von @titzer 'Das Design der Kontrollflusskonstrukte von WebAssembly vereinfacht die Verbraucher, indem es eine schnelle, einfache Verifizierung und eine einfache Konvertierung in ein SSA-Formular in einem Durchgang ermöglicht ...' - es kann _verified_ SSA in einem Durchgang generieren. Selbst wenn wasm SSA für die Codierung verwendet, hat es immer noch die Last, es zu verifizieren und die Dominatorstruktur zu berechnen, was mit den wasm-Kontrollflussbeschränkungen einfach ist.

Ein Großteil der Codierungseffizienz von wasm scheint darauf zurückzuführen zu sein, dass sie für das gemeinsame Codemuster optimiert ist, in dem Definitionen eine einzige Verwendung haben, die in Stapelreihenfolge verwendet wird. Ich gehe davon aus, dass eine SSA-Codierung dies auch tun könnte, sodass sie eine ähnliche Codierungseffizienz aufweisen könnte. Operatoren wie if_else für Rautenmuster helfen ebenfalls sehr. Aber ohne die wasm-Struktur sieht es so aus, als müssten alle Basisblöcke Definitionen aus Registern lesen und Ergebnisse in Register schreiben, und das wäre möglicherweise nicht so effizient. Ich denke beispielsweise, dass wasm mit einem pick Operator noch besser abschneiden kann, der bereichsbezogene Stack-Werte im Stack und über grundlegende Blockgrenzen hinweg referenzieren könnte.

Ich denke, wasm ist nicht weit davon entfernt, den meisten Code im SSA-Stil zu codieren. Wenn Definitionen als Basisblockausgaben den Bereichsbaum hinaufgereicht wurden, könnte er vollständig sein. Könnte die SSA-Codierung orthogonal zur CFG-Angelegenheit sein. ZB könnte es eine SSA-Kodierung mit den CFG-Einschränkungen von wasm geben, es könnte eine registerbasierte VM mit den CFG-Einschränkungen geben.

Ein Ziel von wasm besteht darin, die Optimierungslast aus dem Laufzeitkonsumenten herauszuverlagern. Es gibt starke Widerstände gegen das Hinzufügen von Komplexität im Laufzeitcompiler, da dies die Angriffsfläche erhöht. Ein großer Teil der Designherausforderung besteht darin, sich zu fragen, was getan werden kann, um den Laufzeitcompiler zu vereinfachen, ohne die Leistung zu beeinträchtigen, und es gibt viele Diskussionen!

Nun, jetzt ist es wahrscheinlich zu spät, aber ich möchte die Idee in Frage stellen, dass der Relooper-Algorithmus oder Varianten davon in allen Fällen "gut genug" Ergebnisse liefern können. In den meisten Fällen können sie das eindeutig, da die meisten Quellcodes zunächst keinen irreduziblen Kontrollfluss enthalten, Optimierungen die Dinge normalerweise nicht zu haarig machen, und wenn sie es tun, z nicht zu. Aber was ist mit pathologischen Fällen? Was ist beispielsweise, wenn Sie eine Coroutine haben, die ein Compiler in eine reguläre Funktion mit einer Struktur wie diesem Pseudo-C umgewandelt hat:

void transformed_coroutine(struct autogenerated_context_struct *ctx) {
    int arg1, arg2; // function args
    int var1, var2, var3, …; // all vars used by the function
    switch (ctx->current_label) { // restore state
    case 0:
        // initial state, load function args caller supplied and proceed to start
        arg1 = ctx->arg1;
        arg2 = ctx->arg2;
        break;
    case 1: 
        // restore all vars which are live at label 1, then jump there
        var2 = ctx->var2; 
        var3 = ctx->var3;
        goto resume_1;
    [more cases…]
    }

    [main body goes here...]
    [somewhere deep in nested control flow:]
        // originally a yield/await/etc.
        ctx->var2 = var2;
        ctx->var3 = var3;
        ctx->current_label = 1;
        return;
        resume_1:
        // continue on
}

Sie haben also meistens einen normalen Kontrollfluss, aber mit einigen Gotos, die auf die Mitte zeigen. So funktionieren LLVM-Coroutinen ungefähr.

Ich glaube nicht, dass es eine schöne Möglichkeit gibt, so etwas zu wiederholen, wenn der "normale" Kontrollfluss komplex genug ist. (Könnte falsch sein.) Entweder dupliziert man massive Teile der Funktion, die möglicherweise eine separate Kopie für jeden Fließpunkt benötigen, oder man verwandelt das Ganze in einen riesigen Schalter, der laut

Die VM könnte den Overhead eines riesigen Switches mit Jump-Threading-Optimierungen reduzieren, aber es ist sicherlich teurer für die VM, diese Optimierungen durchzuführen, im Wesentlichen zu erraten, wie der Code auf Gotos reduziert wird, als nur explizite Gotos zu akzeptieren. Wie @kripken sagt, ist es auch weniger vorhersehbar.

Vielleicht ist eine solche Transformation zu Beginn eine schlechte Idee, da danach nichts mehr dominiert, so dass SSA-basierte Optimierungen nicht viel bewirken können ... vielleicht ist es besser auf Assembly-Ebene, vielleicht sollte wasm stattdessen stattdessen native Coroutine-Unterstützung erhalten? Aber der Compiler kann die meisten Optimierungen vor der Transformation durchführen, und es scheint, dass zumindest die Designer von LLVM-Coroutinen keine dringende Notwendigkeit sahen, die Transformation bis zur Codegenerierung zu verschieben. Da andererseits die genaue Semantik, die die Leute von Coroutinen erwarten (z Compiler), ist es flexibler, bereits transformierten Code ordnungsgemäß zu unterstützen, als die VM die Transformation durchführen zu lassen.

Wie auch immer, Coroutinen sind nur ein Beispiel. Ein weiteres Beispiel, das mir einfällt, ist die Implementierung einer VM-in-einer-VM. Während ein häufigeres Merkmal von JITs Side- Exits sind , die kein goto erfordern, gibt es Situationen, die Side- Einträge erfordern – wiederum, die goto in der Mitte von Schleifen und dergleichen erfordern. Ein anderer wären optimierte Interpreter: Nicht dass Interpreter, die auf wasm abzielen, wirklich mit denen übereinstimmen können, die auf nativen Code abzielen, was zumindest die Leistung mit berechneten Gotos verbessern kann und für mehr in die Assemblierung eintauchen können… Verzweigungsprädiktor, indem Sie jedem Fall einen eigenen Sprungbefehl geben, sodass Sie möglicherweise einen Teil des Effekts replizieren können, indem Sie nach jedem Opcode-Handler einen separaten Schalter haben, bei dem die Fälle alle nur gotos wären. Oder haben Sie zumindest ein oder zwei, um nach bestimmten Anweisungen zu suchen, die häufig nach der aktuellen kommen. Es gibt einige Sonderfälle dieses Musters, die mit einem strukturierten Kontrollfluss dargestellt werden können, aber nicht den allgemeinen Fall. Und so weiter…

Sicherlich gibt es eine Möglichkeit, einen beliebigen Kontrollfluss zuzulassen, ohne dass die VM viel Arbeit verrichten muss. Strohmann-Idee, könnte kaputt sein: Sie könnten ein Schema haben, bei dem Sprünge zu untergeordneten Bereichen erlaubt sind, aber nur, wenn die Anzahl der Bereiche, die Sie eingeben müssen, geringer ist als ein vom Zielblock definiertes Limit. Das Limit wäre standardmäßig 0 (keine Sprünge von übergeordneten Geltungsbereichen), wodurch die aktuelle Semantik beibehalten wird, und das Limit eines Blocks darf nicht größer als das Limit des übergeordneten Blocks + 1 sein (einfach zu überprüfen). Und die VM würde ihre Dominanzheuristik von "X dominiert Y, wenn es ein Elternteil von Y ist" zu "X dominiert Y, wenn es ein Elternteil von Y mit einer Entfernung größer als Ys Kind-Sprunggrenze ist" ändern. (Dies ist eine konservative Näherung, die nicht garantiert die exakte Dominatormenge darstellt, aber das gleiche gilt für die vorhandene Heuristik – es ist möglich, dass ein innerer Block die untere Hälfte eines äußeren dominiert.) Da nur Code mit irreduziblem Kontrollfluss einen Grenzwert angeben müsste, würde dies im allgemeinen Fall die Codegröße nicht erhöhen.

Bearbeiten: Interessanterweise würde dies die Blockstruktur im Grunde zu einer Darstellung des Dominanzbaums machen. Ich denke, es wäre viel einfacher, das direkt auszudrücken: ein Baum von Basisblöcken, bei dem ein Block zu einem Geschwister-, Vorfahren- oder unmittelbaren Kindblock springen darf, aber nicht zu einem weiteren Nachkommen. Ich bin mir nicht sicher, wie sich das am besten auf die vorhandene Scope-Struktur abbilden lässt, bei der ein "Block" aus mehreren Basisblöcken mit dazwischenliegenden Unterschleifen bestehen kann.

FWIW: Wasm hat ein besonderes Design, das in wenigen sehr aussagekräftigen Worten erklärt wird, "außer dass die Verschachtelungsbeschränkung es unmöglich macht, von außerhalb der Schleife in die Mitte einer Schleife zu verzweigen".

Wenn es nur eine DAG wäre, könnte die Validierung nur prüfen, ob Verzweigungen vorwärts waren, aber bei Schleifen würde dies eine Verzweigung in die Mitte der Schleife von außerhalb der Schleife ermöglichen, daher das Design des verschachtelten Blocks.

Die CFG ist nur ein Teil dieses Designs, der andere ist der Datenfluss, und es gibt einen Stapel von Werten und Blöcke können auch organisiert werden, um den Wertestapel abzuwickeln, der den Live-Bereich sehr nützlich an den Verbraucher übermitteln kann, was die Konvertierung in SSA erspart .

Es ist möglich, wasm zu einer SSA-Codierung zu erweitern (addieren Sie pick , erlauben Sie Blöcken, mehrere Werte zurückzugeben, und lassen Sie Loop-Einträge Werte anzeigen), sodass interessanterweise die für eine effiziente SSA-Decodierung erforderlichen Einschränkungen möglicherweise nicht erforderlich sind (weil es könnte bereits SSA-kodiert sein)! Dies führt zu einer funktionalen Sprache (die aus Effizienzgründen eine Codierung im Stack-Stil haben könnte).

Wenn dies erweitert würde, um beliebiges CFG zu verarbeiten, könnte es wie folgt aussehen. Dies ist eine Codierung im SSA-Stil, daher sind Werte Konstanten. Es scheint immer noch weitgehend zum Stack-Stil zu passen, nur sind sich nicht alle Details sicher. Innerhalb von blocks könnten also Verzweigungen zu anderen beschrifteten Blöcken in diesem Satz gemacht werden, oder eine andere Konvention verwendet werden, um die Kontrolle an einen anderen Block zu übertragen. Der Code innerhalb des Blocks kann immer noch nützlich auf Werte auf dem Wertestapel weiter oben im Stapel verweisen, um die Übergabe aller Werte zu sparen.

(func f1 (arg1)
  (let ((c1 10)) ; Some values up the stack.
    (blocks ((b1 (a1 a2 a3)
                   ... (br b3)
               (br b2 (+ a1 a2 a3 arg1 c1)))
             (b2 (a1)
                 ... (br b1 ...))
             (b3 ()
                 ...))
   .. regular structured wasm ..
   (br b2 ...)
   ....
   (br b3)
    ...
   ))

Aber würden Webbrowser das intern jemals effizient handhaben?

Würde jemand mit einem Stack-Computer-Hintergrund das Codemuster erkennen und es einer Stack-Codierung zuordnen können?

Es gibt einige interessante Diskussionen über irreduzible Schleifen hier http://bboissin.appspot.com/static/upload/bboissin-thesis-2010-09-22.pdf

Ich habe nicht alles im Schnelldurchlauf verfolgt, aber es erwähnt die Konvertierung irreduzibler Schleifen in reduzierbare Schleifen durch Hinzufügen eines Eingangsknotens. Für wasm klingt es so, als würde man Schleifen einen definierten Eingang hinzufügen, der speziell für das Dispatching innerhalb der Schleife gedacht ist, ähnlich wie bei der aktuellen Lösung, aber mit einer dafür definierten Variablen. Oben erwähnt ist dies virtualisiert, wegoptimiert, in der Verarbeitung. Vielleicht wäre so etwas eine Option?

Wenn dies in Sicht ist und Hersteller bereits eine ähnliche Technik verwenden müssen, jedoch eine lokale Variable verwenden, ist es dann möglicherweise eine Überlegung wert, dass wasm, das früh produziert wird, das Potenzial hat, auf fortgeschritteneren Laufzeiten schneller zu laufen? Dies könnte auch einen Anreiz für den Wettbewerb zwischen den Laufzeiten schaffen, dies zu erkunden.

Dies wären nicht gerade willkürliche Labels und Gotos, sondern etwas, in das diese umgewandelt werden könnten und das eine Chance hat, in Zukunft effizient zusammengestellt zu werden.

Fürs Protokoll, ich bin in dieser Angelegenheit stark mit @oridb und @comex verbunden .
Ich denke, dies ist ein kritisches Thema, das angegangen werden sollte, bevor es zu spät ist.

Aufgrund der Natur von WebAssembly werden alle Fehler, die Sie jetzt machen, wahrscheinlich noch Jahrzehnte andauern (siehe Javascript!). Deshalb ist das Thema so kritisch; Vermeiden Sie es jetzt, gotos aus irgendeinem Grund zu unterstützen (z. B. um die Optimierung zu erleichtern, die --- ehrlich gesagt --- der Einfluss einer bestimmten Implementierung auf ein allgemeines Ding ist, und ehrlich gesagt, ich denke, es ist faul), und Sie werden am Ende mit Probleme auf Dauer.

Ich sehe bereits zukünftige (oder aktuelle, aber zukünftige) WebAssembly-Implementierungen, die versuchen, die üblichen while/switch-Muster zu erkennen, um Labels zu implementieren, um sie richtig zu handhaben. Das ist ein Hack.

WebAssembly ist ein sauberes Blatt, daher ist es jetzt an der Zeit, schmutzige Hacks (oder besser gesagt die Anforderungen dafür) zu vermeiden.

@darkuranium :

WebAssembly, wie es derzeit spezifiziert ist, wird bereits in Browsern und Toolchains ausgeliefert, und Entwickler haben bereits Code erstellt, der die in diesem Design vorgesehene Form annimmt. Daher können wir das Design

Wir können das Design jedoch abwärtskompatibel ergänzen. Ich glaube nicht, dass einer der Beteiligten goto für nutzlos hält. Ich vermute, wir alle verwenden goto regelmäßig und nicht nur in syntaktischer Spielweise.

Zu diesem Zeitpunkt muss jemand mit Motivation einen sinnvollen Vorschlag machen und umsetzen. Ich sehe nicht, dass ein solcher Vorschlag abgelehnt wird, wenn er solide Daten liefert.

Aufgrund der Natur von WebAssembly werden alle Fehler, die Sie jetzt machen, wahrscheinlich noch Jahrzehnte andauern (siehe Javascript!). Deshalb ist das Thema so kritisch; Vermeiden Sie es jetzt, gotos aus irgendeinem Grund zu unterstützen (z. B. um die Optimierung zu erleichtern, die --- ehrlich gesagt --- der Einfluss einer bestimmten Implementierung auf ein allgemeines Ding ist, und ehrlich gesagt, ich denke, es ist faul), und Sie werden am Ende mit Probleme auf Dauer.

Also nenne ich Ihren Bluff: Ich denke, dass es ehrlich gesagt faul ist, die Motivation, die Sie zeigen, zu haben und keinen Vorschlag und eine Umsetzung zu machen, wie ich es oben beschrieben habe.

Ich bin natürlich frech. Bedenken Sie, dass bei uns Leute für Threads, GC, SIMD usw. an unsere Türen klopfen – die alle leidenschaftlich und vernünftig argumentieren, warum ihre Funktion am wichtigsten ist – es wäre großartig, wenn Sie uns helfen könnten, eines dieser Probleme anzugehen. Es gibt Leute, die dies für die anderen Funktionen tun, die ich erwähne. Bisher keine für goto . Bitte machen Sie sich mit den Beitragsrichtlinien dieser Gruppe vertraut und machen Sie mit .

Ansonsten denke ich, dass goto ein großartiges zukünftiges Feature ist . Persönlich würde ich wahrscheinlich zuerst andere angehen, wie zum Beispiel die JIT-Code-Generierung. Das ist mein persönliches Interesse nach GC und Threads.

Hallo. Ich bin gerade dabei, eine Übersetzung von Webassembly zu IR und zurück zu Webassembly zu schreiben, und ich habe mit Leuten über dieses Thema diskutiert.

Ich wurde darauf hingewiesen, dass es schwierig ist, irreduziblen Kontrollfluss in Webassembly darzustellen. Es kann sich als problematisch erweisen, Compiler zu optimieren, die gelegentlich irreduzible Kontrollflüsse schreiben. Dies könnte so etwas wie die Schleife darunter sein, die mehrere Einstiegspunkte hat:

if (x) goto inside_loop;
// banana
while(y) {
    // things
    inside_loop:
    // do things
}

EBB-Compiler würden Folgendes erzeugen:

entry:
    cjump x, inside_loop
    // banana
    jump loop

loop:
    cjump y, exit
    // things
    jump inside_loop

inside_loop:
    // do things
    jump loop
exit:
    return

Als nächstes werden wir dies in Webassembly übersetzen. Das Problem ist, dass, obwohl wir Decompiler schon vor langer Zeit herausgefunden haben , sie immer die Möglichkeit hatten, das Goto in irreduzible Flüsse einzufügen.

Bevor es übersetzt wird, wird der Compiler Tricks dazu machen. Aber schließlich können Sie den Code durchsuchen und die Anfänge und Enden der Strukturen positionieren. Sie haben die folgenden Kandidaten, nachdem Sie die Fall-Through-Sprünge eliminiert haben:

<inside_loop, if(x)>
    // banana
<loop °>
<exit if(y)>
    // things
</inside_loop, if(x)>
    // do things
</loop ↑>
</exit>

Als nächstes müssen Sie einen Stack daraus bauen. Welche geht nach unten? Es ist entweder die 'innere Schleife' oder dann die 'Schleife'. Wir können dies nicht tun, also müssen wir den Stapel abschneiden und Dinge herumkopieren:

if
    // do things
else
    // banana
end
loop
  br out
    // things
    // do things
end

Jetzt können wir dies in Webassembly übersetzen. Entschuldigen Sie, ich bin noch nicht damit vertraut, wie diese Schleifen aufgebaut sind.

Dies ist kein besonderes Problem, wenn wir an alte Software denken. Es ist wahrscheinlich, dass die neue Software in Webassembly übersetzt wird. Aber das Problem liegt in der Funktionsweise unserer Compiler. Sie haben den Kontrollfluss mit Basisblöcken für _Dekaden_ durchgeführt und gehen davon aus, dass alles geht.

Technisch wird die Sprache hinein übersetzt und dann heraus übersetzt. Wir brauchen nur einen Mechanismus, der es den Werten ermöglicht, die Grenzen sauber und ohne Drama zu überschreiten. Der strukturierte Ablauf ist nur für Personen nützlich, die den Code lesen möchten.

Aber zum Beispiel würde Folgendes genauso gut funktionieren:

    cjump x, label(1)
    // banana
0: label
    cjump y, label(2)
    // things
1: label
    // do things
    jump label(0)
2: label
    // exit as usual, picking the values from the top of the stack.

Die Nummern wären implizit, d. h. wenn der Compiler ein 'Label' sieht, weiß er, dass er einen neuen erweiterten Block beginnt und ihm eine neue Indexnummer zuweist, die von 0 an inkrementiert wird.

Um einen statischen Stapel zu erstellen, können Sie verfolgen, wie viele Elemente sich im Stapel befinden, wenn Sie auf einen Sprung in das Etikett stoßen. Kommt es nach einem Sprung in das Label zu einem inkonsistenten Stack, ist das Programm ungültig.

Wenn Sie das oben genannte schlecht finden, können Sie auch versuchen, jedem Label eine explizite Stack-Länge hinzuzufügen (vielleicht Delta von der Stack-Größe des letzten indizierten Labels, wenn der Absolutwert schlecht für die Komprimierung ist) und eine Markierung für jeden Sprung, wie viele Werte es kopiert während des Sprungs von der Spitze des Stapels.

Ich könnte wetten, dass Sie das gzip in keiner Weise dadurch überlisten können, wie Sie den Kontrollfluss darstellen, also können Sie den Fluss wählen, der für die Leute geeignet ist, die hier am härtesten arbeiten. (Ich kann mit meiner flexiblen Compiler-Toolchain für das 'outsmarting the gzip'-Ding veranschaulichen, wenn Sie möchten, senden Sie mir einfach eine Nachricht und lassen Sie uns eine Demo erstellen!)

Ich fühle mich gerade wie ein Zerschmetterter. Lesen Sie einfach die WebAssembly-Spezifikation erneut durch und stellen Sie fest, dass der nicht reduzierbare Kontrollfluss absichtlich vom MVP weggelassen wird, vielleicht aus dem Grund, dass Emscripten das Problem in den frühen Tagen lösen musste.

Die Lösung zum Umgang mit dem irreduziblen Kontrollfluss in WebAssembly wird im Paper "Emscripten: An LLVM-to-JavaScript Compiler" erläutert. Der Relooper reorganisiert das Programm in etwa so:

_b_ = bool(x)
_b_ == 0 if
  // banana
end
block loop
  _b_ if
    // do things
    _b_ = 0
  else
    y br_if 2
    // things
    _b_ = 1
  end
  br 0
end end

Der Grund war, dass der strukturierte Kontrollfluss beim Lesen des Quellcode-Dumps hilft, und ich denke, es hilft den Polyfill-Implementierungen.

Die Leute, die von der Webassembly kompilieren, werden sich wahrscheinlich anpassen, um den zusammengeklappten Kontrollfluss zu handhaben und zu trennen.

So:

  • Wie bereits erwähnt, ist WebAssembly jetzt stabil, sodass die Zeit für eine vollständige Neuschreibung der Ausdrucksweise des Kontrollflusses vorbei ist.

    • In gewisser Hinsicht ist das bedauerlich, denn niemand hat tatsächlich getestet, ob eine direktere SSA-basierte Kodierung die gleiche Kompaktheit wie das aktuelle Design hätte erreichen können.

    • Wenn es jedoch darum geht, goto zu spezifizieren, macht das die Arbeit viel einfacher! Die blockbasierten Anweisungen sind bereits jenseits von Bikeshedding, und es ist keine große Sache zu erwarten, dass Produktionscompiler, die auf wasm abzielen, einen reduzierbaren Kontrollfluss mit ihnen ausdrücken – der Algorithmus ist nicht so schwer. Das Hauptproblem besteht darin, dass ein kleiner Teil des Kontrollflusses nicht ohne Leistungskosten ausgedrückt werden kann. Wenn wir das lösen, indem wir eine neue goto-Anweisung hinzufügen, müssen wir uns nicht annähernd so viele Gedanken über die Codierungseffizienz machen wie bei einem kompletten Redesign. Code, der goto verwendet, sollte natürlich immer noch einigermaßen kompakt sein, aber er muss nicht mit anderen Konstrukten um Kompaktheit konkurrieren; es ist nur für irreduziblen Kontrollfluss und sollte selten verwendet werden.

  • Reduzierbarkeit ist nicht besonders nützlich.

    • Die meisten Compiler-Backends verwenden eine SSA-Darstellung, die auf einem Diagramm von Basisblöcken und Verzweigungen zwischen ihnen basiert. Die Nested-Loop-Struktur, die Reduzierbarkeit garantiert, wird am Anfang so gut wie weggeworfen.

    • Ich habe die aktuellen WebAssembly-Implementierungen in JavaScriptCore, V8 und SpiderMonkey überprüft, und sie scheinen alle diesem Muster zu folgen. (V8 ist komplizierter - eine Art "Meer von Knoten" anstelle von Basisblöcken -, wirft aber auch die Verschachtelungsstruktur weg.)

    • Ausnahme : Die Schleifenanalyse kann nützlich sein, und alle drei dieser Implementierungen geben Informationen darüber an das IR weiter, welche Basisblöcke der Beginn von Schleifen sind. (Vergleichen Sie mit LLVM, das als 'schwergewichtiges' Backend, das für die AOT-Kompilierung entwickelt wurde, es wegwirft und im Backend neu berechnet. Dies ist robuster, da es Dinge finden kann, die im Quellcode nicht wie Schleifen aussehen, aber es tun nach einer Reihe von Optimierungen, aber langsamer.)

    • Die Schleifenanalyse arbeitet mit "natürlichen Schleifen", die Verzweigungen in die Mitte der Schleife verbieten, die nicht durch den Schleifenkopf gehen.

    • WebAssembly sollte weiterhin garantieren, dass loop Blöcke natürliche Schleifen sind.

    • Aber die Schleifenanalyse erfordert nicht, dass die gesamte Funktion reduzierbar ist, noch nicht einmal das Innere der Schleife: Sie verbietet nur Verzweigungen von außen nach innen. Die Basisdarstellung ist immer noch ein willkürlicher Kontrollflussgraph.

    • Ein irreduzibler Kontrollfluss erschwert die Kompilierung von WebAssembly in JavaScript (Polyfilling), da der Compiler den Relooper-Algorithmus selbst ausführen müsste.

    • Aber WebAssembly trifft bereits mehrere Entscheidungen, die jedem Compile-to-JS-Ansatz einen erheblichen Laufzeit-Overhead hinzufügen (einschließlich Unterstützung für nicht ausgerichteten Speicherzugriff und Trapping bei Zugriffen außerhalb der Grenzen), was darauf hindeutet, dass dies nicht als sehr wichtig angesehen wird.

    • Im Vergleich dazu ist es keine große Sache, den Compiler etwas komplexer zu machen.

    • Daher glaube ich nicht, dass es einen guten Grund gibt, nicht irgendeine Art von Unterstützung für irreduziblen Kontrollfluss hinzuzufügen.

  • Die wichtigsten Informationen, die zum Erstellen einer SSA-Repräsentation erforderlich sind (die konstruktionsbedingt in einem Durchgang möglich sein sollte) ist der Dominatorbaum .

    • Derzeit kann ein Backend die Dominanz basierend auf einem strukturierten Kontrollfluss schätzen. Wenn ich die Spezifikation richtig verstehe, beenden die folgenden Anweisungen einen Basisblock:

    • block :



      • Der BB, der den Block startet, wird vom vorherigen BB dominiert.*


      • Der BB, der dem entsprechenden end folgt, wird von dem BB dominiert, der den Block beginnt, aber nicht von dem BB vor end (weil es übersprungen wird, wenn es ein br Out gibt ).



    • loop :



      • Der BB, der den Block beginnt, wird vom vorherigen BB dominiert.


      • Der BB nach end wird von dem BB vor end dominiert (da Sie nach end nicht zur Anweisung gelangen, außer durch Ausführen von end ).



    • if :



      • Die if-Seite, die else-Seite und das BB nach end werden alle vom BB vor if dominiert.



    • br , return , unreachable :



      • (Der BB unmittelbar nach br , return oder unreachable ist nicht erreichbar.)



    • br_if , br_table :



      • Der BB vor br_if / br_table dominiert den nachfolgenden.



    • Bemerkenswert ist, dass dies nur eine Schätzung ist. Es kann keine falsch-positiven Ergebnisse produzieren (sagt, dass A B dominiert, obwohl dies tatsächlich der Fall ist), weil es dies nur sagt, wenn es keine Möglichkeit gibt, nach B zu gelangen, ohne durch A zu gehen, konstruktionsbedingt. Aber es kann zu falschen Negativen führen (sagt, dass A B nicht dominiert, obwohl dies tatsächlich der Fall ist), und ich glaube nicht, dass ein Single-Pass-Algorithmus diese erkennen kann (könnte falsch sein).

    • Beispiel falsch negativ:

      ```

      $outer blockieren

      Schleife

      br $äußer ;; da diese bedingungslos bricht, dominiert sie heimlich das Ende BB

      Ende

      Ende

    • Aber das ist in Ordnung, AFAIK.



      • Falsch-Positive wären schlecht, denn wenn zB Basisblock A den Basisblock B dominiert, kann der Maschinencode für B einen Registersatz in A verwenden (wenn nichts dazwischen dieses Register überschreibt). Wenn A B nicht wirklich dominiert, kann das Register einen Garbage-Wert haben.


      • Falsche Negative sind im Wesentlichen Geisterzweige, die nie auftreten. Der Compiler geht davon aus, dass diese Zweige auftreten könnten, nicht aber , dass sie müssen, so dass der erzeugte Code nur konservativer als notwendig ist.



    • Denken Sie auf jeden Fall darüber nach, wie eine goto Anweisung in Bezug auf den Dominatorbaum funktionieren sollte. Angenommen, A dominiert B, das C dominiert.

    • Wir können nicht von A nach C springen, da dies B überspringen würde (was die Dominanzannahme verletzen würde). Mit anderen Worten, wir können nicht zu nicht unmittelbaren Nachkommen springen. (Und auf der Seite des Binärproduzenten, wenn er den wahren Dominatorbaum berechnet hat, wird es nie einen solchen Sprung geben.)

    • Wir könnten sicher von A nach B springen, aber zu einem unmittelbaren Nachkommen zu gelangen, ist nicht so nützlich. Es entspricht im Grunde einer if- oder switch-Anweisung, die wir bereits ausführen können (mit der Anweisung if , wenn es nur einen binären Test gibt, oder br_table wenn es mehrere gibt).

    • Ebenfalls sicher und interessanter ist es, zu einem Geschwisterkind oder einem Geschwisterkind eines Vorfahren zu springen. Wenn wir zu unseren Geschwistern springen, haben wir die Garantie bewahrt, dass unsere Eltern unsere Geschwister dominieren, da wir unsere Eltern bereits hingerichtet haben müssen, um hierher zu gelangen (da sie auch uns dominieren). Ähnlich bei den Vorfahren.

    • Im Allgemeinen könnte eine bösartige Binärdatei auf diese Weise falsch-negative Dominanz erzeugen, aber wie gesagt, diese sind (a) bereits möglich und (b) akzeptabel.

  • Darauf basierend ist hier ein Strohmann-Vorschlag:

    • Eine neue Block-Anweisung:
    • Labels Ergebnistyp N instr* Ende
    • Es muss genau N unmittelbare untergeordnete Anweisungen geben, wobei "unmittelbares Kind" entweder eine blockartige Anweisung ( loop , block oder labels ) und alles bis zum entsprechenden end , oder eine einzelne Nicht-Block-Anweisung (die den Stack nicht beeinflussen darf).
    • Anstatt wie bei anderen blockartigen Anweisungen ein einzelnes Label zu erstellen, erstellt labels N+1 Labels: N zeigt auf die N Kinder und eines zeigt auf das Ende des labels Blocks. In jedem der Kinder beziehen sich die Label-Indizes 0 bis N-1 auf die Kinder in der Reihenfolge, und der Label-Index N bezieht sich auf das Ende.

    Mit anderen Worten, wenn Sie
    loop ;; outer labels 3 block ;; child 0 br X end nop ;; child 1 nop ;; child 2 end end

    Abhängig von X bezieht sich das br auf:

    | X | Ziel |
    | ---------- | ------ |
    | 0 | Ende der block |
    | 1 | Kind 0 (Anfang der block ) |
    | 2 | Kind 1 (nein) |
    | 3 | Kind 2 (nein) |
    | 4 | Ende von labels |
    | 5 | Anfang der äußeren Schleife |

    • Die Ausführung beginnt beim ersten Kind.

    • Wenn die Ausführung das Ende eines der Kinder erreicht, wird sie mit dem nächsten fortgesetzt. Wenn es das Ende des letzten Kindes erreicht, geht es zum ersten Kind zurück. (Dies dient der Symmetrie, da die Reihenfolge der Kinder nicht signifikant sein soll.)

    • Die Verzweigung zu einem der untergeordneten Elemente wickelt den Operandenstapel bis zu seiner Tiefe am Anfang von labels .

    • Das gilt auch für die Verzweigung zum Ende, aber wenn resulttype nicht ausgegeben und nach dem Abwickeln block .

    • Dominanz: Der Basisblock vor der Anweisung labels dominiert jedes der Kinder, sowie der BB nach dem Ende von labels . Die Kinder dominieren nicht einander oder das Ende.

    • Konstruktionshinweise:

    • N wird im Voraus angegeben, damit der Code in einem Durchgang validiert werden kann. Es wäre seltsam, bis zum Ende des labels Blocks kommen zu müssen, um die Anzahl der Kinder zu kennen, bevor man die Ziele der darin enthaltenen Indizes kennt.

    • Ich bin mir nicht sicher, ob es irgendwann eine Möglichkeit geben sollte, Werte auf dem Operandenstapel zwischen Labels zu übergeben, aber analog zu der Unfähigkeit, Werte an ein block oder loop , das beim Start nicht unterstützt werden kann mit.

Es wäre doch schön, wenn man in eine Schleife springen könnte, oder? IIUC, wenn dieser Fall berücksichtigt würde, würde die böse Kombination Schleife + br_tabelle niemals benötigt ...

Bearbeiten: oh, Sie können eine Schleife ohne loop indem Sie in labels nach oben springen. Ich kann nicht glauben, dass ich das verpasst habe.

@qwertie Wenn eine gegebene Schleife keine natürliche Schleife ist, sollte der auf wasm abzielende Compiler sie mit labels anstelle von loop ausdrücken. Es sollte nie notwendig sein, einen Schalter zur Expresssteuerung hinzuzufügen, wenn Sie sich darauf beziehen. (Schließlich könnten Sie im schlimmsten Fall nur einen riesigen labels Block mit einem Label für jeden Basisblock in der Funktion verwenden. Dies informiert den Compiler nicht über Dominanz und natürliche Schleifen, sodass Sie möglicherweise etwas verpassen Optimierungen. labels ist jedoch nur in Fällen erforderlich, in denen diese Optimierungen nicht anwendbar sind.)

Die Nested-Loop-Struktur, die Reduzierbarkeit garantiert, wird am Anfang so gut wie weggeworfen. [...] Ich habe die aktuellen WebAssembly-Implementierungen in JavaScriptCore, V8 und SpiderMonkey überprüft, und sie scheinen alle diesem Muster zu folgen.

Nicht ganz: Zumindest in SM ist der IR-Graphen kein vollständig allgemeiner Graph; wir gehen von bestimmten Grapheninvarianten aus, die sich aus der Generierung aus einer strukturierten Quelle (JS oder wasm) ergeben und vereinfachen und/oder optimieren oft die Algorithmen. Die Unterstützung einer vollständig allgemeinen CFG würde entweder das Auditieren/Ändern vieler der Durchgänge in der Pipeline erfordern, um diese Invarianten nicht anzunehmen (entweder durch Verallgemeinerung oder Pessimierung im Falle einer Irreduzibilität) oder eine Node-Splitting-Duplizierung im Voraus, um den Graphen reduzierbar zu machen. Das ist natürlich machbar, aber es ist nicht wahr, dass es sich bei Wasm nur um einen künstlichen Flaschenhals handelt.

Auch die Tatsache, dass es viele Optionen gibt und unterschiedliche Engines unterschiedliche Dinge tun, deutet darauf hin, dass die Vorhersehbarkeit der Leistung beim Vorliegen eines irreduziblen Kontrollflusses durch den Hersteller etwas vorhersehbarer wird.

Wenn wir in der Vergangenheit abwärtskompatible Pfade zum Erweitern von wasm mit willkürlicher Goto-Unterstützung diskutiert haben, ist eine große Frage, was hier der Anwendungsfall ist: Ist es "Erzeuger einfacher machen, indem kein Algorithmus vom Relooper-Typ ausgeführt werden muss" oder ist es? "effizienteres Codegen für tatsächlich nicht reduzierbaren Kontrollfluss zulassen"? Wenn es nur ersteres ist, dann würden wir wahrscheinlich ein Schema zum Einbetten beliebiger Labels/Gotos wünschen (das sowohl abwärtskompatibel ist als auch mit zukünftigem blockstrukturiertem try/catch komponiert); es ist nur eine Frage der Kosten-Nutzen-Abwägung und der oben genannten Probleme.

Aber für den letzteren Anwendungsfall haben wir beobachtet, dass, während Sie hin und wieder einen Gerätekoffer von Duff in freier Wildbahn sehen (was eigentlich keine effiziente Möglichkeit ist, eine Schleife abzuwickeln ...), oft wo Sie sehen, dass Irreduzibilität auftaucht, wo die Leistung zählt, sind Interpreterschleifen. Interpreterschleifen profitieren auch vom indirekten Threading, das ein berechnetes goto erfordert. Auch in kräftigen Offline-Compilern neigen Interpreterschleifen dazu, die schlechteste Registerzuordnung zu erhalten. Da die Leistung von Interpreterschleifen ziemlich wichtig sein kann, stellt sich die Frage, ob wir wirklich ein Kontrollfluss-Grundelement benötigen, das es der Engine ermöglicht, indirektes Threading durchzuführen und anständige regalloc auszuführen. (Dies ist eine offene Frage für mich.)

@lukewagner
Ich würde gerne mehr darüber erfahren, welche Durchgänge von Invarianten abhängen. Das von mir vorgeschlagene Design, das ein separates Konstrukt für irreduziblen Fluss verwendet, sollte es für Optimierungsdurchgänge wie LICM relativ einfach machen, diesen Fluss zu umgehen. Aber wenn es andere Arten von Brüchen gibt, an die ich nicht denke, würde ich gerne deren Natur besser verstehen, damit ich besser einschätzen kann, ob und wie sie vermieden werden können.

Wenn wir in der Vergangenheit abwärtskompatible Pfade zum Erweitern von wasm mit willkürlicher Goto-Unterstützung diskutiert haben, ist eine große Frage, was hier der Anwendungsfall ist: Ist es "Erzeuger einfacher machen, indem kein Algorithmus vom Relooper-Typ ausgeführt werden muss" oder ist es? "effizienteres Codegen für tatsächlich nicht reduzierbaren Kontrollfluss zulassen"?

Für mich ist es letzteres; Mein Vorschlag erwartet, dass die Produzenten immer noch einen Algorithmus vom Typ Relooper ausführen, um dem Backend die Arbeit zu ersparen, Dominatoren und natürliche Schleifen zu identifizieren, und nur bei Bedarf auf labels zurückgreifen. Dies würde die Hersteller jedoch noch einfacher machen. Wenn irreduzibler Kontrollfluss einen großen Nachteil hat, sollte ein idealer Produzent sehr hart arbeiten, um ihn zu vermeiden, indem er Heuristiken verwendet, um zu bestimmen, ob es effizienter ist, Code zu duplizieren, wie viel Duplizierung minimal funktionieren kann usw. Wenn der einzige Nachteil potenziell besteht Up-Schleifenoptimierungen ist dies nicht wirklich notwendig oder zumindest nicht mehr notwendig als bei einem normalen Maschinencode-Backend (das seine eigenen Schleifenoptimierungen hat).

Ich sollte wirklich mehr Daten darüber sammeln, wie häufig irreduzible Kontrollflüsse in der Praxis sind…

Ich glaube jedoch, dass die Bestrafung eines solchen Flusses im Wesentlichen willkürlich und unnötig ist. In den meisten Fällen sollte die Auswirkung auf die Gesamtlaufzeit des Programms gering sein. Wenn ein Hotspot jedoch einen nicht reduzierbaren Kontrollfluss enthält, wird dies schwerwiegende Folgen haben; In Zukunft könnten WebAssembly-Optimierungsleitfäden dies als häufigen Fallstrick aufführen und erklären, wie man ihn erkennt und vermeidet. Wenn ich richtig glaube, ist dies eine völlig unnötige Form von kognitivem Overhead für Programmierer. Und selbst wenn der Overhead gering ist, hat WebAssembly im Vergleich zu nativem Code bereits genug Overhead, um zusätzlichen Aufwand zu vermeiden.

Ich bin offen für die Überzeugung, dass mein Glaube falsch ist.

Da die Leistung von Interpreterschleifen ziemlich wichtig sein kann, stellt sich die Frage, ob wir wirklich ein Kontrollfluss-Grundelement benötigen, das es der Engine ermöglicht, indirektes Threading durchzuführen und anständige regalloc auszuführen.

Das klingt interessant, aber ich denke, es wäre besser, mit einem allgemeineren Primitiv zu beginnen. Schließlich würde ein auf Interpreter zugeschnittenes Primitiv immer noch Backends benötigen, um mit irreduziblen Kontrollflüssen umzugehen; Wenn Sie in diesen sauren Apfel beißen wollen, können Sie auch den allgemeinen Fall unterstützen.

Alternativ könnte mein Vorschlag für Dolmetscher bereits als anständiges Primitiv dienen. Wenn Sie labels mit br_table kombinieren, erhalten Sie die Möglichkeit, eine Sprungtabelle direkt auf beliebige Punkte in der Funktion zu zeigen, was sich nicht wesentlich von einem berechneten goto unterscheidet. (Im Gegensatz zu einem C-Switch, der den Kontrollfluss zumindest anfänglich auf Punkte innerhalb des Switch-Blocks lenkt; wenn die Fälle alle gotos sind, sollte der Compiler in der Lage sein, den zusätzlichen Sprung wegzuoptimieren, aber er könnte auch mehrere "redundante" switch-Anweisungen in eine um, was den Vorteil eines separaten Sprungs nach jedem Befehlshandler ruiniert.) Ich bin mir jedoch nicht sicher, was das Problem mit der Registerzuweisung ist ...

@comex Ich denke, man könnte einfach ganze Optimierungsdurchgänge auf Funktionsebene in Gegenwart eines irreduziblen Kontrollflusses abschalten (obwohl SSA-Generierung, regalloc und wahrscheinlich ein paar andere benötigt würden und daher Arbeit erfordern), aber ich ging davon aus, dass wir wollte eigentlich Qualitätscode für Funktionen mit irreduziblem Kontrollfluss generieren und dazu gehört die Prüfung jedes Algorithmus, der zuvor von einem strukturierten Graphen ausgegangen ist.

>

Die verschachtelte Schleifenstruktur, die Reduzierbarkeit garantiert, ist
am Anfang ziemlich weggeworfen. [...] Ich habe den Strom überprüft
WebAssembly-Implementierungen in JavaScriptCore, V8 und SpiderMonkey und
sie scheinen alle diesem Muster zu folgen.

Nicht ganz: Zumindest in SM ist der IR-Graphen kein vollständig allgemeiner Graph; wir
nehmen an, dass bestimmte Grapheninvarianten folgen, die daraus resultieren, dass sie aus a . erzeugt werden
strukturierte Quelle (JS oder wasm) und vereinfachen und/oder optimieren häufig die
Algorithmen.

Gleiches bei V8. Es ist tatsächlich einer meiner größten Probleme mit SSA in beiden Fällen
entsprechende Literatur und Implementierungen, die sie fast nie definieren
was eine "wohlgeformte" CFG ausmacht, aber tendenziell implizit verschiedene annimmt
sowieso undokumentierte Beschränkungen, die in der Regel durch den Bau der
Sprach-Frontend. Ich wette, dass viele / die meisten Optimierungen in bestehenden Compilern
nicht in der Lage wäre, mit wirklich willkürlichen CFGs umzugehen.

Wie @lukewagner sagt, ist der Hauptanwendungsfall für irreduzible Kontrolle wahrscheinlich
"threaded code" für optimierte Interpreter. Schwer zu sagen, wie relevant diese sind
für die Wasm-Domäne sind und ob ihre Abwesenheit tatsächlich die größte ist
Engpass.

Nachdem ich mit einer Reihe von Leuten über irreduziblen Kontrollfluss gesprochen habe
Wenn Sie Compiler-IRs recherchieren, wäre die "sauberste" Lösung wahrscheinlich das Hinzufügen
das Konzept der gegenseitig rekursiven Blöcke. Das würde zufällig zu Wasm passen
Kontrollstruktur recht gut.

Schleifenoptimierungen in LLVM ignorieren im Allgemeinen irreduziblen Kontrollfluss und versuchen nicht, ihn zu optimieren. Die Schleifenanalyse, auf der sie basieren, erkennt nur natürliche Schleifen, Sie müssen sich also nur bewusst sein, dass es CFG-Zyklen geben kann, die nicht als Schleifen erkannt werden. Natürlich sind andere Optimierungen eher lokaler Natur und funktionieren gut mit irreduziblen CFGs.

Aus dem Gedächtnis, und wahrscheinlich falsch, hat SPEC2006 eine einzelne irreduzible Schleife in 401.bzip2 und das war's. In der Praxis ist das eher selten.

Clang gibt nur eine einzelne indirectbr Anweisung in Funktionen aus, die ein berechnetes goto verwenden. Dies hat den Effekt, dass Threaded-Interpreter in natürliche Schleifen mit dem indirectbr Block als Schleifenkopf umgewandelt werden. Nach dem Verlassen von LLVM IR wird das einzelne indirectbr im Code-Generator dupliziert, um das ursprüngliche Tangle zu rekonstruieren.

Es gibt keinen Single-Pass-Verifikationsalgorithmus für irreduziblen Kontrollfluss
die mir bewusst sind. Die Designentscheidung für nur reduzierbaren Kontrollfluss war
stark von dieser Anforderung beeinflusst.

Wie bereits erwähnt, kann irreduzibler Kontrollfluss mindestens zwei modelliert werden
verschiedene Wege. Eine Schleife mit einer switch-Anweisung kann tatsächlich optimiert werden
in den ursprünglichen irreduziblen Graphen durch ein einfaches lokales Jump-Threading
Optimierung (zB durch Faltung des Musters, wo eine Zuweisung einer Konstanten
zu einer lokalen Variablen erfolgt, dann eine Verzweigung zu einer bedingten Verzweigung, die
schaltet diese lokale Variable sofort ein).

Die irreduziblen Kontrollkonstrukte sind also überhaupt nicht notwendig, und es ist
nur eine Frage einer einzigen Compiler-Backend-Transformation, um die
ursprünglichen irreduziblen Graphen und optimieren ihn (für Engines, deren Compiler
unterstützen irreduziblen Kontrollfluss – was keiner der 4 Browser tut, um die
nach meinem besten Wissen).

Am besten,
-Ben

Am Do, 20.04.2017 um 5:20 Uhr, Jakob Stoklund Olesen <
[email protected]> schrieb:

Schleifenoptimierungen in LLVM ignorieren im Allgemeinen irreduziblen Kontrollfluss
und nicht versuchen, es zu optimieren. Die Schleifenanalyse, auf der sie basieren, wird
erkennen nur natürliche Schleifen, also muss man sich nur bewusst sein, dass es möglich ist
CFG-Zyklen sein, die nicht als Schleifen erkannt werden. Natürlich andere
Optimierungen sind eher lokaler Natur und funktionieren gut mit irreduziblen
CFGs.

Aus dem Gedächtnis, und wahrscheinlich falsch, hat SPEC2006 eine einzelne irreduzible Schleife in
401.bzip2 und das war's. In der Praxis ist das eher selten.

Clang gibt nur eine einzige indirekte Anweisung in Funktionen aus, die verwenden
berechnet goto. Dies hat den Effekt, dass Gewindeinterpreten in
natürliche Schleifen mit dem indirectbr-Block als Schleifenkopf. Nach dem Verlassen
LLVM IR, das einzelne Indirectbr wird im Codegenerator mit dem Schwanz dupliziert
um das ursprüngliche Gewirr zu rekonstruieren.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

Ich kann auch weiter sagen, dass, wenn irreduzible Konstrukte zu
WebAssembly, sie würden in TurboFan (V8s Optimierungs-JIT) nicht funktionieren, also so
Funktionen würden entweder interpretiert (extrem langsam) oder
kompiliert von einem Baseline-Compiler (etwas langsamer), da wir es wahrscheinlich nicht tun werden
investieren Sie in die Aufrüstung von TurboFan, um einen nicht reduzierbaren Kontrollfluss zu unterstützen.
Das bedeutet, dass Funktionen mit irreduziblem Kontrollfluss in WebAssembly
wahrscheinlich mit viel schlechterer Leistung enden.

Natürlich wäre eine andere Option für die WebAssembly-Engine in V8, um die
relooper, um TurboFan-reduzierbare Graphen zu füttern, aber das würde die Kompilierung machen
(und Start schlimmer). Relooping sollte in meinem . ein Offline-Verfahren bleiben
Meinung, sonst landen wir bei unvermeidlichen Motorkosten.

Am besten,
-Ben

Am Montag, den 1. Mai 2017 um 12:48 Uhr schrieb Ben L. Titzer [email protected] :

Es gibt keinen Single-Pass-Verifikationsalgorithmus für irreduzible Kontrolle
Fluss, der mir bewusst ist. Die Designwahl nur für reduzierbaren Kontrollfluss
war stark von dieser Forderung geprägt.

Wie bereits erwähnt, kann irreduzibler Kontrollfluss mindestens zwei modelliert werden
verschiedene Wege. Eine Schleife mit einer switch-Anweisung kann tatsächlich optimiert werden
in den ursprünglichen irreduziblen Graphen durch ein einfaches lokales Jump-Threading
Optimierung (zB durch Faltung des Musters, wo eine Zuweisung einer Konstanten
zu einer lokalen Variablen erfolgt, dann eine Verzweigung zu einer bedingten Verzweigung, die
schaltet diese lokale Variable sofort ein).

Die irreduziblen Kontrollkonstrukte sind also überhaupt nicht notwendig, und es ist
nur eine Frage einer einzigen Compiler-Backend-Transformation, um die
ursprünglichen irreduziblen Graphen und optimieren ihn (für Engines, deren Compiler
unterstützen irreduziblen Kontrollfluss – was keiner der 4 Browser tut, um die
nach meinem besten Wissen).

Am besten,
-Ben

Am Do, 20.04.2017 um 5:20 Uhr, Jakob Stoklund Olesen <
[email protected]> schrieb:

Schleifenoptimierungen in LLVM ignorieren im Allgemeinen irreduziblen Kontrollfluss
und nicht versuchen, es zu optimieren. Die Schleifenanalyse, auf der sie basieren, wird
erkennen nur natürliche Schleifen, also muss man sich nur bewusst sein, dass es möglich ist
CFG-Zyklen sein, die nicht als Schleifen erkannt werden. Natürlich andere
Optimierungen sind eher lokaler Natur und funktionieren gut mit irreduziblen
CFGs.

Aus dem Gedächtnis, und wahrscheinlich falsch, hat SPEC2006 eine einzige irreduzible Schleife
in 401.bzip2 und das war's. In der Praxis ist das eher selten.

Clang gibt nur eine einzige indirekte Anweisung in Funktionen aus, die verwenden
berechnet goto. Dies hat den Effekt, dass Gewindeinterpreten in
natürliche Schleifen mit dem indirectbr-Block als Schleifenkopf. Nach dem Verlassen
LLVM IR, das einzelne Indirectbr wird im Codegenerator mit dem Schwanz dupliziert
um das ursprüngliche Gewirr zu rekonstruieren.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/WebAssembly/design/issues/796#issuecomment-295352983 ,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/ALnq1K99AR5YaQuNOIFIckLLSIZbmbd0ks5rxkJQgaJpZM4J3ofA
.

Es gibt etablierte Methoden zur linear-zeitlichen Verifikation des irreduziblen Kontrollflusses. Ein bemerkenswertes Beispiel ist die JVM: Mit Stackmaps verfügt sie über eine Verifikation in linearer Zeit. WebAssembly verfügt bereits über Blocksignaturen für jedes blockartige Konstrukt. Mit expliziten Typinformationen an jedem Punkt, an dem mehrere Kontrollflusspfade zusammenlaufen, ist es nicht erforderlich, Festkommaalgorithmen zu verwenden.

(Nebenbei bemerkt habe ich vor einiger Zeit gefragt, warum man einem hypothetischen pick Operator verbieten sollte, außerhalb seines Blocks in beliebiger Tiefe zu lesen. Dies ist eine Antwort: Es sei denn, Signaturen werden erweitert, um alles zu beschreiben, ein pick könnte lauten, würde eine Typprüfung von pick weitere Informationen erfordern.)

Das Loop-with-a-switch-Muster kann natürlich weggefädelt werden, aber es ist nicht praktisch, sich darauf zu verlassen. Wenn eine Engine sie nicht wegoptimiert, hätte sie einen störenden Overhead. Wenn die meisten Engines es optimieren, ist unklar, was erreicht wird, indem der irreduzible Kontrollfluss aus der Sprache selbst herausgehalten wird.

Seufz... Ich wollte früher antworten, aber das Leben kam dazwischen.

Ich habe mich durch einige JS-Engines gefummelt und muss meine Behauptung über nicht reduzierbaren Kontrollfluss „einfach funktionieren“ schwächen. Ich glaube immer noch nicht, dass es so schwer wäre, es zum Laufen zu bringen, aber es gibt einige Konstrukte, die schwer so angepasst werden könnten, dass sie tatsächlich davon profitieren würden…

Nehmen wir als Argument an, dass es zu schwierig ist, die Optimierungspipeline dazu zu bringen, irreduziblen Kontrollfluss richtig zu unterstützen. Eine JS-Engine kann es immer noch auf hackige Weise wie folgt unterstützen:

Behandeln Sie im Backend einen labels Block bis zur letzten Minute wie eine Schleife+Umschaltung. Mit anderen Worten, wenn Sie einen labels Block sehen, behandeln Sie ihn als Schleifenkopf mit einer äußeren Kante, die auf jedes Label zeigt, und wenn Sie ein branch , das auf ein Label abzielt, erstellen Sie eine Kante, die auf den labels Header zeigt , nicht auf das eigentliche Ziellabel - das irgendwo separat gespeichert werden sollte. Es ist nicht erforderlich, eine tatsächliche Variable zum Speichern des Ziellabels zu erstellen, wie dies bei einer echten Schleife + Schalter der Fall wäre; es sollte ausreichen, den Wert in einem Feld des Verzweigungsbefehls zu speichern oder zu diesem Zweck einen separaten Steuerbefehl zu erstellen. Dann können Optimierungen, Planung und sogar Registerzuordnung so tun, als ob es zwei Sprünge gäbe. Aber wenn es an der Zeit ist, tatsächlich eine native Sprunganweisung zu generieren, überprüfen Sie dieses Feld und generieren einen Sprung direkt zum Ziellabel.

Es kann zB Probleme mit einer Optimierung geben, die Zweige zusammenführt/löscht, aber es sollte ziemlich einfach sein, dies zu vermeiden; die Details hängen von der Motorkonstruktion ab.

In gewisser Weise entspricht mein Vorschlag der „einfachen lokalen Jump-Threading-Optimierung“ von @titzer. Ich schlage vor, den 'nativen' irreduziblen Kontrollfluss wie eine Schleife + Schalter aussehen zu lassen, aber eine Alternative wäre, echte Schleifen + Schalter zu identifizieren - das heißt @titzers "Muster, bei dem eine Zuweisung einer Konstanten an eine lokale Variable erfolgt" eine Verzweigung zu einer bedingten Verzweigung, die diese lokale Variable sofort einschaltet“ – und fügen Sie Metadaten hinzu, die es ermöglichen, die indirekte Verzweigung spät in der Pipeline zu entfernen. Wenn diese Optimierung allgegenwärtig wird, könnte sie ein anständiger Ersatz für eine explizite Anweisung sein.

Wie auch immer, der offensichtliche Nachteil des Hacky-Ansatzes besteht darin, dass Optimierungen den echten Kontrollflussgraphen nicht verstehen; sie verhalten sich effektiv so, als ob jedes Label zu jedem anderen Label springen könnte. Insbesondere muss die Registerzuweisung eine Variable in allen Labels als live behandeln, auch wenn sie beispielsweise immer direkt vor dem Springen zu einem bestimmten Label zugewiesen wird, wie in diesem Pseudocode:

a:
  control = 1;
  goto x;
b:
  control = 2;
  goto x;
...
x:
  // use control

Dies könnte in einigen Fällen zu einer ernsthaft suboptimalen Registernutzung führen. Aber wie ich später anmerken werde, sind die Lebendigkeitsalgorithmen, die JITs verwenden, möglicherweise sowieso nicht in der Lage, dies sowieso nicht gut zu machen…

Wie auch immer, eine späte Optimierung ist viel besser als gar keine Optimierung. Ein einzelner direkter Sprung ist viel schöner als ein Sprung + Vergleich + Laden + indirekter Sprung; der CPU-Zweig-Prädiktor kann eventuell das Ziel des letzteren basierend auf dem vergangenen Zustand vorhersagen, aber nicht so gut wie der Compiler es kann. Und Sie können vermeiden, ein Register und/oder Speicher für die Variable 'aktueller Zustand' auszugeben.

Was die Darstellung betrifft, was ist besser: explizit ( labels Anweisung oder ähnlich) oder implizit (Optimierung von echten Schleifen + Schaltern nach einem bestimmten Muster)?

Implizite Vorteile:

  • Hält die Spezifikation schlank.

  • Funktioniert möglicherweise bereits mit vorhandenem Loop+Switch-Code. Aber ich habe mir das Zeug, das Binaryen erzeugt, nicht angesehen, um zu sehen, ob es einem strengen Muster folgt.

  • Die gesegnete Art und Weise, irreduziblen Kontrollfluss auszudrücken, sich wie ein Hack anfühlt, unterstreicht die Tatsache, dass sie im Allgemeinen langsamer ist und nach Möglichkeit vermieden werden sollte.

Implizite Nachteile:

  • Es fühlt sich an wie ein Hack. Es stimmt, wie @titzer sagt, es benachteiligt Engines nicht wirklich, die irreduziblen Kontrollfluss "richtig" unterstützen; sie können das Muster frühzeitig erkennen und den ursprünglichen irreduziblen Fluss wiederherstellen, bevor sie Optimierungen durchführen. Trotzdem scheint es ordentlicher zu sein, nur die echten Sprünge zuzulassen.

  • Erzeugt eine „Optimierungsklippe“, die WebAssembly im Vergleich zu JS generell vermeiden soll. Um es noch einmal zu sagen, das zu optimierende Grundmuster ist „wo eine Zuweisung einer Konstanten an eine lokale Variable erfolgt, dann eine Verzweigung zu einer bedingten Verzweigung, die diese lokale Variable sofort einschaltet“. Aber was ist, wenn, sagen wir, noch andere Anweisungen dazwischen liegen oder die Zuweisung keine wasm const Anweisung verwendet, sondern nur etwas, das aufgrund von Optimierungen als konstant bekannt ist? Einige Engines sind in Bezug auf das, was sie als dieses Muster erkennen, möglicherweise liberaler als andere, aber Code, der dies (absichtlich oder nicht) nutzt, weist zwischen den Browsern eine sehr unterschiedliche Leistung auf. Durch eine explizitere Codierung werden die Erwartungen deutlicher.

  • Erschwert die Verwendung von Wasm wie eine IR in hypothetischen Nachbearbeitungsschritten. Wenn ein Wasm-Targeting-Compiler die Dinge normal macht und alle Optimierungen/Transformationen mit einem internen IR abwickelt, bevor er schließlich einen Relooper ausführt und schließlich wasm erzeugt, dann würde ihm die Existenz magischer Befehlssequenzen nichts ausmachen. Aber wenn ein Programm irgendwelche Transformationen auf wasm-Code selbst ausführen möchte, müsste es vermeiden, diese Sequenzen aufzubrechen, was ärgerlich wäre.

Jedenfalls ist es mir egal - solange, sollten wir uns für den impliziten Ansatz entscheiden, verpflichten sich die großen Browser tatsächlich, die entsprechende Optimierung durchzuführen.

Um auf die Frage der nativen Unterstützung irreduzibler Strömungen zurückzukommen – was sind die Hindernisse, wie groß der Nutzen ist – hier sind einige spezifische Beispiele von IonMonkey für Optimierungsdurchgänge, die modifiziert werden müssten, um sie zu unterstützen:

AliasAnalysis.cpp: Iteriert über Blöcke in umgekehrter Nachreihenfolge (einmal) und generiert Reihenfolgenabhängigkeiten für eine Anweisung (wie in InstructionReordering verwendet), indem nur zuvor gesehene Stores als möglicherweise Aliasing betrachtet werden. Dies funktioniert nicht für den zyklischen Kontrollfluss. Aber (explizit gekennzeichnete) Schleifen werden speziell behandelt, mit einem zweiten Durchlauf, der Anweisungen in Schleifen gegen spätere Speicherungen irgendwo in derselben Schleife prüft.

-> Also würde es haben einige Schleife Kennzeichnung , für labels Blöcke. In diesem Fall denke ich, dass das Markieren des gesamten labels Blocks als Schleife "einfach funktionieren" würde (ohne die einzelnen Labels speziell zu markieren), da die Analyse zu ungenau ist, um sich um den Kontrollfluss innerhalb der Schleife zu kümmern.

FlowAliasAnalysis.cpp: ein alternativer Algorithmus, der etwas intelligenter ist. Durchläuft auch Blöcke in umgekehrter Postreihenfolge, aber beim Auftreffen auf jeden Block führt es die berechneten Last-Store-Informationen für jeden seiner Vorgänger zusammen (von denen angenommen wird, dass sie bereits berechnet wurden), mit Ausnahme von Schleifenheadern, bei denen die Hinterkante berücksichtigt wird.

-> Messier, weil es davon ausgeht, dass (a) Vorgänger einzelner Basisblöcke immer davor erscheinen, mit Ausnahme von Schleifenhinterkanten, und (b) eine Schleife nur eine Hinterkante haben kann. Es gibt verschiedene Möglichkeiten, dies zu beheben, aber es würde wahrscheinlich eine explizite Behandlung von labels erfordern, und damit der Algorithmus linear bleibt, müsste er in diesem Fall wahrscheinlich ziemlich grob funktionieren, eher wie normale AliasAnalysis - Reduzierung des Nutzens im Vergleich zum Hacky-Ansatz. Ich bin mir nicht sicher, wie schwergewichtige Compiler diese Art der Optimierung handhaben.

BacktrackingAllocator.cpp: Ähnliches Verhalten bei der Registerzuweisung: Es führt einen linearen Rückwärtsdurchlauf durch die Liste von Befehlen durch und nimmt an, dass alle Verwendungen eines Befehls nach seiner Definition erscheinen (dh vor seiner Definition verarbeitet werden), außer wenn Schleifenrückkanten angetroffen werden: Register, die Live am Anfang einer Schleife bleiben Sie einfach während der gesamten Schleife live.

-> Jedes Label müsste wie ein Loop-Header behandelt werden, aber die Lebendigkeit müsste sich über den gesamten Label-Block erstrecken. Nicht schwer zu implementieren, aber auch hier wäre das Ergebnis nicht besser als der Hacky-Ansatz. Ich denke.

@comex Eine weitere Überlegung ist, wie viel Wasm-Motoren leisten sollen. Sie haben zum Beispiel oben die AliasAnalysis von Ion erwähnt, aber die andere Seite der Geschichte ist, dass die Alias-Analyse für WebAssembly-Code nicht so wichtig ist, zumindest im Moment, während die meisten Programme linearen Speicher verwenden.

Der Liveness-Algorithmus BacktrackingAllocator.cpp von Ion würde etwas Arbeit erfordern, aber er wäre nicht unerschwinglich. Der Großteil von Ion verarbeitet bereits verschiedene Formen von irreduziblem Kontrollfluss, da OSR mehrere Einträge in Schleifen erstellen kann.

Eine umfassendere Frage ist hier, welche Optimierungen von WebAssembly-Engines erwartet werden. Wenn man davon ausgeht, dass WebAssembly eine baugruppenähnliche Plattform mit vorhersehbarer Leistung ist, bei der Produzenten/Bibliotheken den größten Teil der Optimierung durchführen, dann wäre ein irreduzibler Kontrollfluss ziemlich kostengünstig, da Engines die großen komplexen Algorithmen nicht benötigen würden, wo sie eine erhebliche Belastung darstellen . Wenn man erwartet, dass WebAssembly ein Bytecode auf höherer Ebene ist, der automatisch mehr Optimierung auf hoher Ebene durchführt, und Engines komplexer sind, dann wird es wertvoller, irreduziblen Kontrollfluss aus der Sprache herauszuhalten, um die zusätzliche Komplexität zu vermeiden.

Übrigens, auch erwähnenswert in dieser Ausgabe ist der On-the-Fly-SSA-Konstruktionsalgorithmus von Braun et al. , der ein einfacher und schneller On-the-Fly-SSA-Konstruktionsalgorithmus ist und irreduziblen Kontrollfluss unterstützt.

Ich bin daran interessiert, WebAssembly als qemu-Backend auf iOS zu verwenden, wo WebKit (und der dynamische Linker, der die Codesignatur überprüft) das einzige Programm ist, das Speicher als ausführbar markieren darf. Qemus Codegen geht davon aus, dass goto-Anweisungen Teil jedes Prozessors sind, für den es codegen muss, was ein WebAssembly-Backend ohne das Hinzufügen von gotos fast unmöglich macht.

@tbodt -

@eholk Das hört sich so an, als wäre es viel viel langsamer als eine direkte Übersetzung von Maschinencode in wasm.

@tbodt Die Verwendung von Binaryen fügt unterwegs eine zusätzliche IR hinzu, ja, aber es sollte nicht viel langsamer sein, denke ich, es ist für die Kompilierungsgeschwindigkeit optimiert. Und es kann auch andere Vorteile als die Handhabung von gotos usw. haben, da Sie optional den Binaryen-Optimierer ausführen können, der möglicherweise Dinge tut, die der qemu-Optimierer nicht tut (wasm-spezifische Dinge).

Eigentlich wäre ich sehr daran interessiert, mit Ihnen zusammenzuarbeiten, wenn Sie möchten :) Ich denke, eine Portierung von Qemu auf wasm wäre sehr nützlich.

Gotos würde also nicht wirklich viel helfen. Der Codegen von Qemu generiert den Code für Basisblöcke, wenn sie zum ersten Mal ausgeführt werden. Wenn ein Block zu einem noch nicht generierten Block springt, generiert er den Block und patcht den vorherigen Block mit einem Goto zum nächsten Block. Dynamisches Laden von Code und Patchen vorhandener Funktionen sind meines Wissens keine Dinge, die in Webassembly durchgeführt werden können.

@kripken Ich wäre an einer Zusammenarbeit interessiert, wo kann man am besten mit dir chatten?

Sie können vorhandene Funktionen nicht direkt patchen, aber Sie können call_indirect und das a WebAssembly.Table zum Jit-Code verwenden. Für jeden nicht generierten Basisblock können Sie JavaScript aufrufen, das WebAssembly-Modul generieren und synchron instanziieren, die exportierte Funktion extrahieren und über den Index in der Tabelle schreiben. Zukünftige Aufrufe verwenden dann Ihre generierte Funktion.

Ich bin mir jedoch nicht sicher, ob dies noch jemand versucht hat, daher wird es wahrscheinlich viele Ecken und Kanten geben.

Das könnte funktionieren, wenn Rückrufe implementiert würden. Sonst würde der Stack ziemlich schnell überlaufen.

Eine weitere Herausforderung wäre die Zuweisung von Speicherplatz in der Standardtabelle. Wie ordnen Sie eine Adresse einem Tabellenindex zu?

Eine andere Möglichkeit besteht darin, die wasm-Funktion bei jedem neuen Basisblock zu regenerieren. Dies bedeutet eine Anzahl von Neukompilierungen, die der Anzahl der verwendeten Blöcke entspricht, aber ich würde wetten, dass dies die einzige Möglichkeit ist, den Code nach der Kompilierung schnell zum Laufen zu bringen (insbesondere innere Schleifen), und er muss nicht vollständig sein recompile, können wir die Binaryen-IR für jeden vorhandenen Block wiederverwenden, IR für den neuen Block hinzufügen und einfach den Relooper für alle ausführen.

(Aber vielleicht können wir qemu dazu bringen, die gesamte Funktion im Voraus zu kompilieren, anstatt träge?)

@tbodt für die Zusammenarbeit mit Binaryen. Eine Möglichkeit besteht darin, ein Repo mit Ihrer Arbeit zu erstellen (und dort Issues verwenden usw.), eine andere besteht darin, ein bestimmtes Issue in Binaryen für qemu zu öffnen.

Wir können qemu nicht dazu bringen, eine ganze Funktion auf einmal zu kompilieren, weil qemu kein Konzept einer "Funktion" hat.

Das erneute Kompilieren des gesamten Cache-Blocks klingt so, als ob es lange dauern könnte. Ich werde herausfinden, wie man den eingebauten Profiler von qemu verwendet, und dann ein Problem auf Binaryen öffnen.

Nebenfrage. Meiner Ansicht nach sollte eine auf WebAssembly ausgerichtete Sprache in der Lage sein, eine effiziente gegenseitig rekursive Funktion bereitzustellen. Für eine Darstellung ihrer Nützlichkeit lade ich Sie ein zu lesen: http://sharp-gamedev.blogspot.com/2011/08/forgotten-control-flow-construct.html

Insbesondere das von Cheery geäußerte Bedürfnis scheint durch eine wechselseitig rekursive Funktion angesprochen zu werden.

Ich verstehe die Notwendigkeit einer Tail-Rekursion, frage mich aber, ob eine gegenseitig rekursive Funktion nur implementiert werden kann, wenn die zugrunde liegende Maschinerie Gotos bietet oder nicht. Wenn dies der Fall ist, ist dies für mich ein legitimes Argument für sie, da es eine Menge Programmiersprachen geben wird, die es sonst schwer haben werden, auf WebAssembly abzuzielen. Wenn dies nicht der Fall ist, wäre vielleicht nur der minimale Mechanismus zur Unterstützung der gegenseitig rekursiven Funktion erforderlich (zusammen mit der Tail-Rekursion).

@davidgrenier , die Funktionen in einem Wasm-Modul sind alle gegenseitig rekursiv. Können Sie erläutern, was Sie an ihnen als ineffizient erachten? Beziehst du dich nur auf das Fehlen von Tail Calls oder etwas anderes?

Allgemeine Rückrufe kommen. Tail-Rekursion (gegenseitig oder anderweitig) wird ein Sonderfall davon sein.

Ich habe nicht gesagt, dass irgendetwas an ihnen ineffizient ist. Ich sage, wenn Sie sie haben, brauchen Sie kein allgemeines Goto, da gegenseitig rekursive Funktionen alles bieten, was ein Sprachimplementierer für WebAssembly benötigen sollte.

Goto ist sehr nützlich für die Codegenerierung aus Diagrammen in der visuellen Programmierung. Vielleicht ist visuelles Programmieren jetzt nicht sehr beliebt, aber in Zukunft kann es mehr Leute erreichen und ich denke, wasm sollte dafür bereit sein. Mehr zur Codegenerierung aus den Diagrammen und Goto: http://drakon-editor.sourceforge.net/generation.html

Die kommende Version von Go 1.11 wird experimentelle Unterstützung für WebAssembly bieten. Dies beinhaltet die volle Unterstützung aller Funktionen von Go, einschließlich Goroutinen, Kanälen usw. Allerdings ist die Leistung der generierten WebAssembly derzeit nicht so gut.

Dies liegt hauptsächlich an der fehlenden goto-Anweisung. Ohne die goto-Anweisung mussten wir in jeder Funktion auf eine Schleife und eine Sprungtabelle auf oberster Ebene zurückgreifen. Die Verwendung des Relooper-Algorithmus ist für uns keine Option, da wir beim Umschalten zwischen Goroutinen in der Lage sein müssen, die Ausführung an verschiedenen Stellen einer Funktion wieder aufzunehmen. Der Relooper kann dabei nicht helfen, sondern nur eine Goto-Anweisung.

Es ist großartig, dass WebAssembly so weit gekommen ist, dass es eine Sprache wie Go unterstützen kann. Aber um wirklich die Assemblierung des Webs zu sein, sollte WebAssembly genauso leistungsfähig sein wie andere Assemblersprachen. Go verfügt über einen fortschrittlichen Compiler, der eine sehr effiziente Assemblierung für eine Reihe anderer Plattformen ausgeben kann. Aus diesem Grund möchte ich argumentieren, dass es hauptsächlich eine Einschränkung von WebAssembly und nicht des Go-Compilers ist, dass es nicht möglich ist, mit diesem Compiler auch effiziente Assemblys für das Web auszugeben.

Die Verwendung des Relooper-Algorithmus ist für uns keine Option, da wir beim Umschalten zwischen Goroutinen in der Lage sein müssen, die Ausführung an verschiedenen Stellen einer Funktion wieder aufzunehmen.

Nur zur Verdeutlichung, ein normales Goto würde dafür nicht ausreichen, ein berechnetes Goto ist für Ihren Anwendungsfall erforderlich, ist das richtig?

Ich denke, ein normales Goto würde wahrscheinlich in Bezug auf die Leistung ausreichen. Sprünge zwischen Basisblöcken sind sowieso statisch und zum Wechseln von Goroutinen sollte ein br_table mit gotos in seinen Verzweigungen performant genug sein. Die Ausgabegröße ist jedoch eine andere Frage.

Es hört sich so an, als ob Sie in jeder Funktion einen normalen Kontrollfluss haben, aber auch die Möglichkeit benötigen, beim Fortsetzen einer Goroutine vom Funktionseintrag zu bestimmten anderen Stellen in der "Mitte" zu springen - wie viele solcher Stellen gibt es? Wenn es sich um jeden einzelnen Basisblock handelt, wäre der Relooper gezwungen, eine Schleife der obersten Ebene auszugeben, die jeder Befehl durchläuft, aber wenn es nur ein paar sind, sollte das kein Problem sein. (Das passiert tatsächlich mit der setjmp-Unterstützung in emscripten - wir erstellen einfach die zusätzlichen notwendigen Pfade zwischen den Basisblöcken von LLVM und lassen das normal vom Relooper verarbeiten.)

Jeder Aufruf einer anderen Funktion ist ein solcher Ort und die meisten Basisblöcke haben mindestens einen Aufrufbefehl. Wir wickeln mehr oder weniger ab und stellen die Aufrufliste wieder her.

Ich verstehe, danke. Ja, ich stimme zu, dass Sie dafür entweder statische Goto- oder Call-Stack-Wiederherstellungsunterstützung benötigen (was auch in Betracht gezogen wurde).

Wird es möglich sein, Funktionen im CPS-Stil aufzurufen oder call/cc in WASM zu implementieren?

@Heimdell , Unterstützung für eine Form von begrenzten Fortsetzungen (auch bekannt als "Stack-Switching") ist auf der Roadmap, die für fast jede interessante Steuerungsabstraktion ausreichen sollte. Wir können jedoch keine unbegrenzten Fortsetzungen (dh vollständiger Aufruf/cc) unterstützen, da der Wasm-Aufrufstapel beliebig mit anderen Sprachen gemischt werden kann, einschließlich wiedereintretender Aufrufe an den Einbetter, und daher nicht als kopierbar oder beweglich angenommen werden kann.

Beim Durchlesen dieses Threads habe ich den Eindruck, dass beliebige Labels und Gotos eine große Hürde haben, bevor sie zu einem Feature werden:

  • Unstrukturierter Kontrollfluss ermöglicht irreduzible Kontrollflussgraphen
  • Eliminierung* jeglicher „schneller, einfacher Verifizierung, einfacher Umwandlung in ein SSA-Formular in einem Durchgang“
  • Öffnen des JIT-Compilers für nichtlineare Leistung
  • Benutzer, die Webseiten durchsuchen, sollten keine Verzögerungen erleiden, wenn der Originalsprachcompiler die Vorarbeit leisten kann

_*obwohl es Alternativen wie den spontanen

Wenn wir immer noch dort stecken bleiben, _und_ Tail-Aufrufe voranschreiten, wäre es vielleicht wert, Sprachcompiler zu bitten, immer noch in gotos zu übersetzen, aber als letzten Schritt vor der WebAssembly-Ausgabe teilen Sie die "Label-Blöcke" in Funktionen auf, und Wandeln Sie die Gotos in Tail Calls um.

Laut Lambda: The Ultimate GOTO von Scheme-Designer Guy Steele aus dem Jahr 1977 sollte die Transformation möglich sein und die Leistung von Tail Calls sollte in der Lage sein, Gotos nahe zu kommen.

Die Gedanken?

Wenn wir immer noch dort stecken bleiben, _und_ Tail-Aufrufe voranschreiten, wäre es vielleicht wert, Sprachcompiler zu bitten, immer noch in gotos zu übersetzen, aber als letzten Schritt vor der WebAssembly-Ausgabe teilen Sie die "Label-Blöcke" in Funktionen auf, und Wandeln Sie die Gotos in Tail Calls um.

Dies ist im Wesentlichen das, was sowieso jeder Compiler tun würde, niemand, den ich kenne, befürwortet nicht verwaltete Gotos, die so viele Probleme in der JVM verursachen, nur für einen Graphen von typisierten EBBs. LLVM, GCC, Cranelift und der Rest haben alle eine (möglicherweise nicht reduzierbare) SSA-Form CFG als ihre interne Repräsentation und die Compiler von Wasm bis Native haben die gleiche interne Repräsentation, daher möchten wir so viele dieser Informationen wie möglich bewahren und so wenig wie möglich von diesen Informationen rekonstruieren. Einheimische sind verlustbehaftet, da sie nicht länger SSA sind, und Wasms Kontrollfluss ist verlustbehaftet, da es keine willkürliche CFG mehr ist. AFAIK mit Wasm eine unendliche Register-SSA-Registermaschine mit eingebetteten feinkörnigen Registerlebensdaten zu haben, wäre wahrscheinlich das Beste für Codegen, aber die Codegröße würde aufblähen, eine Stapelmaschine mit Kontrollfluss, die auf einem beliebigen CFG modelliert ist, ist wahrscheinlich der beste Mittelweg . Ich könnte mich jedoch bei der Codegröße mit einer Registriermaschine irren, es ist möglicherweise möglich, sie effizient zu codieren.

Die Sache mit dem irreduziblen Kontrollfluss ist, dass, wenn er am Front-End irreduzibel ist, er in wasm immer noch irreduzibel ist. Dadurch erhält das Backend weniger Informationen und kann schlechteren Code produzieren. Der einzige Weg, guten Code für irreduzible CFGs zu produzieren, besteht derzeit darin, die von Relooper und Stackifier ausgegebenen Muster zu erkennen und sie wieder in eine irreduzible CFG umzuwandeln. Sofern Sie nicht V8 entwickeln, das AFAIK nur reduzierbaren Kontrollfluss unterstützt, ist die Unterstützung von nicht reduzierbarem Kontrollfluss ein reiner Gewinn - es macht sowohl Frontends als auch Backends viel einfacher (Frontends können Code einfach in dem gleichen Format ausgeben, in dem sie ihn intern speichern, Backends tun es nicht keine Muster erkennen müssen), während eine bessere Ausgabe erzeugt wird, wenn der Kontrollfluss irreduzibel ist, und eine Ausgabe, die genauso gut oder besser ist, wenn der Kontrollfluss normalerweise reduzierbar ist.

Außerdem würde es GCC und Go ermöglichen, mit der Produktion von WebAssembly zu beginnen.

Ich weiß, dass V8 eine wichtige Komponente des WebAssembly-Ökosystems ist, aber es scheint der einzige Teil dieses Ökosystems zu sein, der von der aktuellen Kontrollflusssituation profitiert ob WebAssembly irreduziblen Kontrollfluss darstellen kann oder nicht.

Könnte v8 nicht einfach Relooper integrieren, um Eingabe-CFGs zu akzeptieren? Es scheint, dass große Teile des Ökosystems auf Implementierungsdetails von v8 blockiert sind.

Nur als Referenz habe ich bemerkt, dass switch-Anweisungen in c++ in wasm sehr langsam sind. Wenn ich Code profilierte, musste ich sie in andere Formen konvertieren, die viel schneller funktionierten, um die Bildverarbeitung durchzuführen. Auf anderen Architekturen war das nie ein Problem. Ich würde wirklich gerne aus Leistungsgründen hingehen.

@graph , könnten Sie weitere Details dazu

Ich werde hier posten, da dies für alle Browser gilt. Einfache Anweisungen wie diese, wenn sie mit emscripten kompiliert wurden, waren schneller, wenn ich sie in if-Anweisungen umwandelte.

for(y = ....) {
    for(x = ....) {
        switch(type){
        case IS_RGBA:....
         ....
        case IS_BGRA
        ....
        case IS_RGB
        ....
....

Ich gehe davon aus, dass der Compiler eine Sprungtabelle in alles konvertiert hat, was von wasm unterstützt. Ich habe mir die generierte Assembly nicht angesehen, kann es also nicht bestätigen.

Ich kenne ein paar Dinge, die nichts mit Wasm zu tun haben und die für die Bildverarbeitung im Web optimiert werden können. Ich habe es bereits über die Schaltfläche "Feedback" in Firefox gesendet. Wenn Sie interessiert sind, lassen Sie es mich wissen und ich schicke Ihnen die Probleme per E-Mail.

@graph Ein vollständiger Benchmark wäre hier sehr hilfreich. Im Allgemeinen kann ein Switch in C in wasm zu einer sehr schnellen Sprungtabelle werden, aber es gibt Eckfälle, die noch nicht gut funktionieren und die wir möglicherweise beheben müssen, entweder in LLVM oder in Browsern.

Insbesondere bei emscripten ändert sich die Handhabung von Switches stark zwischen dem alten Fastcomp-Backend und dem neuen Upstream-Backend.

@graph , Wenn emscripten eine br_table erzeugt, generiert das Jit manchmal eine Sprungtabelle und manchmal (wenn es denkt, dass es schneller ist) wird es den Schlüsselraum linear oder mit einer Inline-Binärsuche durchsuchen. Was es tut, hängt oft von der Größe des Schalters ab. Es ist natürlich möglich, dass die Auswahlpolitik nicht optimal ist ... Ich stimme @kripken zu , lauffähiger Code wäre hier sehr hilfreich, wenn Sie etwas teilen möchten.

(Weiß nicht über v8 oder jsc, aber Firefox erkennt derzeit keine Wenn-Dann-Sonst-Kette als möglichen Schalter, daher ist es normalerweise keine gute Idee, Code-Schalter so lange Wenn-Dann-Sonst-Ketten zu öffnen Break-Even-Punkt liegt wahrscheinlich bei nicht mehr als zwei oder drei Vergleichen.)

@lars-t-hansen @kripken @graph es kann gut sein, dass br_table derzeit nur sehr unoptimiert ist, wie dieser Austausch zu zeigen scheint: https://twitter.com/battagline/status/1168310096515883008

@aardappel , das ist seltsam, Benchmarks, die ich gestern ausgeführt habe, zeigten dies nicht, in Firefox auf meinem System lag der Break-Even-Punkt bei etwa 5 Fällen, wie ich mich erinnere, und danach war br_table der Gewinner. Microbenchmark natürlich und mit dem Versuch einer gleichmäßigen Verteilung der Nachschlageschlüssel. Wenn das "if"-Nest auf die wahrscheinlichsten Schlüssel ausgerichtet ist, so dass nicht mehr als ein paar Tests erforderlich sind, wird das "if"-Nest gewinnen.

Wenn die Reichweitenanalyse des Schalterwerts nicht möglich ist, um dies zu vermeiden, muss die br_table auch mindestens einen Filtertest für die Reichweite des Schalters durchführen, was ebenfalls ihren Vorteil ausnutzt.

@lars-t-hansen Ja, wir kennen seinen Testfall nicht, könnte er einen Ausreißerwert haben. So oder so sieht es so aus, als ob Chrome mehr Arbeit zu tun hat als Firefox.

Ich bin im Urlaub, daher fehlende Antworten. Danke für Ihr Verständnis.

@kripken @lars-t-hansen Ich habe einige Tests durchgeführt, es scheint ja, wasm ist jetzt in Firefox besser. Es gibt immer noch Fälle, in denen if-else den Switch übertrifft. Hier ist ein Fall:


Main.cpp

#include <stdio.h>

#include <chrono>
#include <random>

class Chronometer {
public:
    Chronometer() {

    }

    void start() {
        mStart = std::chrono::steady_clock::now();
    }

    double seconds() {
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
    }

private:
    std::chrono::steady_clock::time_point mStart;
};

int main() {
    printf("Starting tests!\n");
    Chronometer timer;
    // we want to prevent optimizations based on known size as most applications
    // do not know the size in advance.
    std::random_device rd;  //Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
    std::uniform_int_distribution<> dis(100000000, 1000000000);
    std::uniform_int_distribution<> opKind(0, 3);
    int maxFrames = dis(gen);
    int switchSelect = 0;
    constexpr int SW1 = 1;
    constexpr int SW2 = 8;
    constexpr int SW3 = 32;
    constexpr int SW4 = 38;

    switch(opKind(gen)) {
    case 0:
        switchSelect = SW1;
        break;
    case 1:
        switchSelect = SW2; break;
    case 2:
        switchSelect = SW3; break;
    case 4:
        switchSelect = SW4; break;
    }
    printf("timing with SW = %d\n", switchSelect);
    timer.start();
    int accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        switch(switchSelect) {
        case SW1:
            accumulator = accumulator*3 + i; break;
        case SW2:
            accumulator = (accumulator < 3)*i; break;
        case SW3:
            accumulator = (accumulator&0xFF)*i + accumulator; break;
        case SW4:
            accumulator = (accumulator*accumulator) - accumulator + i; break;
        }
    }
    printf("switch time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);
    timer.start();
    accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        if(switchSelect == SW1)
            accumulator = accumulator*3 + i;
        else if(switchSelect == SW2)
            accumulator = (accumulator < 3)*i;
        else if(switchSelect == SW3)
            accumulator = (accumulator&0xFF)*i + accumulator;
        else if(switchSelect == SW4)
            accumulator = (accumulator*accumulator) - accumulator + i;
    }
    printf("if-else time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);

    return 0;
}

Abhängig vom Wert von switchSelect. if-else übertrifft. Beispielausgabe:

Starting tests!
timing with SW = 32
switch time = 2.049000 seconds
accumulated value: 0
if-else time = 0.401000 seconds
accumulated value: 0

Wie Sie sehen können, ist für switchSelect = 32 if-else viel schneller. Für die anderen Fälle ist if-else etwas schneller. Für den Fall switchSelect = 1 & 0 ist die switch-Anweisung schneller.

Test in Firefox 69.0.3 (64-bit)
compiled using: emcc -O3 -std=c++17 main.cpp -o main.html
emcc version: emcc (Emscripten gcc/clang-like replacement) 1.39.0 (commit e047fe4c1ecfae6ba471ca43f2f630b79516706b)

Verwendung des neuesten stabilen Emscripen vom 20. Oktober 2019. Neuinstallation ./emcc activate latest .

Ich habe oben bemerkt, dass es einen Tippfehler gibt, aber es sollte sich nicht darauf auswirken, dass das if-else schneller SW3-Fall ist, da sie dieselben Anweisungen ausführen.

Dies wiederum geht über den Break-Even-Punkt von 5 hinaus: Interessant, dass für switchSelect=32 für diesen Fall die Geschwindigkeit ähnlich ist wie bei if-else. Wie Sie für 1003 sehen können, ist if-else etwas schneller. Switch sollte in diesem Fall gewinnen.

Starting tests!
timing with SW = 1003
switch time = 2.253000 seconds
accumulated value: 1903939380
if-else time = 2.197000 seconds
accumulated value: 1903939380


main.cpp

#include <stdio.h>

#include <chrono>
#include <random>

class Chronometer {
public:
    Chronometer() {

    }

    void start() {
        mStart = std::chrono::steady_clock::now();
    }

    double seconds() {
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        return std::chrono::duration_cast<std::chrono::duration<double>>(end - mStart).count();
    }

private:
    std::chrono::steady_clock::time_point mStart;
};

int main() {
    printf("Starting tests!\n");
    Chronometer timer;
    // we want to prevent optimizations based on known size as most applications
    // do not know the size in advance.
    std::random_device rd;  //Will be used to obtain a seed for the random number engine
    std::mt19937 gen(rd()); //Standard mersenne_twister_engine seeded with rd()
    std::uniform_int_distribution<> dis(100000000, 1000000000);
    std::uniform_int_distribution<> opKind(0, 8);
    int maxFrames = dis(gen);
    int switchSelect = 0;
    constexpr int SW1 = 1;
    constexpr int SW2 = 8;
    constexpr int SW3 = 32;
    constexpr int SW4 = 38;
    constexpr int SW5 = 64;
    constexpr int SW6 = 67;
    constexpr int SW7 = 1003;
    constexpr int SW8 = 256;

    switch(opKind(gen)) {
    case 0:
        switchSelect = SW1;
        break;
    case 1:
        switchSelect = SW2; break;
    case 2:
        switchSelect = SW3; break;
    case 3:
        switchSelect = SW4; break;
    case 4:
        switchSelect = SW5; break;
    case 5:
        switchSelect = SW6; break;
    case 6:
        switchSelect = SW7; break;
    case 7:
        switchSelect = SW8; break;
    }
    printf("timing with SW = %d\n", switchSelect);
    timer.start();
    int accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        switch(switchSelect) {
        case SW1:
            accumulator = accumulator*3 + i; break;
        case SW2:
            accumulator = (accumulator < 3)*i; break;
        case SW3:
            accumulator = (accumulator&0xFF)*i + accumulator; break;
        case SW4:
            accumulator = (accumulator*accumulator) - accumulator + i; break;
        case SW5:
            accumulator = (accumulator << 3) - accumulator + i; break;
        case SW6:
            accumulator = (i - accumulator) & 0xFF; break;
        case SW7:
            accumulator = i*i + accumulator; break;
        }
    }
    printf("switch time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);
    timer.start();
    accumulator = 0;
    for(int i = 0; i < maxFrames; ++i) {
        if(switchSelect == SW1)
            accumulator = accumulator*3 + i;
        else if(switchSelect == SW2)
            accumulator = (accumulator < 3)*i;
        else if(switchSelect == SW3)
            accumulator = (accumulator&0xFF)*i + accumulator;
        else if(switchSelect == SW4)
            accumulator = (accumulator*accumulator) - accumulator + i;
        else if(switchSelect == SW5)
            accumulator = (accumulator << 3) - accumulator + i;
        else if(switchSelect == SW6)
            accumulator = (i - accumulator) & 0xFF;
        else if(switchSelect == SW7)
            accumulator = i*i + accumulator;

    }
    printf("if-else time = %lf seconds\n", timer.seconds());
    printf("accumulated value: %d\n", accumulator);

    return 0;
}


Vielen Dank, dass Sie sich diese Testfälle angesehen haben.

Das ist zwar ein sehr spärliches switch , das LLVM sowieso in das Äquivalent eines Satzes von Wenn-Dann-Werten umwandeln sollte, aber anscheinend tut es dies auf eine Weise, die weniger effizient ist als das manuelle Wenn-Dann. Haben Sie versucht, wasm2wat auszuführen, um zu sehen, wie sich diese beiden Schleifen im Code unterscheiden?

Dies hängt auch stark von diesem Test ab, der bei jeder Iteration denselben Wert verwendet. Dieser Test wäre besser, wenn er alle Werte durchlaufen würde, oder noch besser, zufällig aus ihnen ausgewählt (wenn dies kostengünstig möglich ist).

Besser noch, der wahre Grund, warum Leute Switch für die Leistung verwenden, liegt in einem dichten Bereich, sodass Sie garantieren können, dass darunter tatsächlich br_table wird. Zu sehen, in wie vielen Fällen br_table schneller ist als if wäre das Nützlichste.

Der Schalter in den engen Schleifen wurde verwendet, weil es saubererer Code über Leistung war. Aber für wasm war die Auswirkung auf die Leistung zu groß, sodass sie in hässlichere if-Anweisungen umgewandelt wurde. Für die Bildverarbeitung in vielen meiner Anwendungsfälle, wenn ich mehr Leistung aus einem Switch herausholen möchte, würde ich den Switch außerhalb der Schleife verschieben und für jeden Fall einfach Kopien der Schleife haben. Normalerweise schaltet der Wechsel nur zwischen Pixelformat, Farbformat, Kodierung usw. um... Und in vielen Fällen werden die Konstanten über Defines oder Enums berechnet und nicht linear. Ich sehe jetzt, dass mein Problem nicht mit dem Goto-Design zusammenhängt. Ich hatte nur ein unvollständiges Verständnis darüber, was für meine Switch-Anweisungen geschah. Ich hoffe, dass meine Notizen für Browser-Entwickler nützlich sind, die dies lesen, um wasm in diesen Fällen für die Bildverarbeitung zu optimieren. Danke schön.

Ich hätte nie gedacht, dass Goto so eine hitzige Debatte sein kann 😮 . Ich bin im Boot jeder Sprache sollte ein Goto haben 😁 . Ein weiterer Grund, goto hinzuzufügen, besteht darin, dass es die Komplexität für den Compiler verringert, nach wasm zu kompilieren. Ich bin mir ziemlich sicher, dass das oben irgendwo erwähnt wurde. Jetzt habe ich nichts zu beanstanden 😞 .

Gibt es da weitere Fortschritte?

Aufgrund der hitzigen Debatte würde ich annehmen, dass einige Browser Goto als nicht standardmäßige Bytecode-Erweiterung unterstützen würden. Dann kann GCC vielleicht in das Spiel einsteigen, da es eine nicht standardmäßige Version unterstützt. Was meiner Meinung nach insgesamt nicht gut ist, aber mehr Compiler-Konkurrenz ermöglicht. Wurde dies berücksichtigt?

In letzter Zeit gab es keine großen Fortschritte, aber vielleicht möchten Sie sich den Vorschlag für Funklets ansehen .

@graph für mich klingt dein Vorschlag wie "Lass uns alles brechen und auf das Bessere hoffen".
So funktioniert es nicht. Es gibt VIELE Vorteile aus der aktuellen WebAssembly-Struktur (die leider nicht offensichtlich sind). Versuchen Sie, tiefer in die Philosophie des Wasm einzutauchen.

Das Zulassen von "beliebigen Labels und Gotos" bringt uns zurück in die (alten) Zeiten des nicht überprüfbaren Bytecodes. Alle Compiler werden einfach zu einer "faulen Art" wechseln, anstatt "es richtig zu machen".

Es ist klar, dass wasm in seinem aktuellen Zustand einige wesentliche Auslassungen aufweist. Die Leute arbeiten daran, die Lücken zu füllen (wie die von @binji erwähnte), aber ich denke nicht, dass die "globale Wasm-Struktur" überarbeitet werden muss. Nur meine bescheidene Meinung.

@vshymanskyy Der Funclets-Vorschlag, der eine Funktionalität bietet, die beliebigen Labels und Gotos entspricht, ist in linearer Zeit vollständig validierbar.

Ich sollte auch erwähnen, dass wir in unserem Wasm-Compiler mit linearer Zeit alle Wasm-Kontrollflüsse intern in eine funclets-ähnliche Darstellung kompilieren, worüber ich in diesem Blockbeitrag einige Informationen habe und die Umwandlung von Wasm-Kontrollfluss in diese interne Darstellung implementiert ist hier . Der Compiler erhält alle seine Typinformationen aus dieser funclets-ähnlichen Darstellung, es genügt also zu sagen, dass es trivial ist, seine Typsicherheit in linearer Zeit zu validieren.

Ich denke, dass dieses Missverständnis, dass irreduzibler Kontrollfluss nicht in linearer Zeit validiert werden kann, von der JVM stammt, wo irreduzibler Kontrollfluss mit dem Interpreter ausgeführt werden muss, anstatt kompiliert zu werden. Dies liegt daran, dass die JVM keine Möglichkeit hat, Typmetadaten für einen irreduziblen Kontrollfluss darzustellen und daher die Konvertierung von einer Stack-Maschine in eine Registrierungsmaschine nicht durchführen kann. "Beliebige Gotos" (dh Sprung zu Byte/Befehl X) ist überhaupt nicht nachprüfbar, aber eine Funktion in typisierte Blöcke aufzuteilen, zwischen denen dann in beliebiger Reihenfolge gesprungen werden kann, ist nicht schwieriger zu überprüfen, als ein Modul in typisierte Funktionen aufzuteilen , zwischen denen dann in beliebiger Reihenfolge gesprungen werden kann. Sie benötigen keine untypisierten Gotos im Jump-to-Byte-X-Stil, um nützliche Muster zu implementieren, die von Compilern wie GCC und LLVM ausgegeben würden.

Ich liebe den Prozess hier einfach. Seite A erklärt, warum dies in bestimmten Anwendungen erforderlich ist. Seite B sagt, dass sie es falsch machen, bietet aber keine Unterstützung für diese Anwendung. Seite A erklärt, dass keines der pragmatischen Argumente von B stichhaltig ist. Seite B will damit nicht umgehen, weil sie denken, dass Seite A es falsch macht. Seite A versucht, ein Ziel zu erreichen. Seite B sagt, das sei das falsche Ziel und nennt es faul oder brutal. Die tieferen philosophischen Bedeutungen gehen auf Seite A verloren. Die pragmatischen gehen auf Seite B verloren, da sie behaupten, eine höhere moralische Grundlage zu haben. Seite A sieht darin eine amoralische mechanistische Operation. Letztendlich behält Seite B im Allgemeinen die Kontrolle über die Spezifikation, zum Guten oder zum Schlechten, und sie haben unglaublich viel mit ihrer relativen Reinheit erreicht.

Ehrlich gesagt habe ich hier nur meine Nase reingesteckt, weil ich vor Jahren versucht habe, eine TinyCC-Portierung auf WASM zu erstellen, damit ich eine Entwicklungsumgebung auf einem ESP8266 ausführen kann, die auf den ESP8266 abzielt. Ich habe nur ~ 4 MB Speicherplatz, also kommt ein Re-Looper und ein Wechsel zu einem AST sowie viele andere Änderungen nicht in Frage. (Nebenbemerkung: Wie ist Relooper das Einzige wie Relooper? Es ist sooo schrecklich und niemand hat diesen Trottel in In C umgeschrieben!?) Selbst wenn es an dieser Stelle möglich wäre, weiß ich nicht, ob ich ein TinyCC-Ziel schreiben würde zu WASM, da es für mich einfach nicht mehr so ​​interessant ist.

Allerdings dieser Thread. Heilige Kuh, dieser Thread hat mir so viel existentielle Freude gebracht. Zu sehen, wie eine Verzweigung der Menschheit tiefer verläuft als Demokraten oder Republikaner oder Religionen. Ich habe das Gefühl, wenn das jemals gelöst werden kann. Wenn A in der Welt von B leben kann oder B die Behauptung von A bestätigt, dass prozedurale Programmierung ihren Platz hat ... Ich denke, wir könnten den Weltfrieden lösen.

Könnte jemand, der für V8 zuständig ist, in diesem Thread bestätigen, dass der Widerstand gegen irreduziblen Kontrollfluss nicht durch die aktuelle Implementierung von V8 beeinflusst wird?

Ich frage, weil mich das am meisten stört. Für mich scheint dies eine Diskussion auf Spezifikationsebene über die Vor- und Nachteile dieser Funktion zu sein. Sie sollte in keiner Weise davon beeinflusst werden, wie eine bestimmte Implementierung derzeit konzipiert ist. Es gab jedoch Aussagen, die mich glauben machen, dass die Implementierung von V8 dies beeinflusst. Vielleicht liege ich falsch. Eine offene Aussage könnte helfen.

Nun, so sehr es bedauerlich ist, die bisherigen Implementierungen sind so wichtig, dass die Zukunft (vermutlich länger als die Vergangenheit) nicht so wichtig ist. Ich habe versucht, in # 1202 zu erklären, dass die Konsistenz wichtiger ist als die wenigen Implementierungen, aber es scheint, als wäre ich wahnhaft. Viel Glück bei der Erklärung, dass einige Entwicklungsentscheidungen irgendwo in einem Projekt keine universelle Wahrheit darstellen oder standardmäßig als richtig angenommen werden müssen.

Dieser Thread ist ein Kanarienvogel in der W3C-Kohlemine. Obwohl ich großen Respekt vor vielen W3C-Personen habe, wurde die Entscheidung, JavaScript Ecma International und nicht dem W3C anzuvertrauen, nicht ohne Vorurteile getroffen.

Wie @cnlohr hatte ich Hoffnungen auf einen TCC-Wasm-Port, und das aus gutem Grund;

"Wasm ist als portables Kompilierungsziel für Programmiersprachen konzipiert und ermöglicht die Bereitstellung im Web für Client- und Serveranwendungen." - webassembly.org

Sicher, jeder kann erklären, warum goto [JARGON EINFÜGEN] ist, aber wie wäre es, wenn wir Standards den Meinungen vorziehen. Wir sind uns alle einig, dass POSIX C ein gutes Basisziel ist, insbesondere angesichts der Tatsache, dass die heutigen Langs entweder aus C erstellt oder mit C verglichen werden und die Überschrift der WASM-Homepage sich selbst als tragbares Kompilierungsziel für Langs anpreist. Natürlich werden einige Features wie Threads und simd mit Roadmaps versehen. Aber etwas so Grundlegendes wie goto völlig zu ignorieren, ihm nicht einmal den Anstand eines Roadmappings zu geben, entspricht nicht dem erklärten Zweck der WASM und einer solchen Haltung des Standardisierungsgremiums, die <marquee> green grünes Licht gegeben hat ist jenseits des bleichen.

Gemäß SEI CERT C Coding Standard Rec. " Erwägen Sie die Verwendung einer Goto-Kette, wenn Sie eine Funktion bei einem Fehler verlassen, wenn Sie Ressourcen verwenden und freigeben" ;

Viele Funktionen erfordern die Zuweisung mehrerer Ressourcen. Wenn diese Funktion fehlschlägt und irgendwo in der Mitte zurückkehrt, ohne alle zugewiesenen Ressourcen freizugeben, kann dies zu einem Speicherverlust führen. Es ist ein häufiger Fehler, zu vergessen, eine (oder alle) Ressourcen auf diese Weise freizugeben. Daher ist eine Goto-Kette die einfachste und sauberste Methode, um Exits zu organisieren und gleichzeitig die Reihenfolge der freigegebenen Ressourcen beizubehalten.

Die Empfehlung bietet dann ein Beispiel mit der bevorzugten POSIX C-Lösung mit goto . Die Neinsager werden auf den Hinweis hinweisen, dass goto immer noch als schädlich angesehen wird . Interessanterweise ist diese Meinung nicht in einem dieser speziellen Codierungsstandards enthalten, sondern nur als Anmerkung. Was uns zum Kanarienvogel bringt, der "als schädlich gilt".

Unterm Strich sollte eine Betrachtung von "CSS-Regionen" oder goto nur zusammen mit einem Lösungsvorschlag für das Problem abgewogen werden, für das eine solche Funktion verwendet wird. Wenn das Entfernen dieser "schädlichen" Funktion darauf hinausläuft, die vernünftigen Anwendungsfälle ohne Alternative zu entfernen, ist dies keine Lösung, sondern schädlich für die Benutzer der Sprache.

Funktionen sind auch in C nicht kostenlos. Wenn jemand einen Ersatz für gotos & labels anbietet, bitte Canihaz! Wenn jemand sagt, ich brauche es nicht, woher weiß er das? Wenn es um Leistung geht, kann goto uns das kleine Extra geben, das Ingenieuren schwer zu argumentieren ist, dass wir keine leistungsstarken, einfach zu bedienenden Funktionen brauchen, die es seit Anbeginn der Sprache gibt.

Ohne einen Plan, goto , ist WASM ein Spielzeug-Kompilierungsziel, und das ist in Ordnung, vielleicht sieht das W3C das Web so. Ich hoffe, dass WASM als Standard höher hinauskommt, aus dem 32-Bit-Adressraum herauskommt und in das Kompilierungsrennen einsteigt. Ich hoffe, dass der technische Diskurs von "das ist nicht möglich..." wegkommen kann, um GCC-C-Erweiterungen wie Labels as Values schnell zu verfolgen, weil WASM FANTASTISCH sein sollte. Persönlich ist TCC an dieser Stelle wesentlich beeindruckender und nützlicher, ohne all das verschwendete Pontifikat, ohne die Hipster-Landingpage und das glänzende Logo.

@d4tocchini :

Gemäß SEI CERT C Coding Standard Rec. " Erwägen Sie die Verwendung einer Goto-Kette, wenn Sie eine Funktion bei einem Fehler verlassen, wenn Sie Ressourcen verwenden und freigeben" ;

Viele Funktionen erfordern die Zuweisung mehrerer Ressourcen. Wenn diese Funktion fehlschlägt und irgendwo in der Mitte zurückkehrt, ohne alle zugewiesenen Ressourcen freizugeben, kann dies zu einem Speicherverlust führen. Es ist ein häufiger Fehler, zu vergessen, eine (oder alle) Ressourcen auf diese Weise freizugeben. Daher ist eine Goto-Kette die einfachste und sauberste Methode, um Exits zu organisieren und gleichzeitig die Reihenfolge der freigegebenen Ressourcen beizubehalten.

Die Empfehlung bietet dann ein Beispiel mit der bevorzugten POSIX C-Lösung mit goto . Die Neinsager werden auf den Hinweis hinweisen, dass goto immer noch als schädlich angesehen wird . Interessanterweise ist diese Meinung nicht in einem dieser speziellen Codierungsstandards enthalten, sondern nur als Anmerkung. Was uns zum Kanarienvogel bringt, der "als schädlich gilt".

Das in dieser Empfehlung angegebene Beispiel kann direkt mit gekennzeichneten Pausen ausgedrückt werden, die in Wasm verfügbar sind. Es braucht nicht die zusätzliche Macht von willkürlichem goto. (C bietet kein gekennzeichnetes Break and Continue, muss also öfter als nötig auf goto zurückgreifen.)

@rossberg , guter Punkt zu gekennzeichneten Brüchen in diesem Beispiel, aber ich stimme Ihrer qualitativen Annahme nicht zu, dass C "zurückfallen" muss. goto ist ein umfangreicheres Konstrukt als beschriftete Umbrüche. Wenn C zu den portablen Kompilierungszielen gehören soll und C keine beschrifteten Umbrüche unterstützt, ist dies eher ein Mute-Punkt. Java hat Breaks/Continues gekennzeichnet, während Python die vorgeschlagene Funktion abgelehnt hat und wenn man bedenkt, dass sowohl die Sun-JVM als auch der Standard-CPython in C geschrieben sind, würden Sie nicht zustimmen, dass C als unterstützte Sprache höher auf der Prioritätenliste stehen sollte?

Wenn goto so leicht aus der Betrachtung gestrichen werden soll, sollten dann auch die

Gibt es eine Sprache, die nicht in C geschrieben werden kann? C als Sprache sollte die Funktionen von WASM informieren. Wenn POSIX C mit dem heutigen WASM nicht möglich ist, dann gibt es Ihre richtige Roadmap.

Nicht wirklich zum Thema der Argumentation, aber um nicht zu verschleiern, dass die zufälligen Fehler hier und da in der Argumentation im Allgemeinen lauern:

Python hat gekennzeichnete Pausen

Können Sie das näher erläutern? (Aka: Python hat keine gekennzeichneten Umbrüche.)

Wenn goto so leicht aus der Betrachtung gestrichen werden soll, sollten dann auch die Hunderte von Verwendungen von goto in der Quelle von emscripten überdacht werden?

1) Beachten Sie, wie viel davon in musl libc vorhanden ist, nicht direkt in emscripten. (Am zweithäufigsten verwendet ist Tests/Third_Party)
2) Konstrukte auf Quellebene sind nicht dasselbe wie Bytecode-Anweisungen
3) Emscripten hat nicht das gleiche Abstraktionsniveau wie der Wasm-Standard, daher sollte es auf dieser Grundlage nicht überdacht werden.

Insbesondere könnte es heute nützlich sein, die gotos aus der libc heraus neu zu schreiben, da wir dann mehr Kontrolle über die resultierende cfg haben, als relooper/cfgstackify zu vertrauen, dass sie gut damit umgehen. Wir nicht, weil es eine nicht triviale Menge an Arbeit ist, mit wild abweichendem Code von Upstream-Musl fertig zu werden.

Emscripten-Entwickler (die ich zuletzt überprüft habe) neigen aus diesen offensichtlichen Gründen zu der Meinung, dass eine Goto-ähnliche Struktur wirklich schön wäre, also werden sie sie wahrscheinlich nicht außer Betracht lassen, selbst wenn es Jahre dauert, einen akzeptablen Kompromiss zu finden.

eine solche Haltung des Standardisierungsgremiums, dass <marquee> grünes Licht gegeben hat, ist unvorstellbar.

Dies ist eine besonders alberne Aussage.

1) Wir-das-breitere-Internet sind über ein Jahrzehnt davon entfernt, diese Entscheidung getroffen zu haben
2) Wir-the-wasm-CG sind eine völlig (fast?) getrennte Gruppe von Leuten von diesem Tag und sind wahrscheinlich auch einzeln über offensichtliche Fehler der Vergangenheit verärgert.

ohne all das verschwendete Pontifikat, ohne die Hipster-Landingpage und das glänzende Logo.

Dies hätte in "Ich bin frustriert" umformuliert werden können, ohne auf Tonprobleme zu stoßen.

Wie dieser Thread zeigt, sind diese Gespräche ohnehin schon schwierig genug.

Es gibt eine neue Ebene der Besorgnis, wenn Sie einen zutiefst vertrauten und verstandenen Satz von Funktionen für alle neu schreiben möchten, nur weil eine Umgebung für deren Verwendung zusätzliche Schritte durchlaufen muss, um sie zu unterstützen. (obwohl ich immer noch im festen Please-Add-Goto-Lager bin, weil ich es hasse, nur einen bestimmten Compiler zu verwenden)

Ich denke, dieser Thread ist weit davon entfernt, produktiv zu sein - er läuft jetzt seit über vier Jahren und es sieht so aus, als ob hier jedes mögliche Argument für und gegen beliebige goto s verwendet wurde; Es sollte auch beachtet werden, dass keines dieser Argumente besonders neu ist ;)

Es gibt verwaltete Laufzeiten, die sich dafür entschieden haben, keine willkürlichen Sprungmarken zu haben, was für sie gut funktioniert hat. Es gibt auch Programmiersysteme, bei denen beliebige Sprünge erlaubt sind und die auch gut laufen. Am Ende treffen die Autoren eines Programmiersystems Designentscheidungen und nur die Zeit zeigt wirklich, ob diese Entscheidungen erfolgreich sind oder nicht.

Designentscheidungen von Wasm, die willkürliche Sprünge verbieten, sind Kern seiner Philosophie. Es ist unwahrscheinlich, dass es goto s ohne so etwas wie Funclets unterstützen kann, aus den gleichen Gründen unterstützt es keine reinen indirekten Sprünge.

Designentscheidungen von Wasm, die willkürliche Sprünge verbieten, sind Kern seiner Philosophie. Es ist unwahrscheinlich, dass es Gotos ohne so etwas wie Funklets unterstützen kann, aus den gleichen Gründen unterstützt es keine reinen indirekten Sprünge.

@penzn Warum bleibt der Funclets-Vorschlag hängen ? Es existiert seit Oktober 2018 und befindet sich noch in Phase 0.

Wenn wir über ein gewöhnliches Open-Source-Projekt diskutieren würden, würde ich es abspalten und fertig sein. Wir sprechen hier von einem weitreichenden Monopolstandard. Eine energische Reaktion der Gemeinschaft sollte kultiviert werden, weil wir uns darum kümmern.

@J0eCool

  1. Beachten Sie, wie viel davon in musl libc vorhanden ist, nicht direkt in emscripten. (Am zweithäufigsten verwendet ist Tests/Third_Party)

Ja, die Anspielung war, wie oft es in C im Allgemeinen verwendet wird.

  1. Konstrukte auf Quellebene sind nicht dasselbe wie Bytecode-Anweisungen

Was wir hier besprechen, ist natürlich ein internes Anliegen, das sich auf Konstrukte auf Quellebene auswirkt. Das ist ein Teil der Frustration, die Blackbox sollte ihre Bedenken nicht durchsickern lassen.

  1. Emscripten hat nicht das gleiche Abstraktionsniveau wie der Wasm-Standard, daher sollte es auf dieser Grundlage nicht überdacht werden.

Der Punkt war, dass Sie goto s in den meisten großen C-Projekten finden, sogar innerhalb der WebAssembly-Toolchain insgesamt. Ein portables Compiler-Ziel für Sprachen im Allgemeinen, das nicht ausdrucksstark genug ist, um auf seine eigenen Compiler abzuzielen, entspricht nicht genau der Natur unseres Unternehmens.

Insbesondere könnte es heute nützlich sein, die gotos aus der libc heraus neu zu schreiben, da wir dann mehr Kontrolle über die resultierende cfg haben, als relooper/cfgstackify zu vertrauen, dass sie gut damit umgehen.

Das ist zirkulär. Viele der oben genannten haben ernsthafte unbeantwortete Fragen bezüglich der Unfehlbarkeit einer solchen Anforderung aufgeworfen.

Wir nicht, weil es eine nicht triviale Menge an Arbeit ist, mit wild abweichendem Code von Upstream-Musl fertig zu werden.

Es ist möglich, Gotos zu entfernen, wie Sie sagten, es ist nicht trivial ! Schlagen Sie vor, dass alle anderen Codepfade wild voneinander abweichen sollten, weil Gotos nicht unterstützt werden sollte?

Emscripten-Entwickler (die ich zuletzt überprüft habe) neigen aus diesen offensichtlichen Gründen zu der Meinung, dass eine Goto-ähnliche Struktur wirklich schön wäre, also werden sie sie wahrscheinlich nicht außer Betracht lassen, selbst wenn es Jahre dauert, einen akzeptablen Kompromiss zu finden.

Ein Hoffnungsschimmer! Ich wäre zufrieden, wenn der goto/label-Support ernst genommen würde mit einem Roadmap-Item + einer offiziellen Einladung, den Ball in Bewegung zu bringen, auch wenn es Jahre dauert.

Dies ist eine besonders alberne Aussage.

Du hast recht. Verzeihen Sie die Übertreibung, ich bin ein bisschen frustriert. Ich liebe wasm und benutze es oft, aber letztendlich sehe ich eine Straße des Schmerzes vor mir, wenn ich etwas Bemerkenswertes damit machen möchte, wie Port TCC. Nachdem ich alle Kommentare und Artikel gelesen habe, kann ich immer noch nicht herausfinden, ob die Opposition technisch, philosophisch oder politisch ist. Wie @neelance ausgedrückt hat,

„Könnte jemand, der für V8 zuständig ist, in diesem Thread bestätigen, dass der Widerstand gegen irreduziblen Kontrollfluss nicht durch die aktuelle Implementierung von V8 beeinflusst wird?

Ich frage, weil mich das am meisten stört. […]

Wenn ihr etwas von Nutzen hört, nehmt euch @neelances Feedback zu Go 1.11 zu Herzen. Das ist schwer zu argumentieren. Sicher, wir können alle das nicht triviale Abstauben von Goto tun, aber selbst dann nehmen wir einen ernsthaften Perf-Hit hin, der nur mit einer Goto-Anweisung behoben werden kann.

Nochmals, verzeihen Sie meine Frustration, aber wenn dieses Problem ohne die richtige Adresse geschlossen wird, fürchte ich, dass es ein falsches Signal sendet, das diese Art von Community-Antworten nur verärgern wird und für eine unserer größten Standardisierungsbemühungen unangemessen ist Bereich. Es versteht sich von selbst, dass ich ein großer Fan und Unterstützer aller Mitglieder dieses Teams bin. Danke schön!

Hier ist ein weiteres reales Problem, das durch fehlende goto/funclets verursacht wird: https://github.com/golang/go/issues/42979

Für dieses Programm generiert der Go-Compiler derzeit eine Wasm-Binärdatei mit 18.000 verschachtelten block s. Die Wasm-Binärdatei selbst hat eine Größe von 2,7 MB, aber wenn ich sie durch wasm2wat ausführe, bekomme ich eine 4,7 GB .wat-Datei. 🤯.

Ich könnte versuchen, dem Go-Compiler eine Heuristik zu geben, damit er anstelle einer einzigen riesigen Sprungtabelle eine Art Binärbaum erstellen und dann die Sprungzielvariable mehrmals betrachten kann. Aber ist das mit wasm wirklich so?

Ich möchte hinzufügen, dass ich es seltsam finde, dass die Leute denken, dass es völlig in Ordnung ist, wenn nur ein einzelner Compiler (Emscripten[1]) WebAssembly realistisch unterstützen kann.
Erinnert mich etwas an die Libopus-Situation (ein Standard, der normativ von urheberrechtlich geschütztem Code abhängt).

Ich finde es auch seltsam, wie WebAssembly-Entwickler so vehement dagegen sind, obwohl fast jeder vom Compiler-Ende ihnen sagt, dass es erforderlich ist. Denken Sie daran: WebAssembly ist ein Standard, kein Manifest. Und Tatsache ist, dass die meisten modernen Compiler intern irgendeine Form von SSA + Basisblöcken verwenden (oder etwas fast Äquivalentes mit denselben Eigenschaften), die kein Konzept von expliziten Schleifen haben[2]. Sogar JITs verwenden etwas Ähnliches, so häufig ist es.
Die absolute Voraussetzung dafür, dass ein Relooping ohne Fluchtluke von "Just use goto" erfolgen kann, ist meines Wissens[3] außerhalb von Sprache-zu-Sprache-Übersetzern beispiellos --- und selbst dann nur Sprache-zu-Sprache-Übersetzer, die Zielsprachen weniger. Insbesondere habe ich noch nie davon gehört, dass dies für irgendeine Art von IR oder Bytecode außer WebAssembly getan werden muss.

Vielleicht ist es an der Zeit, WebAssembly in WebEmscripten (WebScripten?) umzubenennen.

Wie @d4tocchini sagte, wäre es ohne den (aufgrund der Standardisierungssituation notwendigen) monopolistischen Status von WebAssembly inzwischen wahrscheinlich in etwas gespalten worden, das das unterstützen kann, was die Compiler-Entwickler bereits wissen, was es unterstützen muss.
Und nein, "nur emscripten verwenden" ist kein gültiges Gegenargument, weil es den Standard von einem einzigen Compiler-Hersteller abhängig macht. Ich hoffe, ich muss Ihnen nicht sagen, warum das schlecht ist.

EDIT: Eines habe ich vergessen hinzuzufügen:
Sie haben immer noch nicht geklärt, ob es sich um ein technisches, philosophisches oder politisches Problem handelt. Ich vermute letzteres, würde aber gerne das Gegenteil bewiesen (weil technische und philosophische Probleme viel einfacher zu lösen sind als politische).

Hier ist ein weiteres reales Problem, das durch fehlende goto/funclets verursacht wird: golang/go#42979

Für dieses Programm generiert der Go-Compiler derzeit eine Wasm-Binärdatei mit 18.000 verschachtelten block s. Die Wasm-Binärdatei selbst hat eine Größe von 2,7 MB, aber wenn ich sie durch wasm2wat ausführe, bekomme ich eine 4,7 GB .wat-Datei. 🤯.

Ich könnte versuchen, dem Go-Compiler eine Heuristik zu geben, damit er anstelle einer einzigen riesigen Sprungtabelle eine Art Binärbaum erstellen und dann die Sprungzielvariable mehrmals betrachten kann. Aber ist das mit wasm wirklich so?

Dieses Beispiel ist wirklich interessant. Wie erzeugt ein so einfaches Programm mit gerader Linie diesen Code? Welche Beziehung besteht zwischen der Anzahl der Array-Elemente und der Anzahl der Blöcke? Sollte ich dies insbesondere so interpretieren, dass jeder Zugriff auf ein Array-Element erfordert, dass _mehrere_ Blöcke originalgetreu kompiliert werden?

Und nein, "nur Emscripten verwenden" ist kein gültiges Gegenargument

Ich denke, das eigentliche Gegenargument in dieser Richtung wäre, dass ein anderer Compiler, der Wasm ins Visier nehmen will, seinen eigenen Relooper-ähnlichen Algorithmus implementieren kann / muss. Persönlich denke ich, dass Wasm irgendwann eine Mehrkörperschleife (in der Nähe von Funklets) oder etwas Ähnliches haben sollte, das ein natürliches Ziel für goto .

@conrad-watt Es gibt mehrere Faktoren, die dazu führen, dass jede Aufgabe mehrere Basisblöcke in der CFG verwendet. Einer davon ist, dass der Slice eine Längenprüfung durchführt, da die Länge zur Kompilierzeit nicht bekannt ist. Generell würde ich sagen, dass Compiler Basic-Blöcke als relativ billiges Konstrukt betrachten, aber bei wasm sind sie etwas teuer, besonders in diesem speziellen Fall.

@neelance Im modifizierten Beispiel, in dem der Code auf mehrere Funktionen aufgeteilt ist, ist der (Laufzeit-/Kompilierungs-) Speicheraufwand viel geringer. Werden in diesem Fall weniger Blöcke generiert oder kann die Engine-GC aufgrund der separaten Funktionen nur granularer sein?

@conrad-watt Es ist nicht einmal der Go-Code, der den Speicher verwendet, sondern der WebAssembly-Host: Wenn ich die Wasm-Binärdatei mit Chrome 86 instanziiere, geht meine CPU für 2 Minuten auf 100% und die Speichernutzung der Registerkarte erreicht ihren Höchststand bei 11,3 GB. Dies ist, bevor der Wasm Binary / Go-Code ausgeführt wird. Es ist die Form der Wasm-Binärdatei, die das Problem verursacht.

Das war schon mein Verständnis. Ich würde erwarten, dass eine große Anzahl von Blöcken / Typanmerkungen speziell während der Kompilierung / Instanziierung Speicheraufwand verursacht.

Um zu versuchen, meine vorherige Frage zu entschlüsseln - wenn die geteilte Version des Codes mit weniger Blöcken in Wasm kompiliert wird (wegen einer Relooper-Eigenart), wäre dies eine Erklärung für den geringeren Speicheraufwand und eine gute Motivation, allgemeineres hinzuzufügen Kontrollfluss zu Wasm.

Alternativ kann es sein, dass der geteilte Code zu (ungefähr) der gleichen Gesamtanzahl von Blöcken führt, aber da jede Funktion separat JIT-kompiliert wird, können die Metadaten/IR, die zum Kompilieren jeder Funktion verwendet werden, von der Wasm-Engine eifriger mit GC bearbeitet werden . Ein ähnliches Problem trat in V8 vor Jahren beim Parsen/Kompilieren großer asm.js-Funktionen auf. In diesem Fall würde die Einführung eines allgemeineren Kontrollflusses in Wasm das Problem nicht lösen.

Zuerst möchte ich klarstellen: Der Go-Compiler verwendet nicht den Relooper-Algorithmus, da er von Natur aus mit dem Konzept des Umschaltens von Goroutinen nicht kompatibel ist. Alle Basisblöcke werden über eine Sprungtabelle mit ein wenig Fall-Through, wo möglich, ausgedrückt.

Ich denke, es gibt ein exponentielles Komplexitätswachstum in der Wasm-Laufzeit von Chrome in Bezug auf die Tiefe der verschachtelten block s. Die geteilte Version hat die gleiche Anzahl von Blöcken, aber eine geringere maximale Tiefe.

In diesem Fall würde die Einführung eines allgemeineren Kontrollflusses in Wasm das Problem nicht lösen.

Ich stimme zu, dass dieses Komplexitätsproblem wahrscheinlich am Ende von Chrome gelöst werden kann. Aber ich stelle immer gerne die Frage "Warum gab es dieses Problem überhaupt?". Ich würde argumentieren, dass dieses Problem bei einem allgemeineren Kontrollfluss nie existiert hätte. Außerdem gibt es immer noch den erheblichen allgemeinen Performance-Overhead, der darauf zurückzuführen ist, dass alle Basisblöcke als Sprungtabellen ausgedrückt werden, die meiner Meinung nach durch Optimierung nicht beseitigt werden können.

Ich vermute, dass die Komplexität der Wasm-Laufzeit von Chrome in Bezug auf die Tiefe der verschachtelten Blöcke exponentiell zunimmt. Die geteilte Version hat die gleiche Anzahl von Blöcken, aber eine geringere maximale Tiefe.

Bedeutet dies, dass in einer linearen Funktion mit N Array-Zugriffen der letzte Array-Zugriff verschachtelt ist (ein konstanter Faktor von) N Blöcken tief? Wenn ja, gibt es eine Möglichkeit, dies zu reduzieren, indem der Fehlerbehandlungscode anders berücksichtigt wird? Ich würde erwarten, dass jeder Compiler tuckert, wenn er 3000 verschachtelte Schleifen analysieren muss (sehr grobe Analogie). Wenn dies aus semantischen Gründen unvermeidlich ist, wäre dies auch ein Argument für eine allgemeinere Ablaufsteuerung.

Wenn der Verschachtelungsunterschied weniger stark ist, würde ich meinen, dass V8 fast kein GC'ing von Metadaten durchführt _während_ der Kompilierung einer einzelnen Wasm-Funktion, also selbst wenn wir von Anfang an so etwas wie einen optimierten Funclets-Vorschlag in der Sprache hätten , wären die gleichen Overheads immer noch sichtbar, ohne dass sie eine interessante GC-Optimierung durchführen.

Außerdem gibt es immer noch den erheblichen allgemeinen Performance-Overhead, der darauf zurückzuführen ist, dass alle Basisblöcke als Sprungtabellen ausgedrückt werden, die meiner Meinung nach durch Optimierung nicht beseitigt werden können.

Stimmen Sie zu, dass es (aus rein technischer Sicht) eindeutig vorzuziehen ist, hier ein natürlicheres Ziel zu haben.

Bedeutet dies, dass in einer linearen Funktion mit N Array-Zugriffen der letzte Array-Zugriff verschachtelt ist (ein konstanter Faktor von) N Blöcken tief? Wenn ja, gibt es eine Möglichkeit, dies zu reduzieren, indem der Fehlerbehandlungscode anders berücksichtigt wird? Ich würde erwarten, dass jeder Compiler tuckert, wenn er 3000 verschachtelte Schleifen analysieren muss (sehr grobe Analogie). Wenn dies aus semantischen Gründen unvermeidlich ist, wäre dies auch ein Argument für eine allgemeinere Ablaufsteuerung.

Umgekehrt: Die erste Zuweisung ist so tief verschachtelt, nicht die letzte. Verschachtelte block s und ein einzelnes br_table oben ist, wie eine traditionelle switch Anweisung in wasm ausgedrückt wird. Dies ist die Sprungtabelle, die ich erwähnt habe. Es gibt keine 3000 verschachtelten Schleifen.

Wenn der Verschachtelungsunterschied weniger stark ist, würde ich meinen, dass V8 während der Kompilierung einer einzelnen Wasm-Funktion fast kein GC'ing von Metadaten durchführt, also selbst wenn wir von Anfang an so etwas wie einen optimierten Funclets-Vorschlag in der Sprache hätten , wären die gleichen Overheads immer noch sichtbar, ohne dass sie eine interessante GC-Optimierung durchführen.

Ja, es kann auch einige Implementierungen geben, die eine exponentielle Komplexität in Bezug auf die Anzahl der Basisblöcke aufweisen. Aber die Handhabung von Basisblöcken (auch in großen Mengen) ist das, was viele Compiler den ganzen Tag tun. Zum Beispiel handhabt der Go-Compiler selbst diese Anzahl von Basisblöcken während seiner Kompilierung problemlos, obwohl sie durch mehrere Optimierungsdurchgänge verarbeitet werden.

Ja, es kann auch einige Implementierungen geben, die eine exponentielle Komplexität in Bezug auf die Anzahl der Basisblöcke aufweisen. Aber die Handhabung von Basisblöcken (auch in großen Mengen) ist das, was viele Compiler den ganzen Tag tun. Zum Beispiel handhabt der Go-Compiler selbst diese Anzahl von Basisblöcken während seiner Kompilierung problemlos, obwohl sie durch mehrere Optimierungsdurchgänge verarbeitet werden.

Sicher, aber ein Leistungsproblem hier wäre orthogonal dazu, wie der Kontrollfluss zwischen diesen Basisblöcken in der ursprünglichen Quellsprache ausgedrückt wird (dh keine Motivation für einen allgemeineren Kontrollfluss in Wasm). Um zu sehen, ob V8 hier besonders schlecht ist, könnte man prüfen, ob FireFox/SpiderMonkey oder Lucet/Cranelift die gleichen Kompilierungs-Overheads aufweisen.

Ich habe noch einige Tests durchgeführt: Firefox und Safari zeigen überhaupt keine Probleme. Interessanterweise ist Chrome sogar in der Lage, den Code auszuführen, bevor der intensive Prozess abgeschlossen ist.

Sicher, aber ein Leistungsproblem hier wäre orthogonal dazu, wie der Kontrollfluss zwischen diesen Basisblöcken in der ursprünglichen Quellsprache ausgedrückt wird.

Ich weiß, worauf du hinauswillst.

Ich glaube immer noch, dass die Darstellung von Basisblöcken nicht über Sprungbefehle, sondern über eine Sprungvariable und eine riesige Sprungtabelle / verschachtelte Blöcke das einfache Konzept von Basisblöcken auf recht komplexe Weise ausdrückt. Dies führt zu einem Performance-Overhead und dem Risiko von Komplexitätsproblemen, wie wir sie hier gesehen haben. Ich glaube, dass einfachere Systeme besser und robuster sind als komplexe Systeme. Ich habe immer noch keine Argumente gesehen, die mich davon überzeugen, dass das einfachere System eine schlechte Wahl ist. Ich habe nur gehört, dass V8 es schwer haben würde, einen willkürlichen Kontrollfluss zu implementieren, und meine offene Frage, mir zu sagen, dass diese Aussage falsch ist (https://github.com/WebAssembly/design/issues/796#issuecomment-623431527) hat nicht noch beantwortet worden.

@neelance

Chrome kann den Code sogar ausführen, bevor der intensive Prozess abgeschlossen ist

Es hört sich so an, als ob der Baseline-Compiler Liftoff in Ordnung ist und das Problem im optimierenden Compiler TurboFan liegt. Bitte reichen Sie ein Problem ein oder stellen Sie einen Testfall bereit, und ich kann einen einreichen, wenn Sie es vorziehen.

Allgemeiner gesagt: Glauben Sie, dass die Pläne für den Wasm-Stack-Wechsel die Goroutine-Implementierungsprobleme von Go lösen können? Das ist der beste Link, den ich finden kann, aber er ist jetzt ziemlich aktiv, mit einem zweiwöchentlichen Treffen und mehreren starken Anwendungsfällen, die die Arbeit motivieren. Wenn Go Wasm-Coroutinen verwenden kann, um das große Schaltermuster zu vermeiden, dann denke ich, dass beliebige Gotos nicht erforderlich sind.

Der Go-Compiler verwendet den Relooper-Algorithmus nicht, da er von Natur aus mit dem Konzept des Wechselns von Goroutinen nicht kompatibel ist.

Es stimmt, dass es nicht allein angewendet werden kann. Wir haben jedoch gute Ergebnisse mit der Verwendung von wasm Structured Control Flow + Asyncify erzielt . Die Idee dabei ist, so viel wie möglich normalen Wasm-Kontrollfluss zu erzeugen - ifs, Loops usw. ohne einen einzigen großen Schalter - und Instrumentierung über diesem Muster hinzuzufügen, um das Abwickeln und Zurückspulen des Stapels zu handhaben. Dies führt zu einer relativ kleinen Codegröße und Nicht-Stack-Switching-Code kann grundsätzlich mit voller Geschwindigkeit ausgeführt werden, während ein tatsächlicher Stack-Switch etwas langsamer sein kann (dies ist also gut für den Fall, dass Stack-Switches nicht ständig bei jeder Schleifeniteration usw .).

Ich würde mich sehr freuen, damit auf Go zu experimentieren, wenn Sie interessiert sind! Dies wäre natürlich nicht so gut wie die integrierte Stack-Switching-Unterstützung in wasm, aber es könnte bereits besser sein als das große Switch-Muster. Und es wäre einfacher, später auf die integrierte Stack-Switching-Unterstützung zu wechseln. Konkret könnte dieses Experiment darin bestehen, Go dazu zu bringen, normal strukturierten Code auszugeben, ohne sich Gedanken über den Stack-Wechsel machen zu müssen, und an geeigneten Stellen einfach einen Aufruf an eine spezielle maybe_switch_goroutine Funktion auszugeben. Die Asyncify-Transformation würde sich im Wesentlichen um den Rest kümmern.

Ich interessiere mich für gotos für dynamische rekompilierende Emulatoren wie qemu. Im Gegensatz zu anderen Compilern hat qemu zu keinem Zeitpunkt Kenntnisse über die Ablaufstruktur des Programms, und so sind gotos das einzige vernünftige Ziel. Tailcalls könnten dies beheben, indem sie jeden Block als Funktion und jedes Goto als Tailcall kompilieren.

@kripken Danke für deinen sehr hilfreichen Beitrag.

Es hört sich so an, als ob der Baseline-Compiler Liftoff in Ordnung ist und das Problem im optimierenden Compiler TurboFan liegt. Bitte reichen Sie ein Problem ein oder stellen Sie einen Testfall bereit, und ich kann einen einreichen, wenn Sie es vorziehen.

Hier ist eine wasm-Binärdatei , die Sie mit wasm_exec.html ausführen können .

Glauben Sie, dass die Pläne für den Wasm-Stack-Wechsel in der Lage sein werden, die Goroutine-Implementierungsprobleme von Go zu lösen?

Ja, auf den ersten Blick scheint das hilfreich zu sein.

Wir haben jedoch gute Ergebnisse mit der Verwendung von wasm Structured Control Flow + Asyncify erzielt.

Auch das sieht vielversprechend aus. Wir müssten den Relooper in Go implementieren, aber das ist in Ordnung, denke ich. Ein kleiner Nachteil ist, dass es eine Abhängigkeit von Binaryen hinzufügt, um Wasm-Binärdateien zu erzeugen. Ich werde wahrscheinlich bald einen Vorschlag schreiben.

Ich glaube, der Stackifier-Algorithmus von LLVM ist einfacher/besser, falls Sie das implementieren möchten: https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2

Ich habe einen Vorschlag für das Go-Projekt eingereicht: https://github.com/golang/go/issues/43033

@neelance , schön zu sehen, dass @kripkens Vorschlag ein bisschen bei Golang + Wasm hilft. Wenn man bedenkt, dass dieses Problem eines von Goto / Labels, nicht Stack-Switching ist, und da Asyncify neue Deps / Special Casing-Builds mit Asyncify einführt, bis Stack-Switching veröffentlicht wird usw. - würden Sie dies als Lösung oder weniger als optimale Abschwächung charakterisieren? Wie ist dies im Vergleich zu den geschätzten Vorteilen, wenn goto-Anweisungen verfügbar wären?

Wenn Linus Torvalds' „Guter Geschmack“-Argument für verlinkte Listen auf der Eleganz beruht, ein einziges spezielles Branchen-Statement zu entfernen, ist es schwer, diese Art von Spezialgehäuse-Gymnastik als Gewinn oder sogar als Schritt in die richtige Richtung zu sehen. Nachdem ich gotos persönlich für asynchrone APIs in C verwendet habe, um über den Stapelwechsel zu sprechen, bevor goto-Anweisungen alle Arten von Gerüchen auslösen.

Bitte korrigiert mich, wenn ich mich falsch lese, aber abgesehen von scheinbar vorüberziehenden Antworten, die sich auf marginale Besonderheiten bei einigen aufgeworfenen Fragen konzentrierten, scheinen die Betreuer hier weder Klarheit über die vorliegende Angelegenheit gegeben noch die schwierigen Fragen beantwortet zu haben. Bei allem Respekt, ist diese träge Verknöcherung nicht das Kennzeichen kallusischer Unternehmenspolitik? Wenn dies der Fall ist, verstehe ich die Notlage... Stellen Sie sich vor, all die Sprachen/Compiler, die die Marke von Wasm unterstützen könnte, wenn nur ANSI C ein kompatibler Lackmustest wäre!

@neelance @darkuranium @d4tocchini Nicht alle Wasm-Mitwirkenden denken, dass das Fehlen von Gotos das Richtige ist, tatsächlich würde ich es persönlich als Wasms Designfehler Nr. 1 einstufen. Ich bin absolut dafür, es hinzuzufügen (entweder als Funclets oder direkt).

Die Diskussion über diesen Thread wird jedoch nicht dazu führen, dass Gotos passieren, und es wird nicht auf magische Weise alle an Wasm Beteiligten überzeugen und die Arbeit für Sie erledigen. Hier sind die Schritte:

  1. Treten Sie der Wasm-CG bei.
  2. Jemand investiert die Zeit, um Champion eines Goto-Vorschlags zu werden. Ich empfehle, mit dem bestehenden Funclets-Vorschlag zu beginnen, da er von @sunfishcode bereits gut durchdacht wurde, um aktuelle Engines und Tools, die auf Blockstruktur angewiesen sind, am "wenigsten aufdringlich" zu sein, sodass die
  3. Helfen Sie dabei, durch die 4 Angebotsphasen gedrängt zu werden. Dazu gehört, gute Designs für alle Einwände zu machen, die Ihnen in den Weg geworfen werden, Diskussionen anzustoßen, mit dem Ziel, genügend Menschen glücklich zu machen, damit Sie beim Durchschreiten der Stufen die Mehrheit der Stimmen erhalten.

@d4tocchini Ehrlich gesagt trotzdem an

@aardappel Soweit ich weiß, hat @sunfishcode versucht, den Funclets-Vorschlag zu pushen und ist gescheitert. Warum sollte es bei mir anders sein?

@neelance Ich glaube nicht, dass @sunfishcode viel Zeit hatte, um den Vorschlag über seine anfängliche Erstellung hinaus zu verschieben, daher ist er "festgefahren" und nicht "fehlgeschlagen". Wie ich zu zeigen versuchte, erfordert es, dass ein Champion kontinuierliche Arbeit leistet, damit ein Vorschlag die Pipeline durchläuft.

@neelance

Danke für den Testfall! Ich kann das gleiche Problem lokal bestätigen. Ich habe https://bugs.chromium.org/p/v8/issues/detail?id=11237 eingereicht

Wir müssten den relooper in Go implementieren [..] Ein kleiner Nachteil ist, dass es eine Abhängigkeit von binaryen hinzufügt, um wasm-Binärdateien zu erzeugen.

Übrigens, wenn es helfen würde, können wir eine Bibliothek von Binaryen als einzelne C-Datei erstellen. Vielleicht ist das einfacher zu integrieren?

Auch mit Binaryen können Sie die Relooper Implementierung verwenden , die es gibt . Sie können ihm grundlegende IR-Blöcke übergeben und das Relooping durchführen lassen.

@taralx

Ich glaube, der Stackifier-Algorithmus von LLVM ist einfacher/besser,

Beachten Sie, dass es sich bei diesem Link nicht um Upstream-LLVM handelt, sondern um den Cheerp-Compiler (der eine Abzweigung von LLVM ist). Ihr Stackifier hat einen ähnlichen Namen wie der von LLVM, ist aber anders.

Beachten Sie auch, dass sich dieser Cheerp-Post auf den ursprünglichen Algorithmus von 2011 bezieht – die moderne Relooper-Implementierung (wie bereits erwähnt) hat die genannten Probleme seit vielen Jahren nicht mehr. Mir ist keine einfachere oder bessere Alternative zu diesem allgemeinen Ansatz bekannt, der dem von Cheerp und anderen sehr ähnlich ist - dies sind Variationen eines Themas.

@kripken Danke für die Einreichung des Problems.

Übrigens, wenn es helfen würde, können wir eine Bibliothek von Binaryen als einzelne C-Datei erstellen. Vielleicht ist das einfacher zu integrieren?

Unwahrscheinlich. Der Go-Compiler selbst wurde vor einiger Zeit auf reines Go umgestellt und verwendet afaik keine anderen C-Abhängigkeiten. Ich glaube nicht, dass dies eine Ausnahme sein wird.

Hier ist der aktuelle Stand des Funclets-Vorschlags: Der nächste Schritt im Prozess besteht darin, eine CG-Abstimmung zu fordern, um in Phase 1 einzutreten.

Ich selbst konzentriere mich derzeit auf andere Bereiche in WebAssembly und habe nicht die Bandbreite, um Funclets voranzutreiben; Wenn jemand daran interessiert ist, die Champion-Rolle für Funclets zu übernehmen, gebe ich ihn gerne weiter.

Unwahrscheinlich. Der Go-Compiler selbst wurde vor einiger Zeit auf reines Go umgestellt und verwendet afaik keine anderen C-Abhängigkeiten. Ich glaube nicht, dass dies eine Ausnahme sein wird.

Außerdem löst dies nicht das Problem der extensiven Verwendung von relooper, was zu ernsthaften Performance-Klippen in den WebAssembly-Laufzeiten führt.

@Vurich

Ich denke, dies könnte der beste Fall sein, um Gotos zu Wasm hinzuzufügen, aber jemand müsste überzeugende Daten aus realem Code sammeln, die solch schwerwiegende Perf-Klippen zeigen. Ich selbst habe solche Daten nicht gesehen. Arbeiten zur Analyse von Wasm-Perf-Defiziten wie "Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code" (2019) unterstützen auch nicht den Kontrollfluss als signifikanten Faktor (sie bemerken eine größere Anzahl von Verzweigungsanweisungen, aber das sind sie nicht). durch strukturierten Kontrollfluss - eher durch Sicherheitschecks).

@kripken Haben Sie Vorschläge, wie man solche Daten sammeln könnte? Wie würde man zeigen, dass ein Leistungsdefizit auf einen strukturierten Kontrollfluss zurückzuführen ist?

Es ist unwahrscheinlich, dass es viel Arbeit gibt, die Leistung der Kompilierungsstufe zu analysieren, die hier Teil der Beschwerde ist.

Ich bin etwas überrascht, dass wir noch kein Switch-Case-Konstrukt haben, aber Funclets subsumieren das.

@neelance

Es ist nicht einfach, die spezifischen Ursachen herauszufinden, ja. Für zB Grenzüberprüfungen können Sie sie einfach in der VM deaktivieren und das messen, aber es gibt leider keine einfache Möglichkeit, dasselbe für gotos zu tun.

Eine Möglichkeit besteht darin, den ausgegebenen Maschinencode von Hand zu vergleichen, was sie in diesem verknüpften Papier getan haben.

Eine andere Möglichkeit besteht darin, den Wasm zu etwas zu kompilieren, von dem Sie glauben, dass es den Kontrollfluss optimal handhaben kann, dh die Strukturierung "rückgängig" zu machen. LLVM sollte dazu in der Lage sein, daher könnte es interessant sein, wasm in einer VM auszuführen, die LLVM verwendet (wie WAVM oder wasmer) oder über WasmBoxC. Sie könnten vielleicht CFG-Optimierungen in LLVM deaktivieren und sehen, wie wichtig das ist.

@taralx

Interessant, habe ich etwas über Kompilierzeiten oder Speichernutzung übersehen? Der strukturierte Kontrollfluss sollte dort eigentlich besser sein - zB ist es sehr einfach, von diesem zum SSA-Formular zu gelangen, verglichen mit einem allgemeinen CFG. Dies war in der Tat einer der Gründe, warum wasm überhaupt auf einen strukturierten Kontrollfluss gesetzt hat. Das wird auch sehr sorgfältig gemessen, da es die Ladezeiten im Web beeinflusst.

(Oder meinst du die Compilerleistung auf dem Computer des Entwicklers? Es stimmt, wasm tendiert dazu, dort mehr Arbeit zu leisten und weniger auf den Client.)

Ich meinte die Kompilierungsleistung im Embedder, aber es scheint, dass dies als Fehler behandelt wird, nicht unbedingt als reines Leistungsproblem?

@taralx

Ja, ich denke, das ist ein Bug. Es passiert nur in einer Ebene auf einer VM. Und es gibt keinen grundsätzlichen Grund dafür - strukturierter Kontrollfluss erfordert nicht mehr Ressourcen, er sollte weniger benötigen. Das heißt, ich würde wetten, dass solche Perf-Bugs eher passieren würden, wenn wasm Gotos hätte.

@kripken

Der strukturierte Kontrollfluss sollte dort eigentlich besser sein - zB ist es sehr einfach, von diesem zum SSA-Formular zu gelangen, verglichen mit einem allgemeinen CFG. Dies war in der Tat einer der Gründe, warum wasm überhaupt auf einen strukturierten Kontrollfluss gesetzt hat. Das wird auch sehr sorgfältig gemessen, da es die Ladezeiten im Web beeinflusst.

Eine ganz spezielle Frage für alle Fälle: Kennen Sie einen Wasm-Compiler, der das tatsächlich tut - "sehr einfach" von "strukturiertem Kontrollfluss" zur SSA-Form. Denn auf den ersten Blick ist der Kontrollfluss von Wasm nicht so (vollständig/endgültig) strukturiert. Formal strukturierte Kontrolle ist diejenige, bei der es keine break s, continue s, return s gibt (ungefähr das Programmiermodell von Scheme, ohne Magie wie call/cc). Wenn diese vorhanden sind, kann ein solcher Kontrollfluss grob als "semistrukturiert" bezeichnet werden.

Es gibt einen bekannten SSA-Algo für einen vollständig strukturierten Kontrollfluss: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.45.4503 . Hier ist, was es über den halbstrukturierten Kontrollfluss zu sagen hat:

Für strukturierte Anweisungen haben wir gezeigt, wie Sie beim Parsing sowohl das SSA-Formular als auch den Dominatorbaum in einem Durchgang generieren. Im folgenden Abschnitt zeigen wir, dass es sogar möglich ist, unsere Methode auf eine bestimmte Klasse von unstrukturierten Anweisungen (LOOP/EXIT und RETURN) zu erweitern, die an beliebigen Stellen Ausstiege aus Kontrollstrukturen bewirken können. Da solche Exits jedoch eine Art (diszipliniertes) Goto sind, ist es nicht verwunderlich, dass sie viel schwieriger zu handhaben sind als strukturierte Anweisungen.

OTOH, es gibt noch einen anderen bekannten Algorithmus, https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf der wohl auch Single-Pass ist, aber nicht nur mit unstrukturierter Steuerung keine Probleme hat Fluss, aber sogar mit irreduziblem Kontrollfluss (wenn auch kein optimales Ergebnis dafür).

Die Frage ist also wieder, ob Sie wissen, dass einige Projekte sich die Mühe gemacht haben, den Brandis/Mössenböck-Algorithmus tatsächlich zu erweitern, und auf diesem Weg im Vergleich zu Braun et al. greifbare Vorteile erzielt haben. Algorithmus (als Randbemerkung, meine intuitive Ahnung ist, dass der Braun-Algo genau eine solche "obere Grenze" -Erweiterung ist, obwohl ich zu dumm bin, um es mir selbst intuitiv zu beweisen, ohne über einen formalen Beweis zu sprechen, also das war's - intuitive Ahnung ).

Und das allgemeine Thema der Frage ist es, den ultimativen Grund dafür zu ermitteln (obwohl ich sagen würde, "beibehalten"), warum Wasm sich von der willkürlichen Goto-Unterstützung entschieden hat. Da ich diesen Thread jahrelang beobachte, ist das mentale Modell, das ich aufgebaut habe, dass es so gemacht wird, dass es nicht mit irreduziblen CFGs konfrontiert wird. Und tatsächlich liegt die Kluft zwischen reduzierbaren und irreduziblen CFGs, wobei viele Optimierungsalgorithmen für die reduzierbaren CFGs (viel) einfacher sind, und genau das haben viele Optimierer einprogrammiert. (Semi)strukturierter Kontrollfluss in Wasm ist nur ein billiger Weg, um zu garantieren Reduzierbarkeit.

Die Erwähnung einer besonderen Leichtigkeit der SSA-Produktion für strukturierte CFG (und Wasm-CFGs scheinen im formalen Sinne nicht wirklich strukturiert zu sein) trübt irgendwie das klare Bild oben. Deshalb frage ich, ob es konkrete Hinweise darauf gibt, dass der SSA-Bau praktisch vom Wasm CFG-Formular profitiert.

Danke.

@kripken Ich bin gerade etwas verwirrt und lernbegierig. Ich betrachte die Situation und sehe derzeit Folgendes:


Die Quelle Ihres Programms hat einen bestimmten Kontrollfluss. Diese CFG ist entweder reduzierbar oder nicht, zB wurde goto in der Quellsprache verwendet oder nicht. Es gibt keine Möglichkeit, diese Tatsache zu ändern. Dieses CFG kann in Maschinencode umgewandelt werden, wie es zB der Go-Compiler nativ tut.

Wenn die CFG bereits reduzierbar ist, ist alles gut und die wasm-VM kann sie schnell laden. Jeder Übersetzungsdurchgang sollte in der Lage sein, zu erkennen, dass dies der einfache Fall ist, und schnell gehen. Die Zulassung irreduzibler CFGs sollte diesen Fall nicht verlangsamen.

Wenn der CFG nicht reduzierbar ist, gibt es zwei Möglichkeiten:

  • Der Compiler macht sie reduzierbar, zB durch Einführung einer Sprungtabelle. Bei diesem Schritt gehen Informationen verloren. Es ist schwierig, die ursprüngliche CFG wiederherzustellen, ohne eine Analyse zu haben, die spezifisch für den Compiler ist, der die Binärdatei erstellt hat. Aufgrund dieses Informationsverlusts ist jeder generierte Maschinencode etwas langsamer als der Code, der aus der anfänglichen CFG generiert wurde. Wir können diesen Maschinencode möglicherweise mit einem Single-Pass-Algorithmus generieren, aber dies geht auf Kosten des Informationsverlusts. [1]

  • Wir erlauben dem Compiler, eine irreduzible CFG auszugeben. Die VM muss sie möglicherweise reduzierbar machen. Dies verlangsamt die Ladezeit, jedoch nur in Fällen, in denen die CFG tatsächlich nicht reduzierbar ist. Der Compiler hat die Möglichkeit, zwischen der Optimierung der Ladezeitleistung oder der Laufzeitleistung zu wählen.

[1] Mir ist bewusst, dass es nicht wirklich ein Informationsverlust ist, wenn es noch eine Möglichkeit gibt, den Vorgang rückgängig zu machen, aber besser kann ich es nicht beschreiben.


Wo ist der Denkfehler?

@pfalcon

Kennen Sie einen Wasm-Compiler, der dies tatsächlich tut - "sehr einfach", von "strukturiertem Kontrollfluss" zur SSA-Form zu gehen.

Über VMs: Ich weiß es nicht direkt. Aber IIRC sagte damals @titzer und @lukewagner , es sei bequem, auf diese Weise zu implementieren - vielleicht kann einer von ihnen näher darauf eingehen . Ich bin mir nicht sicher, ob die Irreduzibilität das ganze Problem war oder nicht. Und ich bin mir nicht sicher, ob sie die von Ihnen erwähnten Algorithmen implementiert haben oder nicht.

Zu anderen Dingen als VMs: Der Binaryen-Optimierer profitiert definitiv von dem strukturierten Kontrollfluss von wasm, und nicht nur, dass er reduzierbar ist. Diverse Optimierungen sind einfacher, weil wir immer wissen, wo sich beispielsweise Loop-Header befinden, die im wasm annotiert sind. (Andere OTOH-Optimierungen sind schwieriger durchzuführen, und wir haben auch eine allgemeine CFG-IR für diese ...)

@neelance

Wenn die CFG bereits reduzierbar ist, ist alles gut und die wasm-VM kann sie schnell laden. Jeder Übersetzungsdurchgang sollte in der Lage sein, zu erkennen, dass dies der einfache Fall ist, und schnell gehen. Die Zulassung irreduzibler CFGs sollte diesen Fall nicht verlangsamen.

Vielleicht verstehe ich dich nicht ganz. Dass eine wasm-VM Code schnell laden kann, hängt jedoch nicht nur davon ab, ob er reduzierbar ist oder nicht, sondern auch davon, wie er codiert ist. Konkret hätten wir uns ein Format vorstellen können, bei dem es sich um ein allgemeines CFG handelt, und dann muss die VM die Arbeit erledigen, um zu überprüfen, ob es reduzierbar ist. Wasm hat sich entschieden, diese Arbeit zu vermeiden - die Codierung ist notwendigerweise reduzierbar (dh wenn Sie das wasm lesen und die triviale Validierung durchführen, beweisen Sie auch, dass sie ohne zusätzliche Arbeit reduzierbar ist).

Darüber hinaus garantiert die Kodierung von wasm nicht nur die Reduzierbarkeit, ohne dass dies überprüft werden muss. Es kommentiert auch Schleifenheader, ifs und andere nützliche Dinge (wie ich zuvor in diesem Kommentar zufällig erwähnt habe). Ich bin mir nicht sicher, wie sehr Produktions-VMs davon profitieren, aber ich würde erwarten, dass sie es tun. (Vielleicht besonders in Baseline-Compilern?)

Insgesamt denke ich, dass das Zulassen irreduzibler CFGs den schnellen Fall verlangsamen kann, es sei denn, irreduzible CFGs werden auf separate Weise codiert (wie dies vorgeschlagen wird).

@kripken

Danke für Ihre Erklärung.

Ja, genau diese Unterscheidung versuche ich: Ich sehe den Vorteil der strukturierten Notation/Kodierung für den reduzierbaren CFG-Fall. Es sollte jedoch nicht schwer sein, ein Konstrukt hinzuzufügen, das die Notation einer irreduziblen CFG ermöglicht und dennoch die bestehenden Vorteile im Falle einer reduzierbaren Quell-CFG behält (wenn Sie beispielsweise dieses neue Konstrukt nicht verwenden, ist die CFG garantiert .) reduzierbar sein).

Als Schlussfolgerung sehe ich nicht, wie man argumentieren kann, dass eine rein reduzierbare Notation schneller ist. Im Fall einer reduzierbaren Quellen-CFG ist es genauso schnell. Und im Falle einer irreduziblen CFG-Quelle kann man höchstens argumentieren, dass sie nicht wesentlich langsamer ist, aber einige Fälle aus der Praxis haben bereits gezeigt, dass dies im Allgemeinen unwahrscheinlich ist.

Kurz gesagt, ich verstehe nicht, wie Leistungsüberlegungen ein Argument sein können, das einen irreduziblen Kontrollfluss verhindert, und das lässt mich fragen, warum der nächste Schritt das Sammeln von Leistungsdaten sein muss.

@neelance

Ja, ich stimme zu, dass wir ein neues Konstrukt hinzufügen könnten - wie Funklets - und wenn es nicht verwendet wird, würde es den bestehenden Fall nicht verlangsamen.

Das Hinzufügen neuer Konstrukte hat jedoch einen Nachteil, da es den Wasm komplexer macht. Insbesondere bedeutet dies eine größere Oberfläche auf VMs, was mehr mögliche Fehler und Sicherheitsprobleme bedeutet. Wasm hat dazu tendiert, möglichst viel Komplexität auf der Entwicklerseite zu haben, um die Komplexität auf der VM zu reduzieren.

Bei einigen wasm-Vorschlägen geht es nicht nur um Geschwindigkeit, wie bei GC (die das Sammeln von Zyklen mit JS ermöglicht). Aber bei Vorschlägen, bei denen es um Geschwindigkeit geht, wie beispielsweise Funklets, müssen wir zeigen, dass die Geschwindigkeit die Komplexität rechtfertigt. Wir hatten diese Debatte über SIMD, bei der es auch um Geschwindigkeit geht, und entschieden, dass es sich gelohnt hat, weil wir gesehen haben, dass es zuverlässig sehr hohe Geschwindigkeiten bei realem Code (2x oder sogar mehr) erreichen kann.

(Es gibt andere Vorteile als die Geschwindigkeit, wenn allgemeine CFGs zugelassen werden, stimme ich zu, z. B. einfacher für Compiler, auf wasm abzuzielen. Aber wir können das lösen, ohne wasm-VMs komplexer zu machen. Wir bieten bereits Unterstützung für beliebige CFGs in LLVM und Binaryen , sodass Compiler CFGs ausgeben können und sich keine Gedanken über einen strukturierten Kontrollfluss machen müssen. Wenn das nicht gut genug ist, sollten wir - Tools-Leute, die ich meine, mich eingeschlossen - mehr tun.)

Bei Funclets geht es nicht so sehr um Geschwindigkeit, sondern darum, dass Sprachen mit nicht trivialem Kontrollfluss in WebAssembly kompiliert werden können, wobei C und Go am offensichtlichsten ist, aber es gilt für jede Sprache, die async/await hat. Außerdem führt die Entscheidung für einen hierarchischen Kontrollfluss tatsächlich zu _mehr_ Fehlern in VMs, wie die Tatsache zeigt, dass alle Wasm-Compiler außer V8 den hierarchischen Kontrollfluss ohnehin in eine CFG zerlegen. Die EBBs in einer CFG können die mehreren Kontrollflusskonstrukte in Wasm und mehr darstellen, und ein einziges Konstrukt zum Kompilieren führt zu weit weniger Fehlern als viele verschiedene Arten mit unterschiedlichen Verwendungen.

Sogar Lightbeam, ein sehr einfacher Streaming-Compiler, verzeichnete einen massiven Rückgang von Fehlern bei der falschen Kompilierung, nachdem ein zusätzlicher Übersetzungsschritt hinzugefügt wurde, der den Kontrollfluss in eine CFG zerlegte. Dies gilt doppelt für die andere Seite dieses Prozesses - Relooper ist weitaus fehleranfälliger als das Ausgeben von Funclets, und mir wurde von Entwicklern, die an den Wasm-Backends für LLVM und anderen Compilern arbeiten, die zu implementierende Funclets waren, gesagt, dass sie jeden ausgeben würden Funktion nur mit Funclets, um die Zuverlässigkeit und Einfachheit von Codegen zu verbessern. Alle Compiler, die Wasm produzieren, verwenden EBBs, alle bis auf einen der Compiler, die Wasm konsumieren, verwenden EBBs .

"Irrreduzierbarer Kontrollfluss als schädlich" ist nur ein Diskussionspunkt, Sie können einfach die Einschränkung hinzufügen, dass der Kontrollfluss von Funklets reduzierbar ist und dann, wenn Sie in Zukunft irreduziblen Kontrollfluss zulassen möchten, alle vorhandenen Wasm-Module mit reduzierbarem Kontrollfluss unverändert funktionieren würden auf einem Motor, der zusätzlich irreduziblen Kontrollfluss unterstützt. Es würde lediglich darum gehen, die Reduzierbarkeitsprüfung im Validator zu entfernen.

@Vurich

Sie können ganz einfach die Einschränkung hinzufügen, dass der Kontrollfluss von Funklets reduzierbar ist

Sie können, aber es ist nicht trivial - VMs müssten dies überprüfen. Ich glaube nicht, dass dies in einem einzigen linearen Durchgang möglich ist, was für Baseline-Compiler, die jetzt in den meisten VMs vorhanden sind, ein Problem darstellen würde. (Tatsächlich kann das Auffinden von Schleifenhinterkanten - was ein einfacheres Problem ist und auch aus anderen Gründen notwendig ist - nicht in einem einzigen Vorwärtsdurchgang erfolgen, oder?)

alle Wasm-Compiler außer V8 zerlegen den hierarchischen Kontrollfluss ohnehin in eine CFG.

Beziehen Sie sich auf den Ansatz des "Meeres der Knoten", den TurboFan verwendet? Da ich kein Experte bin, überlasse ich es anderen zu antworten.

Aber im Allgemeinen gilt, selbst wenn Sie das obige Argument für die Optimierung von Compilern nicht kaufen, es gilt noch direkter für Baseline-Compiler, wie bereits erwähnt.

Bei Funclets geht es nicht so sehr um Geschwindigkeit, sondern darum, Sprachen mit nicht trivialem Kontrollfluss zu erlauben, in WebAssembly zu kompilieren [..] Relooper ist weitaus fehleranfälliger als das Ausgeben von Funclets

Ich stimme der Tools-Seite zu 100% zu. Es ist schwieriger, strukturierten Code von den meisten Compilern auszugeben! Aber der Punkt ist, dass es auf der VM-Seite einfacher wird, und wasm hat sich dafür entschieden. Aber auch hier stimme ich zu, dass dies Kompromisse hat, einschließlich der von Ihnen erwähnten Nachteile.

Hat wasm das 2015 falsch verstanden? Es ist möglich. Ich denke, wir haben selbst einige Dinge falsch gemacht (wie die Debugging-Funktion und der späte Wechsel zu einer Stack-Maschine). Aber es ist nicht möglich, diese im Nachhinein zu beheben, und es gibt eine hohe Latte, neue Dinge hinzuzufügen, insbesondere überlappende.

Angesichts all dessen denke ich, dass wir, wenn wir versuchen, konstruktiv zu sein, bestehende Probleme auf der Seite der Tools beheben sollten. Es gibt eine viel, viel niedrigere Leiste für Werkzeugwechsel. Zwei mögliche Vorschläge:

  • Ich kann in Erwägung ziehen, den Binaryen-CFG-Code nach Go zu portieren, wenn das dem Go-Compiler helfen würde - @neelance ?
  • Wir können Funclets oder ähnliches rein werkzeugseitig implementieren. Das heißt, wir stellen heute Bibliothekscode dafür bereit, könnten aber auch ein Binärformat hinzufügen. (Es gibt bereits einen Präzedenzfall für das Hinzufügen zum wasm-Binärformat auf der Seite der Tools in wasm-Objektdateien.)

Wir können Funclets oder ähnliches rein werkzeugseitig implementieren. Das heißt, wir stellen heute Bibliothekscode dafür bereit, könnten aber auch ein Binärformat hinzufügen. (Es gibt bereits einen Präzedenzfall für das Hinzufügen zum wasm-Binärformat auf der Seite der Tools in wasm-Objektdateien.)

Wenn diesbezüglich konkrete Arbeit geleistet wurde, ist es erwähnenswert, dass (AFAIU) die kleinste idiomatische Möglichkeit, dies zu Wasm hinzuzufügen (wie @rossberg anspielte ), darin besteht, die Blockanweisung einzuführen

Multiloop (t in ) _n_ t out (_instr_* end ) _n_

die n beschriftete Körper definiert (mit n Eingabetyp-Anmerkungen, die vorwärts deklariert werden). Die br- Befehlsfamilie wird dann verallgemeinert, so dass alle durch die richtigen Reihenfolge im Geltungsbereich liegen (wie in jeder Körper kann von jedem anderen Körper aus verzweigt werden). Wenn ein multiloop Körper verzweigt ist, springt die Ausführung zu dem _start_ des Körpers (wie bei einem normalen wasm loop). Wenn die Ausführung das Ende eines Körpers erreicht, ohne zu einem anderen Körper zu verzweigen, kehrt das gesamte Konstrukt zurück (kein Fall-Through).

Es wäre einiges zu tun, um die Typannotationen jedes Körpers effizient darzustellen (in der obigen Formulierung können n Körper n verschiedene Eingabetypen haben, müssen jedoch alle denselben Ausgabetyp haben, sodass ich ihn nicht direkt verwenden kann reguläre mehrwertige _blocktype_-Indizes, ohne dass eine überflüssige LUB-Berechnung erforderlich ist), und wie man den auszuführenden Ausgangskörper auswählt (immer der erste oder sollte es einen statischen Parameter geben?).

Dadurch wird die gleiche Expressivität wie bei Funklets erreicht, es wird jedoch vermieden, dass ein neuer Bereich von Steuerbefehlen eingeführt werden muss. In der Tat, wenn die Funclets weiter iteriert worden wären, denke ich, wäre es zu so etwas geworden.

BEARBEITEN: Dies zu optimieren, um ein Fall-Through-Verhalten zu haben, würde die formale Semantik geringfügig komplizieren, wäre aber wahrscheinlich besser für den Anwendungsfall von

Das Konstruktionsprinzip von Wasm, die Arbeit auf die Werkzeuge zu verlagern, um Motoren einfacher/schneller zu machen, ist sehr wichtig und wird auch weiterhin sehr nützlich sein.

Das heißt, wie alles, was nicht trivial ist, ist es ein Kompromiss, nicht schwarz und weiß. Ich glaube, hier haben wir einen Fall, in dem die Schmerzen für die Hersteller unverhältnismäßig sind zu den Schmerzen für die Motoren. Die meisten Compiler, die wir zu Wasm bringen möchten, verwenden entweder intern beliebige CFG-Strukturen (SSA) oder werden verwendet, um auf Dinge abzuzielen, die Gotos nicht stören (CPUs). Wir lassen die Welt durch die Reifen springen, um nicht viel Gewinn zu machen.

Etwas wie Funclets (oder Multiloop) ist schön, weil es modular ist: Wenn ein Produzent es nicht braucht, funktioniert alles wie zuvor. Wenn eine Engine wirklich nicht mit beliebigen CFGs umgehen kann, dann kann sie sie im Moment so ausgeben, als ob es ein loop + br_table Konstrukt wäre, und nur diejenigen, die sie verwenden, zahlen den Preis . Dann "entscheidet der Markt" und wir sehen, ob der Druck auf die Engines besteht, besseren Code dafür auszugeben. Irgendetwas sagt mir, dass es für Engines keine so große Katastrophe sein wird, guten Code für sie auszugeben, wie manche Leute denken, wenn es viel Wasm-Code geben wird, der auf Funklets beruht.

Sie können, aber es ist nicht trivial - VMs müssten dies überprüfen. Ich glaube nicht, dass dies in einem einzigen linearen Durchgang möglich ist, was für Baseline-Compiler, die jetzt in den meisten VMs vorhanden sind, ein Problem darstellen würde.

Vielleicht verstehe ich die Erwartungen an einen Baseline-Compiler falsch, aber warum sollte es sie interessieren? Wenn Sie ein goto sehen, fügen Sie eine Sprunganweisung ein.

Ich stimme der Tools-Seite zu 100% zu. Es ist schwieriger, strukturierten Code von den meisten Compilern auszugeben! Aber der Punkt ist, dass es auf der VM-Seite einfacher wird, und wasm hat sich dafür entschieden. Aber auch hier stimme ich zu, dass dies Kompromisse hat, einschließlich der von Ihnen erwähnten Nachteile.

Nein, wie ich in meinem ursprünglichen Kommentar mehrmals sage, macht es die Dinge auf der VM-Seite _nicht_ einfacher. Ich arbeitete über ein Jahr an einem Baseline-Compiler und mein Leben wurde einfacher und der ausgegebene Code wurde schneller, nachdem ich einen Zwischenschritt hinzugefügt hatte, der den Kontrollfluss von Wasm in eine CFG konvertierte.

Sie können, aber es ist nicht trivial - VMs müssten dies überprüfen. Ich glaube nicht, dass dies in einem einzigen linearen Durchgang möglich ist, was für Baseline-Compiler, die jetzt in den meisten VMs vorhanden sind, ein Problem darstellen würde. (Tatsächlich kann das Auffinden von Schleifenhinterkanten - was ein einfacheres Problem ist und auch aus anderen Gründen notwendig ist - nicht in einem einzigen Vorwärtsdurchgang erfolgen, oder?)

Okay, hier ist die Sache, mein Wissen über die in Compilern verwendeten Algorithmen ist nicht stark genug, um mit absoluter Sicherheit zu sagen, dass irreduzibler Kontrollfluss in einem Streaming-Compiler erkannt werden kann oder nicht, aber die Sache ist, dass dies nicht der Fall sein muss. Die Verifizierung kann gleichzeitig mit der Kompilierung erfolgen. Wenn kein Streaming-Algorithmus existiert, von dem weder Sie noch ich wissen, dass er nicht existiert, können Sie einen Nicht-Streaming-Algorithmus verwenden, sobald die Funktion vollständig empfangen wurde. Wenn (aus irgendeinem Grund) ein irreduzibler Kontrollfluss zu etwas wirklich Schlimmem führt, wie einer Endlosschleife, können Sie die Kompilierung einfach abbrechen und/oder den Kompilierungs-Thread abbrechen. Es gibt jedoch keinen Grund zu der Annahme, dass dies der Fall sein würde.

Vielleicht verstehe ich die Erwartungen an einen Baseline-Compiler falsch, aber warum sollte es sie interessieren? Wenn Sie ein goto sehen, fügen Sie eine Sprunganweisung ein.

Es ist nicht so einfach, weil Sie die unendliche Registermaschine von Wasm (nein, es ist keine Stapelmaschine ) auf die endlichen Register der physischen Hardware abbilden müssen, aber das ist ein Problem, das jeder Streaming-Compiler lösen muss, und es ist völlig orthogonal zu CFGs vs. hierarchischer Kontrollfluss.

Der Streaming-Compiler, an dem ich gearbeitet habe, kann eine beliebige - sogar irreduzible - CFG problemlos kompilieren. Es macht nichts Besonderes. Sie weisen jedem Block einfach eine "Aufrufkonvention" zu (im Grunde die Stelle, an der die Werte im Gültigkeitsbereich in diesem Block sein sollten), wenn Sie zum ersten Mal dorthin springen müssen und wenn Sie jemals an einen Punkt gelangen, an dem Sie bedingt zu zwei verzweigen müssen oder mehr Ziele mit inkompatiblen "Aufrufkonventionen" schiebt man einen "Adapter"-Block in eine Warteschlange und gibt ihn am nächstmöglichen Punkt aus. Dies kann sowohl bei reduzierbarem als auch bei nicht reduzierbarem Kontrollfluss passieren und ist in beiden Fällen fast nie notwendig. Das Argument "irreduzibler Kontrollfluss wird als schädlich angesehen" ist, wie ich bereits sagte, ein Gesprächsthema und kein technisches Argument. Die Darstellung des Kontrollflusses als CFG macht das Schreiben von Streaming-Compilern viel einfacher, und wie ich bereits mehrfach sagte, weiß ich dies aus umfangreicher persönlicher Erfahrung.

Alle Fälle, in denen ein irreduzibler Kontrollfluss das Schreiben von Implementierungen erschwert, von denen ich mir keine vorstellen kann, können einfach ausgeblendet werden und einen Fehler zurückgeben, und wenn Sie einen separaten, nicht streamenden Algorithmus benötigen, um diese Kontrolle mit 100%iger Sicherheit zu erkennen flow irreduzibel ist (damit Sie nicht versehentlich einen irreduziblen Kontrollfluss akzeptieren), dann kann dieser unabhängig vom Baseline-Compiler selbst ausgeführt werden. Mir wurde von jemandem gesagt, von dem ich Grund zu der Annahme habe, dass er eine Autorität auf diesem Gebiet ist (obwohl ich es vermeiden werde, ihn anzurufen, weil ich weiß, dass er nicht in diesen Thread hineingezogen werden möchte), dass es einen relativ einfachen Streaming-Algorithmus gibt für die Feststellung der Irreduzibilität einer CFG, aber ich kann nicht aus erster Hand sagen, dass dies wahr ist.

@oridb

Vielleicht verstehe ich die Erwartungen an einen Baseline-Compiler falsch, aber warum sollte es sie interessieren? Wenn Sie ein goto sehen, fügen Sie eine Sprunganweisung ein.

Baseline-Compiler müssen immer noch Dinge tun, wie zum Beispiel zusätzliche Prüfungen an Schleifenhinterkanten einfügen (so zeigt eine hängende Seite im Web schließlich einen langsamen Skriptdialog), also müssen sie solche Dinge identifizieren. Außerdem versuchen sie, eine einigermaßen effiziente Registerzuordnung durchzuführen (Baseline-Compiler laufen oft mit etwa der Hälfte der Geschwindigkeit des optimierenden Compilers - was sehr beeindruckend ist, da sie Single-Pass sind!). Die Struktur des Kontrollflusses, einschließlich Joins und Splits, macht dies viel einfacher.

@gwvo

Das heißt, wie alles, was nicht trivial ist, ist es ein Kompromiss, nicht schwarz und weiß. [..] Wir lassen die Welt durch die Reifen springen, um nicht viel Gewinn zu machen.

Ich stimme voll und ganz zu, dass es ein Kompromiss ist, und vielleicht hat wasm damals sogar einen Fehler gemacht. Aber ich glaube, es ist viel praktischer, diese Reifen auf der Werkzeugseite zu befestigen.

Dann "entscheidet der Markt" und wir sehen, ob der Druck auf die Engines besteht, besseren Code dafür auszugeben.

Das haben wir bisher eigentlich vermieden. Wir haben versucht, wasm auf der VM so einfach wie möglich zu gestalten, sodass keine komplexen Optimierungen erforderlich sind – nicht einmal Dinge wie Inlining, so weit wie möglich. Das Ziel besteht darin, die harte Arbeit auf der Seite der Tools zu leisten, und nicht, VMs unter Druck zu setzen, besser zu werden.

@Vurich

Ich arbeitete über ein Jahr an einem Baseline-Compiler und mein Leben wurde einfacher und der ausgegebene Code wurde schneller, nachdem ich einen Zwischenschritt hinzugefügt hatte, der den Kontrollfluss von Wasm in eine CFG konvertierte.

Sehr interessant! Welche VM war das?

Ich wäre auch besonders neugierig, ob es sich um ein Single-Pass/Streaming handelt oder nicht (wenn ja, wie hat es die Loop-Backedge-Instrumentierung gehandhabt?) und wie es die Registerzuordnung vornimmt.

Im Prinzip können sowohl Schleifenhinterflanken als auch Registerzuweisungen basierend auf einer linearen Befehlsreihenfolge gehandhabt werden, in der Erwartung, dass Basisblöcke in eine vernünftige Topsort-ähnliche Reihenfolge gebracht werden, ohne dass dies strikt erforderlich ist.

Für Schleifen-Backedges: Definieren Sie einen Backedge als eine Anweisung, die zu einer früheren Stelle im Anweisungsstrom springt. Im schlimmsten Fall, wenn die Blöcke rückwärts angeordnet sind, erhalten Sie mehr Backedge-Checks als unbedingt erforderlich.

Für Registerzuordnung: Dies ist nur die standardmäßige lineare Scan-Registerzuordnung . Die Lebensdauer einer Variablen für die Registerzuordnung reicht von der ersten Nennung der Variablen bis zur letzten Nennung, einschließlich aller Blöcke, die linear dazwischen liegen. Im schlimmsten Fall, wenn die Blöcke herumgemixt werden, erhalten Sie eine längere Lebensdauer als nötig und verschütten so unnötig Dinge auf den Stapel. Die einzigen zusätzlichen Kosten sind die Verfolgung der ersten und letzten Erwähnung jeder Variablen, die für alle Variablen mit einem einzigen linearen Scan durchgeführt werden kann. (Für wasm nehme ich an, dass eine "Variable" entweder ein lokaler oder ein Stack-Slot ist.)

@kripken

Ich kann in Erwägung ziehen, den Binaryen-CFG-Code nach Go zu portieren, wenn das dem Go-Compiler helfen würde - @neelance ?

Für die Integration von Asyncify? Bitte kommentieren Sie den Vorschlag .

@comex

Gute Argumente!

Die einzigen zusätzlichen Kosten sind die Verfolgung der ersten und letzten Erwähnung jeder Variablen

Ja, ich denke, das ist ein wesentlicher Unterschied. Die Zuweisung von linearen Scan-Registern ist besser (aber langsamer) als wasm-Baseline-Compiler derzeit tun , da sie auf Streaming-Art kompilieren, was wirklich schnell ist. Das heißt, es gibt keinen ersten Schritt, um die letzte Erwähnung jeder Variablen zu finden - sie kompilieren in einem einzigen Durchgang, geben Code aus, ohne später Code in der wasm-Funktion zu sehen, unterstützt durch die Struktur, und sie machen auch einfach Wahlen, wie sie gehen ("dumm" ist das Wort, das in diesem Beitrag verwendet wird).

Der Streaming-Ansatz von V8 zur Registerzuweisung sollte genauso gut funktionieren, wenn Blöcke gegenseitig rekursiv sein dürfen (wie in https://github.com/WebAssembly/design/issues/796#issuecomment-742690194), da die einzigen Lebensdauern, mit denen sie sich beschäftigen innerhalb eines einzelnen Blocks gebunden (Stack) oder als funktionsweit (lokal) angenommen werden.

IIUC (mit Bezug auf @titzers Kommentar ) Das Hauptproblem für V8 liegt in der Art von CFGs, die Turbofan optimieren kann.

@kripken

Wir haben versucht, wasm auf der VM so einfach wie möglich zu gestalten, damit keine komplexen Optimierungen erforderlich sind

Dies ist keine "komplexe Optimierung".. Gotos sind für viele Systeme unglaublich einfach und natürlich. Ich wette, es gibt viele Motoren, die dies kostenlos hinzufügen könnten. Ich sage nur, wenn es Motoren gibt, die aus irgendeinem Grund an einem strukturierten CFG-Modell festhalten wollen, können sie das.

Zum Beispiel bin ich mir ziemlich sicher, dass LLVM (derzeit bei weitem unser Nr. 1 Wasm-Produzent) nicht auf die Verwendung von Funclets umsteigen wird, bis es sicher ist, dass es sich nicht um einen Leistungsrückgang bei wichtigen Engines handelt.

@kripken Es ist Teil von Wasmtime. Ja, es ist Streaming und sollte O(N)-Komplexität sein, aber ich bin zu einem neuen Unternehmen gewechselt, bevor das vollständig realisiert war, also ist es nur "O(N)-ish". https://github.com/bytecodealliance/wasmtime/tree/main/crates/lightbeam

Danke @Vurich , interessant. Es wäre großartig, Leistungszahlen zu sehen, wenn diese verfügbar sind, insbesondere für den Start, aber auch für den Durchsatz. Ich würde vermuten, dass Ihr Ansatz langsamer kompiliert als der Ansatz der V8- und SpiderMonkey-Ingenieure, während er schnelleren Code ausgibt. Es ist also ein anderer Kompromiss in diesem Bereich. Es erscheint plausibel, dass Ihr Ansatz nicht von dem strukturierten Kontrollfluss von wasm profitiert, wie Sie sagten, während dies deren tut.

Nein, es ist ein Streaming-Compiler und gibt Code schneller aus als jede dieser beiden Engines (obwohl es degenerierte Fälle gibt, die zu dem Zeitpunkt, als ich das Projekt verließ, nicht behoben wurden). Obwohl ich mein Bestes getan habe, um schnellen Code zu senden, ist es in erster Linie darauf ausgelegt, Code schnell zu senden, wobei die Effizienz der Ausgabe von zweitrangiger Bedeutung ist. Die Startkosten sind meines Wissens null (über den inhärenten Kosten von Wasmtime, die zwischen den Backends geteilt werden), da jede Datenstruktur nicht initialisiert beginnt und die Kompilierung Anweisung für Anweisung erfolgt. Während ich keine Zahlen zum Vergleich mit V8 oder SpiderMonkey zur Hand habe, habe ich Zahlen zum Vergleich mit Cranelift (dem primären Motor in wasmtime). Sie sind zu diesem Zeitpunkt mehrere Monate veraltet, aber Sie können sehen, dass es nicht nur Code schneller ausgibt als Cranelift, sondern auch schnelleren Code als Cranelift. Damals gab es auch schnelleren Code aus als SpiderMonkey, obwohl Sie mir darauf vertrauen müssen, damit ich Ihnen keine Vorwürfe mache, wenn Sie mir nicht glauben. Obwohl ich keine neueren Zahlen zur Hand habe, glaube ich, dass der Zustand jetzt so ist, dass sowohl Cranelift als auch SpiderMonkey die kleine Handvoll Fehler behoben haben, die die Hauptursache für ihre leistungsschwache Ausgabe in diesen Mikrobenchmarks im Vergleich zu Lightbeam waren. aber der Geschwindigkeitsunterschied bei der Kompilierung hat sich während meiner gesamten Projektzeit nicht geändert, da jeder Compiler immer noch im Wesentlichen gleich aufgebaut ist und es die jeweilige Architektur ist, die zu den unterschiedlichen Leistungsniveaus führt. Obwohl ich Ihre Spekulationen schätze, weiß ich nicht, woher Ihre Annahme kommt, dass die von mir skizzierte Methode langsamer wäre.

Hier sind die Benchmarks, die ::compile Benchmarks sind für die Kompilierungsgeschwindigkeit und die ::run Benchmarks sind für die Ausführungsgeschwindigkeit der Maschinencodeausgabe. https://gist.github.com/Vurich/8696e67180aa3c93b4548fb1f298c29e

Die Methodik ist da, Sie können sie klonen und die Benchmarks erneut ausführen, um die Ergebnisse für sich selbst zu bestätigen, aber die PR wird wahrscheinlich nicht mit der neuesten Version von wasmtime kompatibel sein, daher wird Ihnen nur der Leistungsvergleich zum Zeitpunkt der letzten Aktualisierung angezeigt PR. https://github.com/bytecodealliance/wasmtime/pull/1660

Davon abgesehen argumentiere ich _nicht_, dass CFGs eine nützliche interne Darstellung für die Leistung in einem Streaming-Compiler sind. Mein Argument ist, dass CFGs die Leistung in keinem Compiler negativ beeinflussen, und schon gar nicht in dem Maße, das es rechtfertigen würde, die GCC- und Go-Teams vollständig von der Produktion von WebAssembly auszuschließen. Fast niemand in diesem Thread, der sich gegen Funklets oder eine ähnliche Erweiterung von Wasm ausspricht, hat tatsächlich an den Projekten gearbeitet, von denen behauptet wird, dass sie von diesem Vorschlag negativ beeinflusst werden. Um nicht zu sagen, dass man Erfahrung aus erster Hand braucht, um sich zu diesem Thema zu äußern, ich denke, jeder hat einen gewissen wertvollen Beitrag, aber es gibt eine Grenze zwischen einer anderen Meinung zur Farbe des Fahrradschuppens und der Herstellung Behauptungen, die auf nichts anderem als müßigen Spekulationen beruhen.

@Vurich

Nein, es ist ein Streaming-Compiler und gibt Code schneller aus als jede dieser beiden Engines (obwohl es degenerierte Fälle gibt, die nie behoben wurden, weil ich das Projekt verlassen habe).

Tut mir leid, wenn ich mich früher nicht klar genug ausgedrückt habe. Um sicher zu sein, dass wir über dasselbe sprechen, meinte ich die Baseline-Compiler in diesen Engines. Und ich spreche von Kompilierzeit, die der Punkt von Baseline-Compilern in dem Sinne ist, dass V8 und SpiderMonkey den Begriff verwenden.

Der Grund, warum ich skeptisch bin, dass Sie die Baseline-Compilierungszeiten von V8 und SpiderMonkey übertreffen können, liegt darin, dass diese beiden Baseline-Compiler, wie in den Links, die ich zuvor angegeben habe, außergewöhnlich auf die Kompilierungszeit abgestimmt sind. Insbesondere erzeugen sie keine interne IR, sie gehen einfach direkt von Wasm zu Maschinencode. Sie sagten, dass Ihr Compiler eine interne IR ausgibt (für eine CFG) - ich würde erwarten, dass Ihre Kompilierungszeiten nur deswegen langsamer sind (aufgrund von mehr Verzweigung, Speicherbandbreite usw.).

Aber bitte vergleichen Sie diese Baseline-Compiler! Ich würde gerne Daten sehen, die zeigen, dass meine Vermutung falsch ist, und ich bin mir sicher, dass dies auch die V8- und SpiderMonkey-Ingenieure tun würden. Es würde bedeuten, dass Sie ein besseres Design gefunden haben, das sie in Betracht ziehen sollten.

Um gegen V8 zu testen, können Sie d8 --liftoff --no-wasm-tier-up ausführen und für SpiderMonkey können Sie sm --wasm-compiler=baseline ausführen.

(Vielen Dank für die Anweisungen zum Vergleich mit Cranelift, aber Cranelift ist kein Baseline-Compiler, daher ist der Vergleich der Compile-Zeiten in diesem Zusammenhang nicht relevant. Ansonsten sehr interessant, stimme ich zu.)

Meine Intuition ist, dass Baseline-Compiler ihre Kompilierungsstrategie nicht wesentlich ändern müssten , um Funclets/ Interblock -Optimierung durchzuführen. Die von @kripken referenzierte "Struktur des Kontrollflusses, einschließlich Joins und Splits" wird dadurch erfüllt, dass alle Eingabetypen für eine Sammlung von gegenseitig rekursiven Blöcken vorwärts deklariert werden müssen (was sowieso die natürliche Wahl für die Streaming-Validierung scheint). . Ob Lightbeam/Wasmtime Engine-Baseline-Compiler schlagen kann, spielt keine Rolle; Der wichtige Punkt ist, ob Engine-Baseline-Compiler genauso schnell bleiben können wie jetzt.

FWIW, ich wäre daran interessiert, diese Funktion in einem zukünftigen CG-Meeting zur Diskussion zu stellen, und ich stimme @Vurich weitgehend zu, dass Engine- Einwände erheben können, wenn sie nicht bereit sind, sie zu implementieren. Davon abgesehen sollten wir solche Einwände ernst nehmen (ich habe zuvor bei persönlichen Treffen geäußert, dass wir bei der Verfolgung dieser Funktion versuchen sollten, eine WebAssembly-Version der JavaScript Proper Tail Calls-Saga zu vermeiden). Den ersten Schritt zu einer solchen CG-Diskussion mache ich gerne im neuen Jahr selbst, wenn ich meine (derzeit sehr sehr späte) Abschlussarbeit fertig habe.

@kripken

Ja, ich denke, das ist ein wesentlicher Unterschied. Die Zuweisung von linearen Scan-Registern ist besser (aber langsamer) als wasm-Baseline-Compiler derzeit tun , da sie auf Streaming-Art kompilieren, was wirklich schnell ist. Das heißt, es gibt keinen ersten Schritt, um die letzte Erwähnung jeder Variablen zu finden - sie kompilieren in einem einzigen Durchgang, geben Code aus, ohne später Code in der wasm-Funktion zu sehen, unterstützt durch die Struktur, und sie machen auch einfach Wahlen, wie sie gehen ("dumm" ist das Wort, das in diesem Beitrag verwendet wird).

Wow, das ist wirklich ganz einfach.

Auf der anderen Seite… dieser spezielle Algorithmus ist so einfach, dass er nicht von tieferen Eigenschaften des strukturierten Kontrollflusses abhängt. Es hängt kaum noch von flachen Eigenschaften eines strukturierten Kontrollflusses ab.

Wie im Blogbeitrag erwähnt, behält der wasm-Baseline-Compiler von SpiderMonkey den Registerzuordnerstatus nicht durch "Control-Flow-Joins" (dh grundlegende Blöcke mit mehreren Vorgängern) bei, sondern verwendet eine feste ABI oder eine Zuordnung vom wasm-Stack zu nativem Stack und Registern . Ich habe beim Testen festgestellt, dass es beim Eingeben von Blöcken auch eine feste ABI

Die feste ABI ist wie folgt (auf x86):

  • Wenn es eine Anzahl von Parametern ungleich null gibt (beim Betreten eines Blocks) oder zurückkehrt (beim Verlassen eines Blocks), dann geht die Spitze des Wasm-Stacks in rax , und der Rest des Wasm-Stacks entspricht x86 Stapel.
  • Ansonsten entspricht der gesamte wasm-Stack dem x86-Stack.

Warum ist das wichtig?

Denn dieser Algorithmus könnte mit viel weniger Informationen fast genauso funktionieren. Stellen Sie sich als Gedankenexperiment eine alternative Universumsversion von WebAssembly vor, bei der es keine strukturierten Ablaufsteuerungsbefehle gibt, sondern nur Sprungbefehle, ähnlich der nativen Assemblierung. Es müsste nur um eine zusätzliche Information ergänzt werden: eine Möglichkeit zu erkennen, welche Anweisungen das Ziel von Sprüngen sind.

Dann wäre der Algorithmus nur: Anweisungen linear durchlaufen; vor Sprüngen und Sprungzielen, Flush-Register zum festen ABI.

Der einzige Unterschied besteht darin, dass es einen einzigen festen ABI geben müsste, nicht zwei. Es konnte nicht unterscheiden, ob der Top-of-Stack-Wert semantisch das "Ergebnis" eines Sprungs ist oder nur von einem äußeren Block auf dem Stack belassen wird. Es müsste also bedingungslos die Spitze des Stapels in rax .

Aber ich bezweifle, dass dies messbare Kosten für die Leistung hätte; wenn überhaupt, könnte es eine Verbesserung sein.

(Die Verifizierung wäre auch anders, aber immer noch Single-Pass.)

Okay, Vorab-Einschränkungen:

  1. Dies ist kein alternatives Universum; wir bleiben dabei, abwärtskompatible Erweiterungen für die vorhandene WebAssembly zu erstellen.
  2. Der Baseline-Compiler von SpiderMonkey ist nur eine Implementierung, und es ist möglich, dass er in Bezug auf die Registerzuordnung suboptimal ist: Wenn er ein bisschen intelligenter wäre, würde der Laufzeitvorteil die Kosten für die Kompilierzeit überwiegen.
  3. Auch wenn Baseline-Compiler keine zusätzlichen Informationen benötigen, benötigen Optimierungscompiler diese möglicherweise für eine schnelle SSA-Konstruktion.

Vor diesem Hintergrund bestärkt das obige Gedankenexperiment meine Überzeugung, dass Baseline-Compiler keine strukturierte Ablaufsteuerung benötigen . Unabhängig davon, wie auf niedriger Ebene wir ein Konstrukt hinzufügen, können Baseline-Compiler es mit nur geringfügigen Änderungen verarbeiten, solange es grundlegende Informationen enthält, z. B. welche Anweisungen Sprungziele sind. Oder zumindest dieser kann.

@conrad-watt @comex

Das sind sehr gute Punkte! Meine Intuition über Baseline-Compiler kann dann durchaus falsch sein.

Und @comex - ja, wie Sie sagten, diese Diskussion ist getrennt von der Optimierung von Compilern, bei denen SSA von der Struktur profitieren kann. Vielleicht lohnt es sich, ein bisschen aus einem der Links von vorhin zu zitieren:

Designbedingt ist die Umwandlung von WebAssembly-Code in TurboFans IR (einschließlich SSA-Konstruktion) in einem einfachen Durchgang sehr effizient, teilweise aufgrund des strukturierten Kontrollflusses von WebAssembly.

@conrad-watt Ich stimme definitiv zu, dass wir nur direktes Feedback von VM-Mitarbeitern erhalten und aufgeschlossen bleiben müssen. Um es klar zu sagen, mein Ziel hier ist es nicht, irgendetwas aufzuhalten. Ich habe mich hier ausführlich geäußert, weil mehrere Kommentare der Meinung waren, der strukturierte Kontrollfluss von wasm sei ein offensichtlicher Fehler oder einer, der offensichtlich mit Funclets/Multiloop behoben werden sollte - ich wollte hier nur die Geschichte des Denkens darstellen, und es gab starke Gründe für das aktuelle Modell, daher ist es möglicherweise nicht einfach, es zu verbessern.

Ich habe dieses Gespräch sehr gerne gelesen. Ich habe mir selbst viele dieser Fragen gestellt (aus beiden Richtungen kommend) und viele dieser Gedanken geteilt (wieder aus beiden Richtungen), und die Diskussion hat viele nützliche Einsichten und Erfahrungen geliefert. Ich bin mir nicht sicher, ob ich noch eine starke Meinung habe, aber ich habe einen Gedanken, den ich in jede Richtung beitragen kann.

Auf der "für"-Seite ist es nützlich, im Voraus zu wissen, welche Blöcke Hinterkanten haben. Ein Streaming-Compiler kann Eigenschaften verfolgen, die im Typsystem von WebAssembly nicht sichtbar sind (zB liegt der Index in local i innerhalb der Grenzen des Arrays in local arr ). Beim Vorwärtsspringen kann es hilfreich sein, das Ziel mit den Eigenschaften zu versehen, die zu diesem Zeitpunkt gelten. Auf diese Weise kann, wenn ein Label erreicht wird, sein Block mit den Eigenschaften kompiliert werden, die über alle Eingangskanten hinweg gelten, um beispielsweise Array-Grenzen-Prüfungen zu eliminieren. Wenn ein Label jedoch möglicherweise einen unbekannten Hinterrand haben kann, kann sein Block mit diesem Wissen nicht kompiliert werden. Natürlich kann ein Nicht-Streaming-Compiler einige bedeutendere schleifeninvariante Analysen durchführen, aber für einen Streaming-Compiler ist es nützlich, sich keine Gedanken darüber machen zu müssen, was auf ihn zukommt. ( Nebengedanke :

Auf der „Gegen“-Seite hat sich die Diskussion bisher nur auf die lokale Kontrolle konzentriert. Für C ist das in Ordnung, aber was ist mit C++ oder verschiedenen anderen Sprachen mit ähnlichen Ausnahmen? Was ist mit Sprachen mit anderen Formen der nicht-lokalen Kontrolle? Dinge mit dynamischem Gültigkeitsbereich sind oft von Natur aus strukturiert (oder zumindest kenne ich keine Beispiele für sich gegenseitig rekursive dynamische Bereiche). Ich denke, diese Überlegungen sind ansprechbar, aber Sie müssen sie berücksichtigen, damit das Ergebnis in diesen Einstellungen verwendet werden kann. Darüber habe ich nachgedacht, und ich freue mich, meine laufenden Gedanken (die ungefähr wie eine Erweiterung von @conrad-watts Multi-Loop aussehen) mit jedem zu teilen, der interessiert ist (obwohl hier nicht zum Thema gehört), aber Ich wollte zumindest darauf hinweisen, dass es mehr als nur den lokalen Kontrollfluss zu beachten gibt.

(Ich würde auch gerne ein weiteres +1 einwerfen, um mehr von VM-Leuten zu hören, obwohl @kripken meiner Meinung geleistet hat, um die Überlegungen zu vertreten.)

Wenn ich sage, dass Lightbeam eine interne IR erzeugt, ist das wirklich ziemlich irreführend und ich hätte klarstellen sollen. Ich habe eine Weile an dem Projekt gearbeitet und manchmal kann man einen Tunnelblick bekommen. Grundsätzlich verbraucht Lightbeam die Eingabeanweisung für die Anweisung (eigentlich hat es maximal eine Anweisungs-Vorausschau, aber das ist nicht besonders wichtig) und produziert für jede Anweisung träge und in konstantem Raum eine Reihe von internen IR-Anweisungen. Die maximale Anzahl von Befehlen pro Wasm-Befehl ist konstant und klein, etwa 6. Es wird kein Puffer von IR-Befehlen für die gesamte Funktion erstellt und daran gearbeitet. Dann liest es diese IR-Anweisungen nacheinander. Sie können es sich wirklich nur als eine Bibliothek mit allgemeineren Hilfsfunktionen vorstellen, die jede Wasm-Anweisung implementiert. Es produziert wahrscheinlich nicht so schnell Code wie die Baseline-Compiler von V8 oder SpiderMonkey, aber das liegt daran, dass es nicht vollständig optimiert ist und nicht, weil es architektonisch mangelhaft ist. Mein Punkt ist, dass ich den hierarchischen Kontrollfluss von Wasm intern modelliere, als ob es ein CFG wäre, anstatt tatsächlich einen IR-Puffer im Speicher zu erzeugen, wie es bei LLVM oder Cranelift der Fall ist.

Eine andere Möglichkeit besteht darin, den Wasm zu etwas zu kompilieren, von dem Sie glauben, dass es den Kontrollfluss optimal handhaben kann, dh die Strukturierung "rückgängig" zu machen. LLVM sollte dazu in der Lage sein, daher könnte es interessant sein, wasm in einer VM auszuführen, die LLVM verwendet (wie WAVM oder wasmer) oder über WasmBoxC.

@kripken Leider scheint LLVM die Strukturierung noch nicht rückgängig zu machen. Der Jump-Threading-Optimierungsdurchgang sollte dies können, erkennt dieses Muster jedoch noch nicht. Hier ist ein Beispiel, das C++-Code zeigt, der nachahmt, wie der Relooper-Algorithmus eine CFG in eine Schleife+Switch umwandelt. GCC schafft es, es zu "dereloop", aber Clang nicht: https://godbolt.org/z/GGM9rP

@AndrewScheidecker Interessant, danke. Ja, dieses Zeug kann ziemlich unvorhersehbar sein, daher gibt es möglicherweise keine bessere Option, als den ausgegebenen Code zu untersuchen (wie das zuvor verlinkte Papier "No So Fast") und versucht, Abkürzungen wie das Verlassen auf den LLVM-Optimierer zu vermeiden.

@comex

Der Baseline-Compiler von SpiderMonkey ist nur eine Implementierung, und es ist möglich, dass er in Bezug auf die Registerzuordnung suboptimal ist: Wenn er ein bisschen intelligenter wäre, würde der Laufzeitvorteil die Kosten für die Kompilierzeit überwiegen.

Bei der Registerzuordnung könnte es eindeutig klüger sein. Es verschüttet sich unterschiedslos bei Kontrollflussgabeln, Joins und vor Aufrufen und könnte mehr Informationen über den Registerstatus speichern und versuchen, Werte länger / bis sie tot sind, in Registern zu halten. Es könnte ein besseres Register als rax für Wertergebnisse aus Blöcken auswählen oder besser kein festes Register verwenden. Es könnte ein paar Register statisch zuweisen, um lokale Variablen zu speichern; Eine von mir durchgeführte Korpusanalyse ergab, dass für die meisten Funktionen nur wenige Integer- und FP-Register ausreichen. Es könnte im Allgemeinen klüger sein, etwas zu verschütten; So wie es ist, verschüttet es alles in Panik, wenn die Register ausgehen.

Die Kosten für die Kompilierzeit hiervon bestehen hauptsächlich darin, dass jeder Kontrollflusskante eine nicht konstante Menge an Informationen zugeordnet ist (der Registerzustand), und dies kann zu einer umfassenderen Verwendung der dynamischen Speicherzuweisung führen, die der Basiskompilierer so hat weit gemieden. Und natürlich sind mit der Verarbeitung dieser Informationen variabler Größe bei jedem Join (und an anderen Stellen) Kosten verbunden. Aber es gibt bereits einige nicht konstante Kosten, da der Registerzustand durchlaufen werden muss, um Spill-Code zu generieren, und im Großen und Ganzen kann es nur wenige Werte geben, also kann dies in Ordnung sein (oder nicht). Natürlich kann es sich bei modernen Chips mit ihren schnellen Caches und der ooo-Ausführung auszahlen oder nicht, mit dem regalloc klüger zu sein ...

Ein subtilerer Kostenfaktor ist die Wartbarkeit des Compilers ... er ist bereits ziemlich komplex, und da er in einem Durchgang arbeitet und keinen IR-Graphen erstellt oder überhaupt dynamischen Speicher verwendet, ist er resistent gegen Schichtung und Abstraktion.

@RossTate

Was Funclets / Gotos betrifft, habe ich neulich die Funclet-Spezifikation überflogen und auf den ersten Blick sah es nicht so aus, als ob ein One-Pass-Compiler wirklich Probleme damit haben sollte, schon gar nicht mit einem vereinfachenden Regalloc-Schema. Aber selbst mit einem besseren Schema könnte es in Ordnung sein: Die erste Kante, die einen Join-Punkt erreicht, würde entscheiden, wie die Registerzuweisung lautet, und andere Kanten müssten konform sein.

@conrad-watt wie Sie gerade im CG-Meeting erwähnt haben, wären wir sehr daran interessiert, Details darüber zu erfahren, wie Ihr Multi-Loop aussehen würde.

@aardappel ja, das Leben hat mich schnell @rossberg sie ursprünglich als Reaktion auf den ersten Entwurf von Funklets entworfen hat.

Eine Referenz, die aufschlussreich sein könnte, ist etwas veraltet, verallgemeinert jedoch bekannte Konzepte von Schleifen, um mit DJ-Graphen mit irreduziblen

Wir hatten einige Diskussionsrunden darüber in der CG, und ich habe eine Zusammenfassung und ein Folgedokument verfasst. Wegen der Länge habe ich es zu einem eigenen Inhalt gemacht.

https://gist.github.com/conrad-watt/6a620cb8b7d8f0191296e3eb24dffdef

Ich denke, die beiden unmittelbar umsetzbaren Fragen (weitere Informationen finden Sie im Abschnitt zur Nachbereitung) sind:

  • Können wir "wilde" Programme finden, die derzeit leiden und leistungsmäßig von multiloop profitieren würden? Dies können Programme sein, für die LLVM-Transformationen einen irreduziblen Kontrollfluss einführen, selbst wenn dieser im Quellprogramm nicht vorhanden ist.
  • Gibt es eine Welt, in der multiloop Herstellerseite implementiert wird, mit einer Verknüpfungs-/Übersetzungs-Bereitstellungsschicht für "Web" Wasm?

Es gibt wahrscheinlich auch eine freizügigere Diskussion über die Konsequenzen der Ausnahmebehandlungsprobleme, die ich im Folgedokument bespreche, und natürlich die übliche Fahrradabschottung über semantische Details, wenn wir mit etwas Konkretem vorankommen.

Da diese Diskussionen etwas verzweigen können, kann es angebracht sein, einige von ihnen in Issues im Funclets- Repository zu integrieren.

Ich freue mich sehr über die Fortschritte bei diesem Thema. Ein großes "Dankeschön" an alle Beteiligten!

Können wir "wilde" Programme finden, die derzeit leiden und leistungsmäßig von Multiloop profitieren würden? Dies können Programme sein, für die LLVM-Transformationen einen irreduziblen Kontrollfluss einführen, selbst wenn dieser im Quellprogramm nicht vorhanden ist.

Ich möchte ein wenig vor Zirkelschluss warnen: Programme, die derzeit eine schlechte Performance aufweisen, treten aus genau diesem Grund seltener "in the wild" auf.

Ich denke, die meisten Go-Programme sollten viel davon profitieren. Der Go-Compiler benötigt entweder WebAssembly-Coroutinen oder multiloop , um effizienten Code ausgeben zu können, der Gos Goroutinen unterstützt.

Vorkompilierte Matcher für reguläre Ausdrücke führen zusammen mit anderen vorkompilierten Zustandsmaschinen oft zu einem irreduziblen Kontrollfluss. Es ist schwer zu sagen, ob der "Fusion"-Algorithmus für Schnittstellentypen zu einem irreduziblen Kontrollfluss führt oder nicht.

  • Stimmen Sie zu, dass diese Diskussion auf Probleme in den Funklets (oder einem neuen) Repository verschoben werden sollte.
  • Stimmen Sie zu, dass es schwer zu quantifizieren ist, Programme zu finden, die davon profitieren würden, ohne dass LLVM (und Go und andere) tatsächlich den optimalsten Kontrollfluss ausgeben (der nicht reduzierbar sein kann). Die Ineffizienz, die durch FixIrreducibleControlFlow und Freunde verursacht wird, kann ein "Tod durch tausend Schnitte"-Problem in einer großen Binärdatei sein.
  • Ich würde zwar eine reine Tools-Implementierung als absoluten Mindestfortschritt aus dieser Diskussion begrüßen, aber es wäre immer noch nicht optimal, da Hersteller jetzt die schwierige Wahl haben, diese Funktionalität aus Bequemlichkeitsgründen zu nutzen (aber dann mit unvorhersehbaren Leistungsrückgängen konfrontiert sind/ Klippen) oder machen sich die harte Arbeit, ihren Output in Standard-Wasm zu verwandeln, damit die Dinge vorhersehbar sind.
  • Wenn entschieden würde, dass "gotos" bestenfalls ein Tool-only-Feature ist, würde ich argumentieren, dass Sie wahrscheinlich mit einem noch einfacheren Feature als Multiloop davonkommen könnten, da Sie sich nur um die Bequemlichkeit des Produzenten kümmern. Als absolutes Minimum wäre ein goto <function_byte_offset> das einzige, was in reguläre Wasm-Funktionskörper eingefügt werden müsste, damit WABT oder Binaryen es in legales Wasm umwandeln können. Dinge wie Typsignaturen sind nützlich, wenn Engines eine Multiloop schnell verifizieren müssen, aber wenn es sich um ein praktisches Werkzeug handelt, können sie das Ausgeben genauso gut machen.

Stimmen Sie zu, dass es schwer zu quantifizieren ist, Programme zu finden, die davon profitieren würden, ohne dass LLVM (und Go und andere) tatsächlich den optimalsten Kontrollfluss ausgeben (der nicht reduzierbar sein kann).

Ich stimme zu, dass das Testen von modifizierten Toolchains + VMs optimal wäre. Aber wir können aktuelle Wasm-Builds mit nativen Builds vergleichen, die einen optimalen Kontrollfluss haben. Not So Fast und andere haben dies auf verschiedene Weise untersucht (Leistungsindikatoren, direkte Untersuchung) und haben keinen nicht reduzierbaren Kontrollfluss als signifikanten Faktor festgestellt.

Genauer gesagt, fanden sie es nicht als signifikanten Faktor für C/C++. Das hat möglicherweise mehr mit C/C++ zu tun als mit der Leistung des irreduziblen Kontrollflusses. (Ich weiß es ehrlich gesagt nicht.) Es hört sich so an, als hätte @neelance Grund zu der Annahme, dass das Gleiche für Go nicht gelten würde.

Meiner Meinung nach hat dieses Problem mehrere Facetten und es lohnt sich, es in mehreren Richtungen anzugehen.

Erstens scheint es ein allgemeines Problem mit der Generierbarkeit von WebAssembly zu geben. Vieles davon wird durch die Einschränkung von WebAssembly verursacht, eine kompakte Binärdatei mit effizienter Typprüfung und Streaming-Kompilierung zu haben. Wir könnten dieses Problem zumindest teilweise lösen, indem wir eine standardisierte "Prä"-WebAssembly entwickeln, die einfacher zu generieren ist, aber garantiert in "echte" WebAssembly übersetzbar ist, idealerweise nur durch Codeduplizierung und Einfügen von "löschbaren" Anweisungen/Anmerkungen, mit zumindest einem Werkzeug, das eine solche Übersetzung bereitstellt.

Zweitens können wir überlegen, welche Funktionen von "Prä"-WebAssembly es wert sind, direkt in "echtes" WebAssembly integriert zu werden. Wir können dies auf informierte Weise tun, weil wir "Pre"-WebAssembly-Module haben werden, die wir analysieren können, bevor sie in "echte" WebAssembly-Module verzerrt wurden.

Vor einigen Jahren habe ich versucht, einen bestimmten Bytecode-Emulator für eine dynamische Sprache (https://github.com/ciao-lang/ciao) für die Webassembly zu kompilieren, und die Leistung war alles andere als optimal (manchmal 10 Mal langsamer als die native Version). Die Hauptausführungsschleife enthielt einen großen Bytecode-Dispatch-Schalter, und die Engine wurde jahrzehntelang fein abgestimmt, um auf echter Hardware zu laufen, und wir machen intensiven Gebrauch von Labels und Gotos. Ich frage mich, ob diese Art von Software von der Unterstützung für irreduziblen Kontrollfluss profitieren würde oder ob das Problem ein anderes war. Ich hatte keine Zeit, weitere Nachforschungen anzustellen, aber ich würde es gerne noch einmal versuchen, wenn bekannt ist, dass sich die Dinge verbessert haben. Natürlich verstehe ich, dass das Kompilieren von VMs in anderen Sprachen nach wasm nicht der Hauptanwendungsfall ist, aber ich wäre gut zu wissen, ob dies irgendwann machbar ist, zumal universelle Binärdateien, die überall effizient laufen, einer der versprochenen Vorteile von . sind wasm. (Danke und Entschuldigung, falls dieses spezielle Thema in einer anderen Ausgabe behandelt wurde)

@jfmc Mein Verständnis ist, dass, wenn das Programm realistisch ist (dh nicht erfunden wurde, um pathologisch zu sein) und Sie sich um seine Leistung kümmern, es ein absolut gültiger Anwendungsfall ist. WebAssembly soll ein gutes Allzweckziel sein. Ich denke, es wäre großartig, zu verstehen, warum Sie eine so erhebliche Verlangsamung gesehen haben. Wenn dies auf Einschränkungen des Kontrollflusses zurückzuführen ist, wäre es sehr nützlich, dies in dieser Diskussion zu wissen. Wenn es an etwas anderem liegt, wäre es dennoch nützlich zu wissen, wie WebAssembly im Allgemeinen verbessert werden kann.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

thysultan picture thysultan  ·  4Kommentare

mfateev picture mfateev  ·  5Kommentare

void4 picture void4  ·  5Kommentare

aaabbbcccddd00001111 picture aaabbbcccddd00001111  ·  3Kommentare

badumt55 picture badumt55  ·  8Kommentare