Perché in Roslyn sono presenti classi di macchine a stati asincrone (e non strutture)?


87

Consideriamo questo metodo asincrono molto semplice:

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

Quando lo compilo con VS2013 (compilatore pre Roslyn), la macchina a stati generata è una struttura.

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

Quando lo compilo con VS2015 (Roslyn) il codice generato è questo:

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

Come puoi vedere Roslyn genera una classe (e non una struttura). Se ricordo bene le prime implementazioni del supporto async / await nel vecchio compilatore (CTP2012 immagino) hanno anche generato classi e poi è stato cambiato in struct per motivi di prestazioni. (in alcuni casi puoi evitare completamente la boxe e l'allocazione dell'heap ...) (Vedi questo )

Qualcuno sa perché questo è stato cambiato di nuovo a Roslyn? (Non ho alcun problema al riguardo, so che questa modifica è trasparente e non cambia il comportamento di nessun codice, sono solo curioso)

Modificare:

La risposta di @Damien_The_Unbeliever (e il codice sorgente :)) imho spiega tutto. Il comportamento descritto di Roslyn si applica solo per la build di debug (ed è necessario a causa della limitazione CLR menzionata nel commento). In Release genera anche una struttura (con tutti i vantaggi di ciò ..). Quindi questa sembra essere una soluzione molto intelligente per supportare sia Modifica che Continua e prestazioni migliori in produzione. Roba interessante, grazie per tutti coloro che hanno partecipato!


2
Sospetto che abbiano deciso che la complessità (strutture modificabili) non ne valeva la pena. asynci metodi hanno quasi sempre un vero punto asincrono, awaitche fornisce il controllo, il che richiederebbe comunque che la struttura sia boxata. Credo che le strutture allevierebbero la pressione della memoria solo per i asyncmetodi che si sono verificati in modo sincrono.
Stephen Cleary

Risposte:


112

Non lo sapevo in anticipo, ma dato che Roslyn è open source in questi giorni, possiamo andare a cercare nel codice una spiegazione.

E qui, sulla riga 60 dell'AsyncRewriter , troviamo:

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

Quindi, sebbene l'uso di structs sia interessante, la grande vittoria di consentire a Modifica e Continua di lavorare all'interno dei asyncmetodi è stata ovviamente scelta come l'opzione migliore.


18
Cattura molto buona! E sulla base di questo ecco quello che ho scoperto anche io: questo accade solo quando lo costruisci in debug (ha senso, è quando fai EnC ..), ma in Release creano una struttura (ovviamente EnableEditAndContinue è falso in quel caso .. .). Btw. Ho anche provato a esaminare il codice, ma non l'ho trovato. Grazie molto!
gregkalapos

3

È difficile dare una risposta definitiva per qualcosa di simile (a meno che qualcuno del team del compilatore non intervenga :)), ma ci sono alcuni punti che puoi considerare:

Il "bonus" delle prestazioni degli struct è sempre un compromesso. Fondamentalmente, ottieni quanto segue:

  • Semantica dei valori
  • Possibile allocazione dello stack (forse anche registro?)
  • Evitare l'indirizzamento

Cosa significa questo nel caso in attesa? Beh, in realtà ... niente. C'è solo un periodo di tempo molto breve durante il quale la macchina a stati è in pila - ricorda,await fa effettivamente a return, quindi lo stack del metodo muore; la macchina a stati deve essere conservata da qualche parte, e quel "da qualche parte" è sicuramente sul mucchio. La durata dello stack non si adatta bene al codice asincrono :)

Oltre a questo, la macchina a stati viola alcune buone linee guida per la definizione di strutture:

  • structI file dovrebbero avere una dimensione massima di 16 byte: la macchina a stati contiene due puntatori, che da soli riempiono il limite di 16 byte in modo ordinato su 64 bit. A parte questo, c'è lo stato stesso, quindi va oltre il "limite". Questo non è un grande problema, poiché è molto probabile che sia passato solo per riferimento, ma nota come ciò non si adatta perfettamente al caso d'uso per le strutture, una struttura che è fondamentalmente un tipo di riferimento.
  • structs dovrebbe essere immutabile - beh, questo probabilmente non ha bisogno di molti commenti. È una macchina a stati . Di nuovo, questo non è un grosso problema, poiché la struttura è un codice generato automaticamente e privato, ma ...
  • structs dovrebbe logicamente rappresentare un singolo valore. Sicuramente non è il caso qui, ma già questo deriva dall'avere uno stato mutevole in primo luogo.
  • Non dovrebbe essere inscatolato frequentemente - non è un problema qui, dato che usiamo generici ovunque . Lo stato è in definitiva da qualche parte sul mucchio, ma almeno non viene inscatolato (automaticamente). Ancora una volta, il fatto che sia usato solo internamente rende questo praticamente vuoto.

E, naturalmente, tutto questo è in un caso in cui non ci sono chiusure. Quando si hanno locals (o campi) che attraversano la awaits, lo stato viene ulteriormente gonfiato, limitando l'utilità di usare una struttura.

Considerato tutto ciò, l'approccio di classe è decisamente più pulito e non mi aspetterei alcun notevole aumento delle prestazioni dall'utilizzo di a struct. Tutti gli oggetti interessati hanno durata simile, quindi l'unico modo per migliorare le prestazioni della memoria sarebbe quello di rendere tutti loro structs (conservare in qualche tampone, per esempio) - che è impossibile nel caso generale, naturalmente. E la maggior parte dei casi in cui useresti awaitin primo luogo (cioè, un po 'di lavoro di I / O asincrono) coinvolge già altre classi - ad esempio, buffer di dati, stringhe ... È piuttosto improbabile che tu possa fare awaitqualcosa che semplicemente ritorna 42senza fare alcun allocazioni heap.

Alla fine, direi che l'unico posto in cui vedresti davvero una reale differenza di prestazioni sarebbero i benchmark. E l'ottimizzazione per i benchmark è un'idea sciocca, per non dire altro ...


Non hai sempre bisogno di un membro del team del compilatore quando puoi andare a leggere il sorgente, e hanno lasciato un commento utile :-)
Damien_The_Unbeliever

3
@Damien_The_Unbeliever Sì, è stata sicuramente un'ottima scoperta, ho già votato positivamente la tua risposta: P
Luaan

1
La struttura aiuta molto nel caso in cui il codice non venga eseguito in modo asincrono, ad esempio i dati sono già in un buffer.
Ian Ringrose
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.