Warum gibt es in Roslyn asynchrone Zustandsmaschinenklassen (und keine Strukturen)?

86

Betrachten wir diese sehr einfache asynchrone Methode:

static async Task myMethodAsync() 
{
    await Task.Delay(500);
}

Wenn ich dies mit VS2013 (Pre Roslyn Compiler) kompiliere, ist die generierte Zustandsmaschine eine Struktur.

private struct <myMethodAsync>d__0 : IAsyncStateMachine
{  
    ...
    void IAsyncStateMachine.MoveNext()
    {
        ...
    }
}

Wenn ich es mit VS2015 (Roslyn) kompiliere, lautet der generierte Code wie folgt:

private sealed class <myMethodAsync>d__1 : IAsyncStateMachine
{
    ...
    void IAsyncStateMachine.MoveNext()
    {
        ...
    }
}

Wie Sie sehen können, generiert Roslyn eine Klasse (und keine Struktur). Wenn ich mich richtig erinnere, haben die ersten Implementierungen der Unterstützung für asynchrone / warten im alten Compiler (CTP2012, denke ich) auch Klassen generiert, und dann wurde sie aus Leistungsgründen in struct geändert. (In einigen Fällen können Sie das Boxen und die Heap-Zuweisung vollständig vermeiden…) (Siehe hier )

Weiß jemand, warum dies in Roslyn wieder geändert wurde? (Ich habe kein Problem damit, ich weiß, dass diese Änderung transparent ist und das Verhalten von Code nicht ändert, ich bin nur neugierig)

Bearbeiten:

Die Antwort von @Damien_The_Unbeliever (und dem Quellcode :)) imho erklärt alles. Das beschriebene Verhalten von Roslyn gilt nur für den Debug-Build (und dies ist aufgrund der im Kommentar erwähnten CLR-Einschränkung erforderlich). In Release wird auch eine Struktur generiert (mit allen Vorteilen davon ..). Dies scheint also eine sehr clevere Lösung zu sein, um sowohl Bearbeiten als auch Fortfahren und eine bessere Leistung in der Produktion zu unterstützen. Interessantes, danke für alle, die teilgenommen haben!

gregkalapos
quelle
2
Ich vermute, dass sie entschieden haben, dass sich die Komplexität (wiederveränderliche Strukturen) nicht gelohnt hat. asyncMethoden haben fast immer einen echten asynchronen Punkt - einen await, der eine Kontrolle ergibt, für die die Struktur ohnehin eingerahmt werden müsste. Ich glaube, Strukturen würden den Speicherdruck nur für asyncMethoden verringern , die zufällig synchron ausgeführt wurden.
Stephen Cleary

Antworten:

111

Ich hatte keine Vorkenntnisse darüber, aber da Roslyn heutzutage Open Source ist, können wir den Code nach einer Erklärung durchsuchen.

Und hier in Zeile 60 des AsyncRewriter finden wir:

// The CLR doesn't support adding fields to structs, so in order to enable EnC in an async method we need to generate a class.
var typeKind = compilationState.Compilation.Options.EnableEditAndContinue ? TypeKind.Class : TypeKind.Struct;

Obwohl die Verwendung von structs eine gewisse Anziehungskraft hat, wurde der große Gewinn, Edit und Continue innerhalb von asyncMethoden arbeiten zu lassen, offensichtlich als die bessere Option gewählt.

Damien_The_Unbeliever
quelle
18
Sehr guter Fang! Und basierend darauf habe ich hier auch Folgendes entdeckt: Dies geschieht nur, wenn Sie es im Debug erstellen (sinnvoll, wenn Sie EnC ausführen ..), aber in Release erstellen sie eine Struktur (offensichtlich ist EnableEditAndContinue in diesem Fall falsch. .). Übrigens. Ich habe auch versucht, in den Code zu schauen, habe dies aber nicht gefunden. Danke vielmals!
Gregkalapos
3

Es ist schwer, eine endgültige Antwort auf so etwas zu geben (es sei denn, jemand aus dem Compilerteam kommt vorbei :)), aber es gibt ein paar Punkte, die Sie berücksichtigen können:

