Pourquoi existe-t-il des classes de machines d'état asynchrones (et non des structures) dans Roslyn?

87

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

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

Lorsque je compile ceci avec VS2013 (pré-compilateur 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 du support async / await 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 des tas ...) (Voir ce )

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

Éditer:

La réponse de @Damien_The_Unbeliever (et le code source :)) à mon humble avis explique tout. Le comportement décrit de Roslyn s'applique uniquement à 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 Edit et Continue et de meilleures performances en production. Des choses intéressantes, merci à tous ceux qui ont participé!

Gregkalapos
la source
2
Je soupçonne qu'ils ont décidé que la complexité (structures modifiables) n'en valait pas la peine. asyncles méthodes ont presque toujours un vrai point asynchrone - un awaitqui donne le contrôle, ce qui exigerait de toute façon que la structure soit encadrée. Je crois que les structs soulageraient uniquement la pression de la mémoire pour les asyncméthodes qui s'exécutaient de manière synchrone.
Stephen Cleary

Réponses:

112

Je n'avais aucune pré-connaissance à ce sujet, mais comme Roslyn est open-source ces jours-ci, nous pouvons parcourir le code pour une explication.

Et ici, à la ligne 60 d'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;

Ainsi, bien qu'il y ait un certain intérêt à utiliser structs, le grand avantage de permettre à Edit and Continue de fonctionner avec des asyncméthodes a été évidemment choisi comme la meilleure option.

Damien_The_Unbeliever
la source
18
Très bonne prise! Et sur cette base, voici ce que j'ai également découvert: cela ne se produit que lorsque vous le construisez en débogage (c'est logique, c'est à ce moment-là que vous faites EnC ..), mais dans Release, ils créent une structure (évidemment EnableEditAndContinue est faux dans ce cas ... .). Btw. J'ai également essayé de regarder dans le code, mais je ne l'ai pas trouvé. Merci beaucoup!
gregkalapos
3

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

Le «bonus» de performance des structures est toujours un compromis. En gros, vous obtenez ce qui suit:

  • Sémantique des valeurs
  • Allocation possible de la pile (peut-être même du registre?)
  • É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 à états est sur la pile - rappelez-vous, fait awaiteffectivement a return, donc la pile de méthodes 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 convient pas bien au code asynchrone :)

En dehors de cela, la machine à états enfreint certaines bonnes directives pour définir les structures:

  • structs doit avoir une taille maximale de 16 octets - la machine à états 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 un gros problème, car il est fort probable qu'il ne soit jamais passé que par référence, mais notez que 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 une 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 valeur unique. 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 emballé fréquemment - pas de 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 vide.

Et bien sûr, tout cela est dans un cas où il n'y a pas de fermeture. Lorsque vous avez des locals (ou des champs) qui traversent le 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 structplace un . Tous les objets impliqués ont la vie même, la seule façon d'améliorer les performances de la mémoire serait de faire tout leur structs (magasin dans une mémoire tampon, par exemple) - ce qui est impossible dans le cas général, bien sûr. Et la plupart des cas où vous utiliseriez awaiten 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 improbable que vous fassiez awaitquelque chose qui renvoie simplement 42sans rien faire. allocations 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 repères. Et optimiser pour les benchmarks est une idée idiote, c'est le moins qu'on puisse dire ...

Luaan
la source
Vous n'avez pas toujours besoin d' un membre de l'équipe du compilateur lorsque vous pouvez aller lire la source, et ils ont laissé un commentaire utile :-)
Damien_The_Unbeliever
3
@Damien_The_Unbeliever Ouais, c'était vraiment une excellente trouvaille, j'ai déjà voté pour votre réponse: P
Luaan
1
La structure aide beaucoup dans le cas où le code ne s'exécute pas de manière asynchrone, par exemple les données sont déjà dans un tampon.
Ian Ringrose