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

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

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, я думаю), также сгенерировали классы, а затем он был изменен на структуру из соображений производительности. (в некоторых случаях вы можете полностью избежать бокса и распределения кучи...) (см. this)

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

Edit:

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

Ответы

Ответ 1

У меня не было никаких предвидений этого, но поскольку 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;

Итак, хотя есть некоторые призывы к использованию struct s, большая победа в разрешении "Редактировать и продолжить" для работы в методах async был выбран в качестве лучшего варианта.

Ответ 2

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

Эффективность "бонуса" структур всегда является компромиссом. В основном вы получаете следующее:

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

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

Кроме того, государственный механизм нарушает некоторые хорошие рекомендации для определения структур:

  • struct должно быть не более 16 байт - машина состояний содержит два указателя, которые сами по себе заполняют 16-байтовое ограничение аккуратно на 64-разрядном. Помимо этого, есть само государство, поэтому оно переходит через "предел". Это не имеет большого значения, так как это, скорее всего, только когда-либо передавалось по ссылке, но обратите внимание, что это не совсем подходит для использования для structs - структуры, которая в основном является ссылочным типом.
  • struct должен быть неизменным - хорошо, это, вероятно, не требует большого количества комментариев. Это государственная машина. Опять же, это не имеет большого значения, поскольку структура является автоматически сгенерированным кодом и частным, но...
  • struct должно логически представлять одно значение. Определенно, не здесь, но это уже происходит из-за наличия изменчивого состояния.
  • Он не должен быть помещен в коробку часто - здесь не проблема, так как мы используем дженерики повсюду. Государство в конечном счете находится где-то в куче, но по крайней мере оно не помещается в коробку (автоматически). Опять же, тот факт, что он используется только внутренне, делает это в значительной степени недействительным.

И, конечно, все это в случае, когда нет закрытий. Когда у вас есть locals (или поля), которые пересекают await s, состояние еще раздувается, ограничивая полезность использования структуры.

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

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