Der Leistungsbonus von Strukturen ist immer ein Kompromiss. Grundsätzlich erhalten Sie Folgendes:

  • Wertesemantik
  • Mögliche Stapelzuordnung (vielleicht sogar Registrierung?)
  • Indirektion vermeiden

Was bedeutet das im Wartefall? Naja eigentlich ... nichts. Es gibt nur einen sehr kurzen Zeitraum, in dem sich die Zustandsmaschine auf dem Stapel befindet. Denken Sie daran, dass a awaiteffektiv ausgeführt wird return, sodass der Methodenstapel stirbt. Die Zustandsmaschine muss irgendwo aufbewahrt werden, und das "irgendwo" ist definitiv auf dem Haufen. Die Stapellebensdauer passt nicht gut zu asynchronem Code :)

Abgesehen davon verstößt die Zustandsmaschine gegen einige gute Richtlinien zum Definieren von Strukturen:

  • structs sollte höchstens 16 Byte groß sein - die Zustandsmaschine enthält zwei Zeiger, die allein die 16-Byte-Grenze für 64-Bit sauber ausfüllen. Abgesehen davon gibt es den Staat selbst, so dass er die "Grenze" überschreitet. Das ist kein großer Sache, da es höchstwahrscheinlich immer nur als Referenz übergeben wird. Beachten Sie jedoch, dass dies nicht ganz zum Anwendungsfall für Strukturen passt - eine Struktur, die im Grunde genommen ein Referenztyp ist.
  • structs sollte unveränderlich sein - nun, das braucht wahrscheinlich keinen großen Kommentar. Es ist eine Zustandsmaschine . Auch dies ist keine große Sache, da die Struktur automatisch generierter Code und privat ist, aber ...
  • structs sollte logisch einen einzelnen Wert darstellen. Dies ist hier definitiv nicht der Fall, aber das ergibt sich bereits aus einem veränderlichen Zustand.
  • Es sollte nicht häufig verpackt werden - hier kein Problem, da wir überall Generika verwenden . Der Zustand befindet sich letztendlich irgendwo auf dem Haufen, aber zumindest wird er nicht (automatisch) geboxt. Auch hier macht die Tatsache, dass es nur intern verwendet wird, dies ziemlich ungültig.

Und das alles natürlich in einem Fall, in dem es keine Schließungen gibt. Wenn Sie Einheimische (oder Felder) haben, die das awaits durchlaufen , wird der Status weiter aufgeblasen, was die Nützlichkeit der Verwendung einer Struktur einschränkt.

Angesichts all dessen ist der Klassenansatz definitiv sauberer, und ich würde keine merkliche Leistungssteigerung erwarten, wenn ich structstattdessen a verwende. All beteiligten Objekte haben ähnliche Lebensdauer, so dass der einzige Weg , die Gedächtnisleistung zu verbessern wäre , um alle von ihnen structs (Speicher in einem gewissen Puffer, zum Beispiel) - die im allgemeinen Fall natürlich unmöglich ist. Und die meisten Fälle, in denen Sie verwenden würdenawait zuerst verwenden würden (dh einige asynchrone E / A-Arbeiten), betreffen bereits andere Klassen - zum Beispiel Datenpuffer, Zeichenfolgen ... Es ist eher unwahrscheinlich, dass Sie awaitetwas 42tun , das einfach zurückkehrt, ohne etwas zu tun Heap-Zuweisungen.

Am Ende würde ich sagen, dass der einzige Ort, an dem Sie wirklich einen echten Leistungsunterschied sehen würden, Benchmarks sind. Und die Optimierung für Benchmarks ist, gelinde gesagt, eine dumme Idee ...

Luaan
quelle
Man muss nicht immer brauchen ein Mitglied des Compiler - Team , wenn Sie die Quelle gehen und lesen können, und sie haben einen hilfreichen Kommentar verfasst :-)
Damien_The_Unbeliever
3
@ Damien_The_Unbeliever Ja, das war definitiv ein großartiger Fund, ich habe Ihre Antwort bereits positiv bewertet: P
Luaan
1
Die Struktur hilft sehr, wenn der Code nicht asynchron ausgeführt wird, z. B. wenn sich die Daten bereits in einem Puffer befinden.
Ian Ringrose