c# - ¿Mi compilador ignorará el código inútil?
.net compiler-optimization (9)
¿El compilador ignora el código (explícito o no)?
No se puede determinar fácilmente que es inútil, por lo que el compilador tampoco puede. El captador de TestRunTime.Length
puede tener efectos secundarios, por ejemplo.
El fondo es que mantengo una gran cantidad de código (que no escribí) y me preguntaba si el código inútil debería ser un objetivo.
Antes de refactorizar un fragmento de código, debe verificar lo que hace para poder cambiarlo y decir después que todavía tiene el mismo resultado. Las pruebas unitarias son una gran manera de hacer esto.
He hecho algunas preguntas en la red sobre este tema, pero no encontré ninguna respuesta para mi pregunta, o es para otro idioma o no responde por completo (el código muerto no es un código inútil), así que aquí está mi pregunta :
¿El compilador ignora el código (explícito o no)?
Por ejemplo, en este código:
double[] TestRunTime = SomeFunctionThatReturnDoubles;
// A bit of code skipped
int i = 0;
for (int j = 0; j < TestRunTime.Length; j++)
{
}
double prevSpec_OilCons = 0;
¿Se eliminará el bucle for?
Yo uso .net4.5 y vs2013
El fondo es que mantengo una gran cantidad de código (que no escribí) y me preguntaba si el código inútil debería ser un objetivo o si podía dejar que el compilador se encargara de eso.
El JIT es básicamente capaz de eliminar el código muerto. No es muy minucioso. Las variables y expresiones muertas se matan de forma fiable. Esa es una optimización fácil en forma de SSA.
No estoy seguro sobre el flujo de control. Si anidas dos bucles, solo el interno será borrado, lo recuerdo.
Si desea averiguar con seguridad qué se elimina y qué no, consulte el código x86 generado. El compilador de C # hace muy pocas optimizaciones. El JIT hace unos pocos.
Los JIT 4.5 32 y 64 bits son diferentes bases de código y tienen un comportamiento diferente. Se acerca un nuevo JIT (RyuJIT) que en mis pruebas generalmente funciona peor, a veces mejor.
El bucle no se puede eliminar , el código no está muerto , por ejemplo:
// Just some function, right?
private static Double[] SomeFunctionThatReturnDoubles() {
return null;
}
...
double[] TestRunTime = SomeFunctionThatReturnDoubles();
...
// You''ll end up with exception since TestRunTime is null
for (int j = 0; j < TestRunTime.Length; j++)
{
}
...
Por lo general, el compilador simplemente no puede predecir todos los resultados posibles de SomeFunctionThatReturnDoubles
y es por eso que conserva el bucle
En su bucle hay dos operaciones implícitas en cada iteración. Un incremento:
j++;
y comparacion
j<TestRunTime.Length;
Por lo tanto, el bucle no está vacío, aunque parece que lo está. Hay algo que se está ejecutando al final y, por supuesto, el compilador no lo ignora.
Esto sucede también en otros bucles.
En su mayor parte, no debe preocuparse por la eliminación proactiva de código inútil. Si te encuentras con problemas de rendimiento, y tu generador de perfiles dice que algún código inútil está consumiendo tus ciclos de reloj, entonces hazlo nuclear. Sin embargo, si el código realmente no hace nada y no tiene efectos secundarios, entonces probablemente tendrá poco impacto en el tiempo de ejecución.
Dicho esto, la mayoría de los compiladores no están obligados a realizar optimizaciones, por lo que confiar en las optimizaciones del compilador no siempre es la opción más inteligente. Sin embargo, en muchos casos, incluso un bucle de giro inútil puede ejecutarse bastante rápido. Un spinlock básico que se repite un millón de veces se compilaría en algo como mov eax, 0 / inc eax / cmp eax, 1000000 / jnz -8
. Incluso si descontamos las optimizaciones en la CPU, eso es solo 3 ciclos por bucle (en un chip de estilo RISC reciente) ya que no hay acceso a la memoria, por lo que no habrá ninguna invalidación de caché. En una CPU de 1 GHz, eso es solo 3,000,000 / 1,000,000,000 segundos, o 3 milisegundos. Eso sería un éxito bastante importante si intentara ejecutarlo 60 veces por segundo, pero en muchos casos, probablemente ni siquiera se notará.
Un bucle como el que describí sería casi sin duda optimizado para mov eax 1000000
, incluso en un entorno JIT. Probablemente se optimizaría más que eso, pero dado que no existe otro contexto, esa optimización es razonable y no causaría efectos negativos.
tl; dr: Si su generador de perfiles dice que el código muerto / inútil está utilizando una cantidad considerable de sus recursos de tiempo de ejecución, elimínelo. Sin embargo, no vayas a la caza de brujas por código muerto; dejar eso para el reescritura / refactor masivo en la línea.
Bono: si el generador de código sabía que eax
no se iba a leer para otra cosa que no fuera la condición de bucle y deseaba retener el spinlock, podría generar mov eax, 1000000 / dec eax / jnz -3
y reducir la penalización del bucle por un ciclo. Sin embargo, la mayoría de los compiladores simplemente lo eliminan por completo
He hecho un pequeño formulario para probarlo de acuerdo con algunas respuestas de algunas personas sobre el uso de long.MaxValue
, aquí está mi código de referencia:
public Form1()
{
InitializeComponent();
Stopwatch test = new Stopwatch();
test.Start();
myTextBox.Text = test.Elapsed.ToString();
}
Y aquí está el código con un código inútil:
public Form1()
{
InitializeComponent();
Stopwatch test = new Stopwatch();
test.Start();
for (int i = 0; i < int.MaxValue; i++)
{
}
myTextBox.Text = test.Elapsed.ToString();
}
int.MaxValue
que usé int.MaxValue
lugar de long.MaxValue
, no quería pasar el día del año en este caso.
Como puedes ver:
---------------------------------------------------------------------
| | Debug | Release |
---------------------------------------------------------------------
|Ref | 00:00:00.0000019 | 00:00:00.0000019 |
|Useless code | 00:00:05.3837568 | 00:00:05.2728447 |
---------------------------------------------------------------------
El código no está optimizado. Espera un poco, lo intentaré con algo de int[]
para probar int[].Lenght
public Form1()
{
InitializeComponent();
int[] myTab = functionThatReturnInts(1);
Stopwatch test = new Stopwatch();
test.Start();
for (int i = 0; i < myTab.Length; i++)
{
}
myTextBox.Text = test.Elapsed.ToString();
}
public int[] functionThatReturnInts(int desiredSize)
{
return Enumerable.Repeat(42, desiredSize).ToArray();
}
Y aquí están los resultados:
---------------------------------------------
| Size | Release |
---------------------------------------------
| 1 | 00:00:00.0000015 |
| 100 | 00:00:00 |
| 10 000 | 00:00:00.0000035 |
| 1 000 000 | 00:00:00.0003236 |
| 100 000 000 | 00:00:00.0312673 |
---------------------------------------------
Así que incluso con matrices, no se optimiza en absoluto.
No será ignorado. Sin embargo, cuando llegue a IL habrá una instrucción de salto, por lo que for se ejecutará como si fuera una instrucción if. También ejecutará el código para ++ y la longitud, como lo mencionó @Fleve. Solo será un código extra. Por motivos de legibilidad, así como para cumplir con los estándares de código, eliminaría el código si no lo usas.
Obviamente, su compilador no ignorará el código inútil, sino que lo analizará cuidadosamente y luego tratará de eliminarlo, si realiza optimizaciones.
En su caso, la primera cosa interesante es si la variable j se usa después del bucle o no. La otra cosa interesante es TestRunTime.Length. El compilador lo mirará y verificará si siempre devuelve el mismo resultado, y si es así, si tiene algún efecto secundario, y si es así, si llamarlo una vez tiene el mismo efecto secundario total que llamarlo repetidamente.
Si TestRunTime.Length no tiene ningún efecto secundario y j no se utiliza, se elimina el bucle.
De lo contrario, si llamar a TestRunTime.Length repetidamente tiene más efectos secundarios que llamarlo una vez, o si las llamadas repetidas devuelven valores diferentes, entonces el bucle debe ejecutarse.
De lo contrario, j = max (0, TestRunTime.Length).
A continuación, el compilador puede determinar si la asignación TestRunTime.Length es necesaria. Puede ser reemplazado por un código que solo determina lo que sería TestRunTime.Length.
Luego, por supuesto, es posible que su compilador no intente realizar optimizaciones sofisticadas, o que las reglas del idioma sean las mismas, de modo que no pueda determinar estas cosas y se quede atascado.
Bueno, sus variables i
y prevSpec_OilCons
, si no se utilizan en cualquier lugar, se optimizarán, pero no su bucle.
Así que si su código se ve como:
static void Main(string[] args)
{
int[] TestRunTime = { 1, 2, 3 };
int i = 0;
for (int j = 0; j < TestRunTime.Length; j++)
{
}
double prevSpec_OilCons = 0;
Console.WriteLine("Code end");
}
bajo ILSpy será:
private static void Main(string[] args)
{
int[] TestRunTime = new int[]
{
1,
2,
3
};
for (int i = 0; i < TestRunTime.Length; i++)
{
}
Console.WriteLine("Code end");
}
Dado que el bucle tiene un par de declaraciones, como comparación e incremento, podría usarse para implementar un período de espera / espera algo corto. (aunque no es una buena práctica hacerlo) .
Considere el siguiente bucle, que es un bucle vacío, pero tomará mucho tiempo para ejecutarse.
for (long j = 0; j < long.MaxValue; j++)
{
}
El bucle en su código no es un código muerto, en lo que respecta al código muerto, el siguiente es un código muerto y se optimizará.
if (false)
{
Console.Write("Shouldn''t be here");
}
El bucle, ni siquiera será eliminado por los nervios .NET. Basado en esta answer