c# .net language-lawyer roslyn

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



.net language-lawyer (2)

Consideremos este método asíncrono 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 asíncrono / en espera en el compilador anterior (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 del montón ...) (Ver this )

¿Alguien sabe por qué esto cambió nuevamente en Roslyn? (No tengo ningún problema con respecto a esto, 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 :)) imho 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 de CLR mencionada en el comentario). En Release también genera una estructura (con todos los beneficios de eso ...). Por lo tanto, esta parece ser una solución muy inteligente para admitir tanto Editar como Continuar y un mejor rendimiento en la producción. ¡Cosas interesantes, gracias por todos los que participaron!


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:

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

  • Semántica de valor
  • Posible asignación de pila (¿tal vez incluso registro?)
  • Evitar la indirección

¿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, await efectivamente hace un return , por lo que la pila de métodos muere; la máquina de estados debe conservarse en algún lugar, y ese "en algún lugar" definitivamente está en el montón. La duración de la pila no se ajusta bien al código asincrónico :)

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

  • struct deben tener un máximo de 16 bytes: la máquina de estado contiene dos punteros, que por sí mismos llenan el límite de 16 bytes de forma clara 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 encaja en el caso de uso de las estructuras, una estructura que es básicamente un tipo de referencia.
  • struct deben ser inmutables, bueno, esto probablemente no necesita mucho comentario. Es una máquina de estados . Nuevamente, esto no es un gran problema, ya que la estructura es un código generado automáticamente y privado, pero ...
  • struct deben representar lógicamente un valor único. Definitivamente no es el caso aquí, pero eso ya se sigue de tener un estado mutable en primer lugar.
  • No debe encuadrarse con frecuencia, no es un problema aquí, ya que estamos usando genéricos en todas partes . El estado finalmente está en algún lugar del montón, pero al menos no está encuadrado (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 las await , 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 una struct lugar. 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 sean struct (almacenar 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 await en primer lugar (es decir, un trabajo de E / S asíncrono) ya involucran otras clases, por ejemplo, almacenamientos intermedios de datos, cadenas ... Es bastante improbable que await algo que simplemente devuelve 42 sin hacer ninguna asignación de montón.

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


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

Y aquí, en la línea 60 de 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, si bien es atractivo usar struct , la gran opción de permitir que Editar y continuar funcione dentro de los métodos async fue obviamente la mejor opción.