Cuando tengo una variable de solo lectura:
public readonly bool myVar = true;Y compruébalo en un código como este:
for(int i = 0; i != 10; i++) { if(myVar) DoStuff(); else DoOtherStuff(); }Al observar la IL emitida, puedo ver que la verificación se realiza en cada iteración del ciclo. Esperaría que el código anterior produzca el mismo IL que este:
if (myVar) { for(int i = 0; i != 10; i++) { DoStuff(); } } else { for(int i = 0; i != 10; i++) { DoOtherStuff(); } }Entonces, ¿por qué la verificación no está optimizada para el exterior del ciclo, ya que el campo es de solo lectura y no se puede cambiar entre iteraciones?
Porque se puede cambiar usando Reflection:
using System; using System.Reflection; public class Program { public static void Main() { var t = new Test(); var field = typeof(Test).GetField("myVar", BindingFlags.Instance | BindingFlags.Public); Console.WriteLine(t.myVar); // prints True field.SetValue(t, false); Console.WriteLine(t.myVar); // prints False // Trying to use t.myVar = false or true; <-- does not compile } } public class Test { public readonly bool myVar = true; }Violín de trabajo: https://dotnetfiddle.net/W9UO3m
Tenga en cuenta que no hay forma en que el optimizador de código de tiempo de compilación pueda predecir o detectar con absoluta certeza si dicho código de reflexión existe o no, y si existe, si se ejecutará o no.
Su optimización propuesta realmente es una combinación de dos transformaciones individuales más simples. Lo primero es sacar el acceso del miembro fuera del bucle. Desde
for(int i = 0; i != 10; i++) { var localVar = this.memberVar; if(localVar) DoStuff(); else DoOtherStuff(); }para
var localVar = this.memberVar; for(int i = 0; i != 10; i++) { if(localVar) DoStuff(); else DoOtherStuff(); }El segundo es intercambiar la condición de bucle con la condición if. Desde
var localVar = this.memberVar; for(int i = 0; i != 10; i++) { if(localVar) DoStuff(); else DoOtherStuff(); }para
var localVar = this.memberVar; if (localVar) { for(int i = 0; i != 10; i++) DoStuff(); } else { for(int i = 0; i != 10; i++) DoOtherStuff(); } El primero está influenciado por readonly . Para hacerlo, el compilador tiene que demostrar que la memberVar no puede cambiar dentro del ciclo y readonly garantiza este 1 , aunque este ciclo se puede llamar dentro de un constructor y el valor de la memberVar se puede cambiar en el constructor después de que finaliza el ciclo. , no se puede cambiar en el cuerpo del bucle: DoStuff() no es un constructor del objeto actual, ni tampoco lo es DoOtherStuff() . Reflection no cuenta, aunque es posible usar Reflection para romper invariantes, no está permitido hacerlo. Los hilos cuentan, ver nota al pie.
La segunda es una transformación simple pero una decisión más difícil de tomar para el compilador, porque es difícil predecir si realmente mejorará el rendimiento. Naturalmente, puede verlo por separado haciendo la primera transformación en el código usted mismo y viendo qué código se genera.
Quizás una consideración más importante es que en .NET, el paso de optimización se lleva a cabo entre MSIL y el código de máquina, no durante la compilación de C# a IL. ¡Así que no puede ver qué optimizaciones se están haciendo mirando el MSIL!
1 ¿O sí? El modelo de memoria de .NET es considerablemente más indulgente que, por ejemplo, el modelo de C++, donde cualquier carrera de datos conduce muy rápidamente a un comportamiento indefinido a menos que el objeto se defina como volatile /atómico. ¿Qué sucede si este bucle se ejecuta en un subproceso de trabajo generado por el constructor de objetos y, después de generar el subproceso, el constructor continúa (lo que llamaré la "segunda mitad") para cambiar el miembro de readonly ? ¿El modelo de memoria requiere que el subproceso de trabajo vea ese cambio? ¿Qué pasa si DoStuff() y la segunda mitad del constructor fuerzan vallas de memoria, por ejemplo, acceden a otros miembros que son volatile o toman un bloqueo? Entonces , solo readonly solo permitiría la optimización en un entorno de un solo subproceso .
Un campo de readonly se puede inicializar con diferentes valores en diferentes puntos del código (múltiples constructores). No se puede optimizar porque múltiples constructores permiten múltiples valores del campo en cuestión. Ahora, si solo tuviera un constructor, sin bifurcaciones relevantes y, por lo tanto, solo un valor posible para ese campo, esperaría más del optimizador. Sin embargo, ese tipo de análisis es una de las razones por las que C++ tarda mucho más en compilarse que C#, y este tipo de tareas normalmente no se delegan al tiempo de ejecución.
Para una explicación más clara:
Nota
La palabra clave readonly es diferente de la palabra clave const. Un campo const solo se puede inicializar en la declaración del campo. Un campo de solo lectura se puede asignar varias veces en la declaración del campo y en cualquier constructor. Por lo tanto, los campos de solo lectura pueden tener diferentes valores según el constructor utilizado. Además, mientras que un campo const es una constante de tiempo de compilación, el campo de solo lectura se puede usar para constantes de tiempo de ejecución como en el siguiente ejemplo:
Consulte https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/readonly
Para su caso específico, sugeriría usar const en lugar de readonly , aunque admito que no he buscado diferencias en la IL generada.
Honestamente, me pregunto si realmente hace una diferencia. Cualquier CPU que realice una predicción de bifurcación y tenga un caché medianamente decente probablemente producirá el mismo rendimiento en cualquier caso. (Tenga en cuenta que no lo he probado ; es solo una sospecha).