web-dev-qa-db-fra.com

Pourquoi les classes de machines à états asynchrones (et non les structures) sont-elles à Roslyn?

Considérons cette méthode asynchrone très simple:

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

Quand je compile ceci avec VS2013 (compilateur pré Roslyn), la machine à états générée est une structure.

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

Lorsque je le compile avec VS2015 (Roslyn), le code généré est le suivant:

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

Comme vous pouvez le voir, Roslyn génère une classe (et non une structure). Si je me souviens bien, les premières implémentations de la prise en charge async/wait dans l'ancien compilateur (CTP2012 je suppose) ont également généré des classes, puis elles ont été modifiées en struct pour des raisons de performances. (dans certains cas, vous pouvez éviter complètement la boxe et l'allocation de tas…) (Voir this )

Est-ce que quelqu'un sait pourquoi cela a encore changé à Roslyn? (Je n'ai aucun problème à ce sujet, je sais que ce changement est transparent et ne change pas le comportement d'un code, je suis juste curieux)

Modifier:

La réponse de @Damien_The_Unbeliever (et le code source :)) à mon humble avis explique tout. Le comportement décrit de Roslyn ne s'applique qu'à la version de débogage (et cela est nécessaire en raison de la limitation CLR mentionnée dans le commentaire). Dans Release, il génère également une structure (avec tous les avantages de cela ..). Cela semble donc être une solution très intelligente pour prendre en charge à la fois les modifications et la poursuite et de meilleures performances en production. Des trucs intéressants, merci pour tous ceux qui ont participé!

86
gregkalapos

Je n'avais aucune connaissance préalable de cela, mais comme Roslyn est open-source ces jours-ci, nous pouvons aller chercher le code pour une explication.

Et ici, sur ligne 60 de l'AsyncRewriter , on trouve:

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

Donc, bien qu'il y ait un certain attrait à utiliser structs, la grande victoire de permettre à Edit and Continue de fonctionner avec les méthodes async a évidemment été choisie comme la meilleure option.

110

Il est difficile de donner une réponse définitive à quelque chose comme ça (à moins que quelqu'un de l'équipe du compilateur ne vienne :)), mais il y a quelques points que vous pouvez considérer:

Le "bonus" de performance des structures est toujours un compromis. Fondamentalement, vous obtenez les éléments suivants:

  • Sémantique des valeurs
  • Allocation possible de pile (peut-être même de s'inscrire?)
  • Éviter l'indirection

Qu'est-ce que cela signifie dans le cas d'attente? Eh bien, en fait ... rien. Il n'y a qu'une très courte période pendant laquelle la machine d'état est sur la pile - rappelez-vous, await fait effectivement un return, donc la pile de méthode meurt; la machine d'état doit être préservée quelque part, et ce "quelque part" est définitivement sur le tas. La durée de vie de la pile ne correspond pas bien au code asynchrone :)

En dehors de cela, la machine d'état viole quelques bonnes directives pour définir des structures:

  • structs doit être d'au plus 16 octets - la machine d'état contient deux pointeurs, qui à eux seuls remplissent parfaitement la limite de 16 octets sur 64 bits. En dehors de cela, il y a l'État lui-même, donc il dépasse la "limite". Ce n'est pas une affaire gros, car il est très probable qu'il ne soit jamais transmis que par référence, mais notez comment cela ne correspond pas tout à fait au cas d'utilisation des structures - une structure qui est essentiellement un type de référence.
  • structs devrait être immuable - eh bien, cela n'a probablement pas besoin de beaucoup de commentaires. C'est un machine d'état. Encore une fois, ce n'est pas un gros problème, car la structure est du code généré automatiquement et privé, mais ...
  • structs devrait logiquement représenter une seule valeur. Ce n'est certainement pas le cas ici, mais cela découle déjà d'un état mutable en premier lieu.
  • Il ne devrait pas être encadré fréquemment - ce n'est pas un problème ici, car nous utilisons des génériques partout. L'état est finalement quelque part sur le tas, mais au moins il n'est pas encadré (automatiquement). Encore une fois, le fait qu'il ne soit utilisé qu'en interne rend cela à peu près nul.

Et bien sûr, tout cela est dans un cas où il n'y a pas de fermetures. Lorsque vous avez des sections locales (ou des champs) qui traversent les awaits, l'état est encore gonflé, ce qui limite l'utilité d'utiliser une structure.

Compte tenu de tout cela, l'approche de classe est définitivement plus propre, et je ne m'attendrais pas à une augmentation notable des performances en utilisant à la place un struct. Tous les objets impliqués ont une durée de vie similaire, donc la seule façon d'améliorer les performances de la mémoire serait d'en faire tousstructs (stocker dans un tampon, par exemple) - ce qui est impossible dans le cas général, bien sûr. Et la plupart des cas où vous utiliseriez await en premier lieu (c'est-à-dire certains travaux d'E/S asynchrones) impliquent déjà d'autres classes - par exemple, des tampons de données, des chaînes ... Il est plutôt peu probable que vous await quelque chose qui renvoie simplement 42 sans faire d'allocation de tas.

En fin de compte, je dirais que le seul endroit où vous verriez vraiment une réelle différence de performance serait les références. Et l'optimisation des repères est une idée idiote, pour dire le moins ...

3
Luaan