¿Por qué hay clases de máquinas de estado asíncronas (y no estructuras) en Roslyn?

87

Consideremos este método asincrónico muy simple:

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

Cuando compilo esto con VS2013 (compilador anterior a Roslyn), la máquina de estado generada es una estructura.

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

Cuando lo compilo con VS2015 (Roslyn), el código generado es este:

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

Como puede ver, Roslyn genera una clase (y no una estructura). Si recuerdo correctamente, las primeras implementaciones del soporte async / await en el compilador antiguo (CTP2012 supongo) también generaron clases y luego se cambió a estructura por razones de rendimiento. (en algunos casos puede evitar por completo el boxeo y la asignación de pila…) (Vea esto )

¿Alguien sabe por qué se volvió a cambiar esto en Roslyn? (No tengo ningún problema al respecto, sé que este cambio es transparente y no cambia el comportamiento de ningún código, solo tengo curiosidad)

Editar:

La respuesta de @Damien_The_Unbeliever (y el código fuente :)) en mi humilde opinión lo explica todo. El comportamiento descrito de Roslyn solo se aplica a la compilación de depuración (y eso es necesario debido a la limitación CLR mencionada en el comentario). En Release también genera una estructura (con todos los beneficios de eso ..). Así que esta parece ser una solución muy inteligente para admitir tanto Editar como Continuar y mejorar el rendimiento en producción. Cosas interesantes, ¡gracias a todos los que participaron!

Gregkalapos
fuente
2
Sospecho que decidieron que la complejidad (estructuras removibles) no valía la pena. asyncLos métodos casi siempre tienen un verdadero punto asincrónico, awaitque produce control, lo que requeriría que la estructura esté enmarcada de todos modos. Creo que las estructuras solo aliviarían la presión de la memoria para los asyncmétodos que se ejecutaron sincrónicamente.
Stephen Cleary

Respuestas:

112

No tenía ningún conocimiento previo de esto, pero como Roslyn es de código abierto en estos días, podemos buscar una explicación en el código.

Y aquí, en la línea 60 del 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;

Entonces, aunque hay algo de atractivo en usar structs, la gran ventaja de permitir que Editar y Continuar funcione dentro de los asyncmétodos obviamente se eligió como la mejor opción.

Damien_The_Unbeliever
fuente
18
¡Muy buena captura! Y en base a esto, esto es lo que también descubrí: esto solo sucede cuando lo construyes en debug (tiene sentido, ahí es cuando haces EnC ...), pero en Release crean una estructura (obviamente EnableEditAndContinue es falso en ese caso ... .). Por cierto. También intenté buscar en el código, pero no encontré esto. ¡Muchas gracias!
gregkalapos
3

Es difícil dar una respuesta definitiva para algo como esto (a menos que alguien del equipo del compilador ingrese :)), pero hay algunos puntos que puede considerar:

La "bonificación" de rendimiento de las estructuras es siempre una compensación. Básicamente, obtienes lo siguiente:

  • Semántica de valores
  • Posible asignación de pila (¿tal vez incluso registro?)
  • Evitando la indirecta

¿Qué significa esto en el caso de espera? Bueno, en realidad ... nada. Solo hay un período de tiempo muy corto durante el cual la máquina de estado está en la pila; recuerde, awaitefectivamente hace a return, por lo que la pila de métodos muere; la máquina de estado debe conservarse en algún lugar, y ese "lugar" definitivamente está en el montón. La vida útil de la pila no se ajusta bien al código asincrónico :)

Aparte de esto, la máquina de estados viola algunas buenas pautas para definir estructuras:

  • structs debe tener un tamaño máximo de 16 bytes: la máquina de estado contiene dos punteros, que por sí solos llenan el límite de 16 bytes de forma ordenada en 64 bits. Aparte de eso, está el estado mismo, por lo que supera el "límite". Esto no es un gran problema, ya que es muy probable que solo se pase por referencia, pero tenga en cuenta que eso no se ajusta al caso de uso de las estructuras, una estructura que es básicamente un tipo de referencia.
  • structs debe ser inmutable - bueno, esto probablemente no necesite mucho comentario. Es una máquina de estado . Nuevamente, esto no es un gran problema, ya que la estructura es un código generado automáticamente y es privado, pero ...
  • structs debe representar lógicamente un valor único. Definitivamente no es el caso aquí, pero eso ya se deriva de tener un estado mutable en primer lugar.
  • No debería empaquetarse con frecuencia, no es un problema aquí, ya que usamos genéricos en todas partes . En última instancia, el estado está en algún lugar del montón, pero al menos no está en caja (automáticamente). Nuevamente, el hecho de que solo se use internamente hace que esto sea prácticamente nulo.

Y, por supuesto, todo esto es en un caso donde no hay cierres. Cuando tiene locales (o campos) que atraviesan la awaits, el estado se infla aún más, lo que limita la utilidad de usar una estructura.

Dado todo esto, el enfoque de clase es definitivamente más limpio, y no esperaría ningún aumento notable del rendimiento al usar un struct. Todos los objetos involucrados tienen una vida útil similar, por lo que la única forma de mejorar el rendimiento de la memoria sería hacer que todos ellos fueran structs (almacenarlos en algún búfer, por ejemplo), lo cual es imposible en el caso general, por supuesto. Y la mayoría de los casos en los que usaría awaiten primer lugar (es decir, algún trabajo de E / S asincrónico) ya involucran otras clases, por ejemplo, búferes de datos, cadenas ... Es bastante poco probable que haga awaitalgo que simplemente regrese 42sin hacer nada asignaciones de montón.

Al final, diría que el único lugar donde realmente vería una diferencia de rendimiento real serían los puntos de referencia. Y optimizar para los puntos de referencia es una idea tonta, por decir lo menos ...

Luaan
fuente
No siempre necesitas un miembro del equipo del compilador cuando puedes ir y leer la fuente, y han dejado un comentario útil :-)
Damien_The_Unbeliever
3
@Damien_The_Unbeliever Sí, definitivamente fue un gran hallazgo, ya voté a favor de tu respuesta: P
Luaan
1
La estructura ayuda mucho en el caso de que el código no se ejecute de forma asincrónica, por ejemplo, los datos ya están en un búfer.
Ian Ringrose