Por que são classes de máquinas de estado assíncrono (e não structs) em Roslyn?

87

Vamos considerar este método assíncrono muito simples:

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

Quando eu compilo isso com VS2013 (pré-compilador Roslyn), a máquina de estados gerada é uma estrutura.

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

Quando eu compilo com VS2015 (Roslyn) o código gerado é este:

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

Como você pode ver, Roslyn gera uma classe (e não uma estrutura). Se bem me lembro, as primeiras implementações do suporte async / await no compilador antigo (CTP2012 eu acho) também geraram classes e, em seguida, ele foi alterado para struct por motivos de desempenho. (em alguns casos, você pode evitar completamente o boxing e a alocação de heap ...) (Veja isto )

Alguém sabe por que isso mudou novamente em Roslyn? (Não tenho nenhum problema quanto a isso, sei que essa mudança é transparente e não altera o comportamento de nenhum código, só estou curioso)

Editar:

A resposta de @Damien_The_Unbeliever (e o código-fonte :)) imho explica tudo. O comportamento descrito do Roslyn se aplica apenas à compilação de depuração (e isso é necessário devido à limitação do CLR mencionada no comentário). No Release também gera uma estrutura (com todos os benefícios disso ..). Portanto, esta parece ser uma solução muito inteligente para oferecer suporte a Editar e Continuar e melhor desempenho na produção. Coisa interessante, obrigado a todos que participaram!

Gregkalapos
fonte
2
Suspeito que eles decidiram que a complexidade (estruturas refeitas) não valia a pena. asyncos métodos quase sempre têm um verdadeiro ponto assíncrono - um awaitque produz controle, o que exigiria que a estrutura fosse encaixotada de qualquer maneira. Eu acredito estruturas só iria aliviar a pressão de memória para asyncmétodos que aconteceram para ser executado de forma síncrona.
Stephen Cleary

Respostas:

112

Eu não tinha nenhum conhecimento prévio disso, mas como Roslyn é um código-fonte aberto atualmente, podemos procurar uma explicação no código.

E aqui, na linha 60 do AsyncRewriter , encontramos:

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

Portanto, embora haja algum apelo no uso de structs, a grande vantagem de permitir que Editar e Continuar funcione dentro dos asyncmétodos foi obviamente escolhida como a melhor opção.

Damien_The_Unbeliever
fonte
18
Muito boa captura! E com base nisso aqui está o que eu também descobri: Isso só acontece quando você constrói no debug (faz sentido, é quando você faz EnC ..), mas no Release eles criam uma estrutura (obviamente, EnableEditAndContinue é falso nesse caso .. .). Btw. Também tentei examinar o código, mas não encontrei. Muito Obrigado!
gregkalapos
3

É difícil dar uma resposta definitiva para algo assim (a menos que alguém da equipe do compilador apareça :)), mas há alguns pontos que você pode considerar:

O "bônus" de desempenho das estruturas é sempre uma troca. Basicamente, você obtém o seguinte:

  • Semântica de valor
  • Possível alocação de pilha (talvez até mesmo registro?)
  • Evitando indireção

O que isso significa no caso de espera? Bem, na verdade ... nada. Há apenas um curto período de tempo durante o qual a máquina de estado está na pilha - lembre-se, awaitefetivamente faz a return, então a pilha de métodos morre; a máquina de estado deve ser preservada em algum lugar, e esse "algum lugar" está definitivamente na pilha. O tempo de vida da pilha não se ajusta bem ao código assíncrono :)

Além disso, a máquina de estado viola algumas boas diretrizes para definir estruturas:

  • structs deve ter no máximo 16 bytes - a máquina de estado contém dois ponteiros, que por si próprios preenchem o limite de 16 bytes de forma ordenada em 64 bits. Além disso, existe o próprio estado, então ele ultrapassa o "limite". Isso não é grande coisa, já que provavelmente só é passado por referência, mas observe como isso não se encaixa perfeitamente no caso de uso de structs - uma estrutura que é basicamente um tipo de referência.
  • structs deve ser imutável - bem, isso provavelmente não precisa de muitos comentários. É uma máquina de estado . Novamente, isso não é grande coisa, já que a estrutura é um código gerado automaticamente e privado, mas ...
  • structs deve representar logicamente um único valor. Definitivamente não é o caso aqui, mas isso já se segue de ter um estado mutável em primeiro lugar.
  • Não deve ser embalado com frequência - o que não é um problema aqui, uma vez que usamos genéricos em todos os lugares . O estado está em algum lugar na pilha, mas pelo menos não está sendo encaixotado (automaticamente). Novamente, o fato de ser usado apenas internamente torna isso praticamente vazio.

E, claro, tudo isso em um caso em que não há fechamentos. Quando você tem locais (ou campos) que atravessam os awaits, o estado é ainda mais inflado, limitando a utilidade de usar uma estrutura.

Diante de tudo isso, a abordagem de classe é definitivamente mais limpa e eu não esperaria nenhum aumento de desempenho perceptível com o uso de um struct. Todos os objetos envolvidos têm vida útil semelhante, então a única maneira de melhorar o desempenho da memória seria fazer todos eles structs (armazenar em algum buffer, por exemplo) - o que é impossível no caso geral, é claro. E a maioria dos casos em que você usaria awaitem primeiro lugar (isto é, algum trabalho de E / S assíncrono) já envolve outras classes - por exemplo, buffers de dados, strings ... É bastante improvável que você faria awaitalgo que simplesmente retorna 42sem fazer nenhum alocações de heap.

No final, eu diria que o único lugar onde você realmente veria uma diferença real de desempenho seriam nos benchmarks. E otimizar para benchmarks é uma ideia boba, para dizer o mínimo ...

Luaan
fonte
Você nem sempre precisa de um membro da equipe do compilador quando pode ler o código-fonte, e eles deixaram um comentário útil :-)
Damien_The_Unbeliever
3
@Damien_The_Unbeliever Sim, foi definitivamente um ótimo achado, já votei a favor de sua resposta: P
Luaan
1
A estrutura ajuda muito no caso de o código não rodar de forma assíncrona, por exemplo, os dados já estão em um buffer.
Ian Ringrose