¿Hay alguna diferencia entre estas dos versiones de código?
foreach (var thing in things)
{
int i = thing.number;
// code using 'i'
// pay no attention to the uselessness of 'i'
}
int i;
foreach (var thing in things)
{
i = thing.number;
// code using 'i'
}
¿O al compilador no le importa? Cuando hablo de la diferencia me refiero en términos de rendimiento y uso de memoria. ¿O básicamente cualquier diferencia o los dos terminan siendo el mismo código después de la compilación?
c#
performance
memory
Alternatex
fuente
fuente
Respuestas:
TL; DR : son ejemplos equivalentes en la capa IL.
DotNetFiddle hace que esto sea bonito de responder ya que le permite ver la IL resultante.
Utilicé una variación ligeramente diferente de su construcción de bucle para acelerar mis pruebas. Solía:
Variación 1:
Variación 2:
En ambos casos, la salida IL compilada hizo lo mismo.
Entonces, para responder a su pregunta: el compilador optimiza la declaración de la variable y hace que las dos variaciones sean equivalentes.
Según tengo entendido, el compilador .NET IL mueve todas las declaraciones de variables al comienzo de la función, pero no pude encontrar una buena fuente que estableciera claramente que 2 . En este ejemplo en particular, verá que los movió con esta declaración:
En donde nos volvemos demasiado obsesivos al hacer comparaciones ...
Caso A, ¿todas las variables se mueven hacia arriba?
Para profundizar un poco más en esto, probé la siguiente función:
La diferencia aquí es que declaramos un
int i
o unstring j
basado en la comparación. Nuevamente, el compilador mueve todas las variables locales a la parte superior de la función 2 con:Me pareció interesante observar que, aunque
int i
no se declarará en este ejemplo, el código para admitirlo todavía se genera.Caso B: ¿Qué pasa en
foreach
lugar defor
?Se señaló que
foreach
tiene un comportamiento diferentefor
y que no estaba comprobando lo mismo por lo que me habían preguntado. Así que puse estas dos secciones de código para comparar la IL resultante.int
declaración fuera del bucle:int
declaración dentro del bucle:La IL resultante con el
foreach
bucle fue de hecho diferente de la IL generada usando elfor
bucle. Específicamente, el bloque init y la sección del bucle cambiaron.El
foreach
enfoque generó más variables locales y requirió algunas ramificaciones adicionales. Esencialmente, la primera vez que salta al final del bucle para obtener la primera iteración de la enumeración y luego salta a casi la parte superior del bucle para ejecutar el código del bucle. Luego continúa girando como cabría esperar.Pero más allá de las diferencias de ramificación causadas por el uso de las construcciones
for
yforeach
, no hubo diferencias en la IL según el lugar dondeint i
se colocó la declaración. Entonces todavía estamos en los dos enfoques que son equivalentes.Caso C: ¿Qué pasa con las diferentes versiones del compilador?
En un comentario que quedó 1 , había un enlace a una pregunta de SO con respecto a una advertencia sobre el acceso variable con foreach y el uso del cierre . La parte que realmente me llamó la atención en esa pregunta fue que puede haber diferencias en cómo funcionaba el compilador .NET 4.5 en comparación con versiones anteriores del compilador.
Y ahí es donde el sitio DotNetFiddler me decepcionó: todo lo que tenían disponible era .NET 4.5 y una versión del compilador de Roslyn. Así que saqué una instancia local de Visual Studio y comencé a probar el código. Para asegurarme de que estaba comparando las mismas cosas, comparé el código construido localmente en .NET 4.5 con el código DotNetFiddler.
La única diferencia que noté fue con el bloque de inicio local y la declaración de variable. El compilador local fue un poco más específico al nombrar las variables.
Pero con esa pequeña diferencia, fue tan lejos, tan bueno. Tuve una salida IL equivalente entre el compilador DotNetFiddler y lo que estaba produciendo mi instancia VS local.
Entonces, reconstruí el proyecto dirigido a .NET 4, .NET 3.5 y, en buena medida, el modo de lanzamiento de .NET 3.5.
Y en los tres casos adicionales, la IL generada fue equivalente. La versión específica de .NET no tuvo ningún efecto sobre la IL que se generó en estas muestras.
Para resumir esta aventura: creo que podemos decir con confianza que al compilador no le importa dónde declara el tipo primitivo y que no hay ningún efecto sobre la memoria o el rendimiento con ninguno de los métodos de declaración. Y eso es cierto independientemente de usar un bucle
for
oforeach
.Pensé en ejecutar otro caso más que incorporaba un cierre dentro del
foreach
bucle. Pero usted había preguntado acerca de los efectos de dónde se declaró una variable de tipo primitiva, así que pensé que estaba profundizando demasiado más allá de lo que le interesaba preguntar. La pregunta SO que mencioné anteriormente tiene una gran respuesta que proporciona una buena visión general sobre los efectos de cierre en las variables de iteración foreach.1 Gracias a Andy por proporcionar el enlace original a la pregunta SO que aborda los cierres dentro de los
foreach
bucles.2 Vale la pena señalar que la especificación ECMA-335 aborda esto con la sección I.12.3.2.2 'Variables locales y argumentos'. Tuve que ver la IL resultante y luego leer la sección para que quede claro con respecto a lo que estaba sucediendo. Gracias a Ratchet Freak por señalar eso en el chat.
fuente
foreach
bucle y también verifiqué la versión de .NET objetivo.Dependiendo del compilador que use (ni siquiera sé si C # tiene más de uno), su código se optimizará antes de convertirse en un programa. Un buen compilador verá que reinicia la misma variable cada vez con un valor diferente y administrará el espacio de memoria de manera eficiente.
Si inicializara la misma variable a una constante cada vez, el compilador también la inicializaría antes del ciclo y la referenciaría.
Todo depende de qué tan bien esté escrito su compilador, pero en lo que respecta a los estándares de codificación, las variables siempre deben tener el menor alcance posible . Así que declarar dentro del ciclo es lo que siempre me han enseñado.
fuente
en primer lugar, solo está declarando e inicializando el bucle interno para que cada vez que se repita el bucle se reinicialice "i". En segundo lugar, solo declaras fuera del ciclo.
fuente