Dlaczego w Roslyn znajdują się klasy maszyn stanu asynchronicznego (a nie struktury)?

87

Rozważmy tę bardzo prostą metodę asynchroniczną:

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

Kiedy kompiluję to za pomocą VS2013 (kompilator sprzed Roslyn), wygenerowana maszyna stanu jest strukturą.

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

Kiedy kompiluję go z VS2015 (Roslyn), wygenerowany kod wygląda tak:

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

Jak widać Roslyn generuje klasę (a nie strukturę). Jeśli dobrze pamiętam, pierwsze implementacje async / await w starym kompilatorze (chyba CTP2012) również generowały klasy, a następnie ze względów wydajnościowych zostało zmienione na struct. (w niektórych przypadkach można całkowicie uniknąć boksowania i alokacji sterty…) (Zobacz to )

Czy ktoś wie, dlaczego zmieniono to ponownie w Roslyn? (Nie mam z tym problemu, wiem że ta zmiana jest przejrzysta i nie zmienia zachowania żadnego kodu, jestem po prostu ciekawy)

Edytować:

Odpowiedź od @Damien_The_Unbeliever (i kod źródłowy :)) imho wyjaśnia wszystko. Opisane zachowanie Roslyn dotyczy tylko kompilacji debugowania (i jest to potrzebne ze względu na ograniczenie środowiska CLR wspomniane w komentarzu). W wersji Release generuje również strukturę (ze wszystkimi tego korzyściami ...). Wydaje się więc, że jest to bardzo sprytne rozwiązanie obsługujące zarówno Edycję, jak i Kontynuuj oraz lepszą wydajność w produkcji. Ciekawe rzeczy, dziękujemy wszystkim, którzy wzięli udział!

gregkalapos
źródło
2
Podejrzewam, że zdecydowali, że złożoność (re mutowalne struktury) nie była tego warta. asyncmetody prawie zawsze mają prawdziwy punkt asynchroniczny - awaitktóry zapewnia kontrolę, która i tak wymagałaby zapakowania struktury. Uważam, że struktury zmniejszyłyby obciążenie pamięci tylko w przypadku asyncmetod, które działały synchronicznie.
Stephen Cleary

Odpowiedzi:

112

Nie miałem żadnej wiedzy na ten temat, ale ponieważ Roslyn jest obecnie open-source, możemy poszukać wyjaśnienia w kodzie.

A tutaj, w linii 60 AsyncRewriter , znajdujemy:

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

Tak więc, chociaż użycie structs jest atrakcyjne , to duża korzyść, jaką daje zezwolenie Edycji i Kontynuuj na pracę w ramach asyncmetod, została oczywiście wybrana jako lepsza opcja.

Damien_The_Unbeliever
źródło
18
Bardzo dobry chwyt! I na tej podstawie odkryłem to, co również odkryłem: Dzieje się tak tylko wtedy, gdy budujesz go w debugowaniu (ma to sens, wtedy robisz EnC ..), ale w wersji Release tworzą strukturę (oczywiście EnableEditAndContinue jest w tym przypadku fałszywe. .). Przy okazji. Próbowałem też zajrzeć do kodu, ale nie znalazłem tego. Wielkie dzięki!
gregkalapos
3

Trudno udzielić ostatecznej odpowiedzi na coś takiego (chyba, że ​​wpadnie ktoś z zespołu kompilatorów :)), ale jest kilka punktów, które możesz wziąć pod uwagę:

Premia za wydajność struktur jest zawsze kompromisem. Zasadniczo otrzymujesz:

  • Semantyka wartości
  • Możliwa alokacja stosu (może nawet rejestru?)
  • Unikanie pośrednictwa

Co to oznacza w przypadku czekania? Właściwie ... nic. Jest tylko bardzo krótki okres czasu, w którym maszyna stanu znajduje się na stosie - pamiętaj, awaitefektywnie wykonuje a return, więc stos metody umiera; maszyna stanu musi gdzieś zostać zachowana, a to „gdzieś” jest zdecydowanie na kupie. Okres istnienia stosu nie pasuje dobrze do kodu asynchronicznego :)

Poza tym maszyna stanowa narusza kilka dobrych wytycznych dotyczących definiowania struktur:

  • structs powinien mieć co najwyżej 16 bajtów - automat stanowy zawiera dwa wskaźniki, które samodzielnie wypełniają 16-bajtowe ograniczenie na 64-bitowym. Poza tym jest sam stan, więc przekracza „granicę”. To nie jest wielka sprawa, ponieważ jest całkiem prawdopodobne, że jest ona przekazywana tylko przez odniesienie, ale zauważ, że nie pasuje to do przypadku użycia dla struktur - struktury, która jest w zasadzie typem referencyjnym.
  • structs powinny być niezmienne - cóż, prawdopodobnie nie wymaga to większego komentarza. To maszyna stanowa . Ponownie, nie jest to wielka sprawa, ponieważ struktura jest automatycznie generowanym kodem i prywatnym, ale ...
  • structs powinny logicznie reprezentować pojedynczą wartość. Zdecydowanie nie w tym przypadku, ale to już w pewnym sensie wynika z posiadania zmiennego stanu w pierwszej kolejności.
  • Nie należy go często umieszczać w pudełkach - nie stanowi to problemu, ponieważ wszędzie używamy leków generycznych . Stan ostatecznie znajduje się gdzieś na stercie, ale przynajmniej nie jest pakowany (automatycznie). Ponownie, fakt, że jest używany tylko wewnętrznie, sprawia, że ​​jest to prawie nieważne.

I oczywiście wszystko to dzieje się w przypadku, gdy nie ma zamknięć. Kiedy masz lokale (lub pola), które przechodzą przez awaits, stan jest dalej zawyżany, co ogranicza użyteczność użycia struktury.

Biorąc pod uwagę to wszystko, podejście klasowe jest zdecydowanie czystsze i nie spodziewałbym się zauważalnego wzrostu wydajności przy użyciu structzamiast tego. Wszystkich obiektów biorących udział mają podobną żywotność, więc jedynym sposobem, aby poprawić wydajność pamięci byłoby, aby wszystkie z nich structs (sklep w jakiś bufor, na przykład) - co jest niemożliwe w przypadku ogólnym, oczywiście. A większość przypadków, w których używałbyś awaitw pierwszej kolejności (to znaczy pewnej asynchronicznej pracy we / wy) obejmuje już inne klasy - na przykład bufory danych, ciągi znaków ... Jest raczej mało prawdopodobne, abyś zrobił awaitcoś, co po prostu powróci 42bez wykonywania żadnych alokacje sterty.

Ostatecznie powiedziałbym, że jedynym miejscem, w którym naprawdę widać prawdziwą różnicę w wydajności, są testy porównawcze. A optymalizacja pod kątem testów porównawczych to co najmniej głupi pomysł ...

Luaan
źródło
Nie zawsze potrzebujesz członka zespołu kompilatorów, kiedy możesz iść i przeczytać źródło, a oni zostawili pomocny komentarz :-)
Damien_The_Unbeliever
3
@Damien_The_Unbeliever Tak, to było zdecydowanie świetne znalezisko, już zagłosowałem za twoją odpowiedź: P
Luaan
1
Struktura bardzo pomaga w przypadku, gdy kod nie działa asynchronicznie, np. Dane są już w buforze.
Ian Ringrose