Почему классы асинхронных конечных автоматов (а не структуры) в Roslyn?

87

Давайте рассмотрим этот очень простой асинхронный метод:

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

Когда я компилирую это с помощью VS2013 (до компилятора Roslyn), сгенерированный конечный автомат представляет собой структуру.

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

Когда я компилирую его с VS2015 (Roslyn), сгенерированный код выглядит так:

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

Как видите, Roslyn генерирует класс (а не структуру). Если я правильно помню, первые реализации поддержки async / await в старом компиляторе (я думаю, CTP2012) также генерировали классы, а затем он был изменен на структуру из соображений производительности. (в некоторых случаях можно полностью избежать боксов и выделения кучи…) (См. это )

Кто-нибудь знает, почему это снова поменяли в Roslyn? (У меня нет проблем с этим, я знаю, что это изменение прозрачное и не меняет поведения какого-либо кода, мне просто любопытно)

Редактировать:

Ответ от @Damien_The_Unbeliever (и исходный код :)) imho все объясняет. Описанное поведение Roslyn применяется только для отладочной сборки (и это необходимо из-за ограничения CLR, упомянутого в комментарии). В Release он также генерирует структуру (со всеми преимуществами этого ..). Таким образом, это кажется очень умным решением для поддержки как редактирования, так и продолжения, а также повышения производительности в производственной среде. Интересный материал, спасибо всем, кто участвовал!

Gregkalapos
источник
2
Я подозреваю, что они решили, что сложность (изменяемые структуры) того не стоит. asyncметоды почти всегда имеют истинную асинхронную точку - точку, awaitкоторая дает управление, которое в любом случае потребует упаковки структуры. Я считаю, что структуры только уменьшат давление на память для asyncметодов, которые выполняются синхронно.
Стивен Клири

Ответы:

112

Я не знал об этом заранее, но, поскольку в наши дни Roslyn имеет открытый исходный код, мы можем поискать объяснения по коду.

И здесь, в строке 60 AsyncRewriter , мы находим:

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

Таким образом, несмотря на то, что есть некоторая привлекательность в использовании structs, большая победа в том, что разрешить Edit и Continue работать внутри asyncметодов, была явно выбрана как лучший вариант.

Damien_The_Unbeliever
источник
18
Очень хороший улов! И на основе этого вот что я также обнаружил: это происходит только тогда, когда вы создаете его в отладке (имеет смысл, когда вы выполняете EnC ..), но в Release они создают структуру (очевидно, что EnableEditAndContinue в этом случае ложно .. .). Кстати. Я тоже пытался заглянуть в код, но не нашел. Большое спасибо!
gregkalapos
3

Трудно дать окончательный ответ на что-то вроде этого (если только кто-то из команды компиляторов не заглянет :)), но есть несколько моментов, которые вы можете учесть:

«Бонус» производительности структур всегда является компромиссом. В основном вы получаете следующее:

  • Семантика значений
  • Возможное размещение стека (может, даже регистра?)
  • Избегайте косвенного обращения

Что это означает в случае ожидания? Ну вообще ... ничего. Существует только очень короткий период времени, в течение которого конечный автомат находится в стеке - помните, awaitэффективно выполняет a return, поэтому стек методов умирает; конечный автомат должен где-то сохраняться, а это «где-то» точно лежит в куче. Время жизни стека плохо подходит для асинхронного кода :)

Помимо этого, конечный автомат нарушает некоторые хорошие рекомендации по определению структур:

  • structs должен быть не более 16 байтов - конечный автомат содержит два указателя, которые сами по себе заполняют 16-байтовый предел на 64-битном. Кроме того, есть само состояние, поэтому оно выходит за «лимит». Это не имеет большого значения , поскольку, скорее всего, он всегда передается только по ссылке, но обратите внимание, что это не совсем подходит для варианта использования структур - структуры, которая в основном является ссылочным типом.
  • structs должен быть неизменным - ну, это, вероятно, не требует особых комментариев. Это государственная машина . Опять же, это не имеет большого значения, поскольку структура является автоматически сгенерированным кодом и закрытой, но ...
  • structs должен логически представлять одно значение. Определенно не здесь, но это уже как бы следует из наличия изменяемого состояния в первую очередь.
  • Его не следует часто ставить в коробку - здесь не проблема, поскольку мы везде используем дженерики . Состояние в конечном итоге находится где-то в куче, но, по крайней мере, оно не упаковывается (автоматически). Опять же, тот факт, что он используется только для внутренних целей, делает это практически пустым.

И, конечно же, все это в том случае, когда нет закрытий. Когда у вас есть локальные переменные (или поля), которые пересекают awaits, состояние еще больше увеличивается, что ограничивает полезность использования структуры.

Учитывая все это, классовый подход определенно чище, и я бы не ожидал заметного увеличения производительности от использования structвместо него. Все объекты , задействованные имеют одинаковую продолжительность жизни, так что единственный способ улучшить производительность памяти было бы сделать все из них structс (магазин в некотором буфере, например) - что невозможно в общем случае, конечно. И в большинстве случаев, когда вы использовали быawait в первую очередь (то есть некоторая работа с асинхронным вводом-выводом), уже вовлекают другие классы - например, буферы данных, строки ... Маловероятно, что вы будете что- awaitто, что просто возвращается 42без каких-либо действий. распределения кучи.

В конце концов, я бы сказал, что единственное место, где вы действительно увидите реальную разницу в производительности, - это тесты. А оптимизация для тестов - это, мягко говоря, глупая идея ...

Луаан
источник
Вам не всегда нужен член команды компиляторов, когда вы можете пойти и прочитать исходный
текст
3
@Damien_The_Unbeliever Да, это определенно была отличная находка, я уже поддержал ваш ответ: P
Luaan
1
Структура очень помогает в случае, если код не выполняется асинхронно, например, данные уже находятся в буфере.
Ян Рингроуз