Se permite la optimización del comstackdor de C # en las variables locales y la recuperación del valor de la memoria

EDITAR : pregunto qué sucede cuando dos subprocesos acceden simultáneamente a los mismos datos sin la sincronización adecuada (antes de esta edición, ese punto no se expresó claramente).

Tengo una pregunta sobre las optimizaciones que realizan el comstackdor de C # y el comstackdor JIT.

Considere el siguiente ejemplo simplificado:

class Example { private Action _action; private void InvokeAction() { var local = this._action; if (local != null) { local(); } } } 

_action en el ejemplo que leer _action puede generar un valor almacenado en caché y desactualizado, ya que no hay un especificador volátil ni ninguna otra sincronización. Ese no es el punto 🙂

¿Se permite al comstackdor (o en realidad al jitter en tiempo de ejecución) optimizar la asignación a la variable local y, en su lugar, leer la _action de la memoria dos veces?

 class Example { private Action _action; private void InvokeAction() { if (this._action != null) { this._action(); // might be set to null by an other thread. } } } 

que podría lanzar una NullReferenceException cuando el campo _action se establece en null por una asignación concurrente.

Por supuesto, en este ejemplo, tal “optimización” no tendría ningún sentido porque sería más rápido almacenar el valor en un registro y, por lo tanto, utilizar la variable local. Pero en casos más complejos, ¿existe una garantía de que esto funcione como se espera sin volver a leer el valor de la memoria?

Es una optimización legal según el modelo de memoria definido en la especificación ECMA. Si la _acción fuera volátil, el modelo de memoria garantizaría que el valor se lea solo una vez, por lo que esta optimización no podría ocurrir.

Sin embargo, creo que las implementaciones actuales de CLR de Microsoft no optimizan las variables locales.

Diré (parcialmente) lo contrario de mgronber 🙂 Aaaah … Al final estoy diciendo las mismas cosas … Solo que estoy citando un artículo 🙁 Le daré un +1.

Es una optimización LEGAL según las especificaciones de ECMA, pero es una optimización ilegal bajo .NET> = 2.0 “especificaciones”.

Desde la comprensión del impacto de las técnicas de locking bajo en aplicaciones multiproceso

Lea aquí Modelo fuerte 2: .NET Framework 2.0

Punto 2:

Lecturas y escrituras no se pueden introducir.

La explicación a continuación:

Sin embargo, el modelo no permite que se introduzcan lecturas, ya que esto implicaría recuperar un valor de la memoria, y en la memoria de código de locking bajo podría estar cambiando.

PERO la nota debajo en la misma página, en la Técnica 1: Evitar lockings en algunas lecturas

En los sistemas que utilizan el modelo ECMA, hay una sutileza adicional. Incluso si solo se busca una ubicación de memoria en una variable local y ese local se usa varias veces, ¡cada uso puede tener un valor diferente! Esto se debe a que el modelo ECMA permite al comstackdor eliminar la variable local y volver a buscar la ubicación en cada uso. Si las actualizaciones se realizan al mismo tiempo, cada captura podría tener un valor diferente. Este comportamiento se puede suprimir con declaraciones volátiles, pero el problema es fácil de ignorar.

Si está escribiendo bajo Mono, debe saber que al menos hasta 2008 estuvo trabajando en el modelo de memoria ECMA (o eso escribieron en su lista de correo )

Con C # 7 , debe escribir el ejemplo de la siguiente manera y, de hecho, el IDE lo sugerirá como una “simplificación” para usted. El comstackdor generará un código que usa un local temporal para leer solo la ubicación de _action de la memoria principal una sola vez (independientemente de que sea nulo o no), y esto ayuda a prevenir la carrera común que se muestra en el segundo ejemplo del OP, es decir, donde _action se accede dos veces, y se puede establecer en nulo por otro hilo entre ellos.

 class Example { private Action _action; private void InvokeAction() { this._action?.Invoke(); } }