java - ¿Diferencia entre declarar variables antes o en bucle?
performance loops (24)
Siempre me he preguntado si, en general, declarar una variable desechable antes de un bucle, en lugar de repetidamente dentro del bucle, ¿hace alguna diferencia (rendimiento)? Un ejemplo (bastante inútil) en Java:
a) declaración antes del bucle:
double intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
b) declaración (repetidamente) dentro del bucle:
for(int i=0; i < 1000; i++){
double intermediateResult = i;
System.out.println(intermediateResult);
}
¿Cuál es mejor, a o b ?
Sospecho que la declaración de variables repetidas (ejemplo b ) genera más sobrecarga en teoría , pero que los compiladores son lo suficientemente inteligentes como para que no importe. El ejemplo b tiene la ventaja de ser más compacto y limitar el alcance de la variable a donde se usa. Aún así, tiendo a codificar según el ejemplo a .
Edit: Estoy especialmente interesado en el caso de Java.
¿Cuál es mejor, a o b ?
Desde una perspectiva de rendimiento, tendrías que medirlo. (Y en mi opinión, si puedes medir una diferencia, el compilador no es muy bueno).
Desde una perspectiva de mantenimiento, b es mejor. Declare e inicialice las variables en el mismo lugar, en el ámbito más estrecho posible. No deje un hueco entre la declaración y la inicialización, y no contamine los espacios de nombres que no necesita.
A) es una apuesta segura que B) ......... Imagina que estás inicializando la estructura en un bucle en lugar de ''int'' o ''float'', ¿entonces qué?
me gusta
typedef struct loop_example{
JXTZ hi; // where JXTZ could be another type...say closed source lib
// you include in Makefile
}loop_example_struct;
//then....
int j = 0; // declare here or face c99 error if in loop - depends on compiler setting
for ( ;j++; )
{
loop_example loop_object; // guess the result in memory heap?
}
Ciertamente está obligado a enfrentar problemas con fugas de memoria. Por lo tanto, creo que ''A'' es una apuesta más segura, mientras que ''B'' es vulnerable a la acumulación de memoria, especialmente al trabajar con bibliotecas de código fuente cercanas.
Bueno, corrí tus ejemplos A y B 20 veces cada uno, repitiendo 100 millones de veces (JVM - 1.5.0)
A: tiempo de ejecución promedio: .074 seg.
B: tiempo medio de ejecución: .067 seg.
Para mi sorpresa B fue un poco más rápido. Tan rápido como las computadoras son ahora difíciles de decir si pudiera medir esto con precisión. Yo también lo codificaría de la manera A pero diría que realmente no importa.
Bueno, siempre se podría hacer un alcance para eso:
{ //Or if(true) if the language doesn''t support making scopes like this
double intermediateResult;
for (int i=0; i<1000; i++) {
intermediateResult = i;
System.out.println(intermediateResult);
}
}
De esta manera solo declaras la variable una vez, y morirá cuando dejes el bucle.
Como regla general, declaro mis variables en el ámbito más interno posible. Entonces, si no estás utilizando intermediosResultado fuera del bucle, entonces me quedo con B.
Creo que depende del compilador y es difícil dar una respuesta general.
Depende del idioma y del uso exacto. Por ejemplo, en C # 1 no hizo ninguna diferencia. En C # 2, si la variable local es capturada por un método anónimo (o la expresión lambda en C # 3) puede hacer una diferencia muy significativa.
Ejemplo:
using System;
using System.Collections.Generic;
class Test
{
static void Main()
{
List<Action> actions = new List<Action>();
int outer;
for (int i=0; i < 10; i++)
{
outer = i;
int inner = i;
actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
}
foreach (Action action in actions)
{
action();
}
}
}
Salida:
Inner=0, Outer=9
Inner=1, Outer=9
Inner=2, Outer=9
Inner=3, Outer=9
Inner=4, Outer=9
Inner=5, Outer=9
Inner=6, Outer=9
Inner=7, Outer=9
Inner=8, Outer=9
Inner=9, Outer=9
La diferencia es que todas las acciones capturan la misma variable outer
, pero cada una tiene su propia variable inner
separada.
Desde una perspectiva de rendimiento, el exterior es (mucho) mejor.
public static void outside() {
double intermediateResult;
for(int i=0; i < Integer.MAX_VALUE; i++){
intermediateResult = i;
}
}
public static void inside() {
for(int i=0; i < Integer.MAX_VALUE; i++){
double intermediateResult = i;
}
}
Ejecuté ambas funciones 1 billón de veces cada una. Fuera () tomó 65 milisegundos. dentro () tomó 1.5 segundos.
En mi opinión, b es la mejor estructura. En a, el último valor de intermediosResultado se mantiene una vez finalizado el bucle.
Edición: Esto no hace mucha diferencia con los tipos de valor, pero los tipos de referencia pueden ser algo pesados. Personalmente, me gusta que las variables se eliminen de referencia tan pronto como sea posible para la limpieza, yb lo hace por usted,
Es dependiente del idioma: IIRC C # optimiza esto, por lo que no hay ninguna diferencia, pero JavaScript (por ejemplo) hará la asignación de memoria completa cada vez.
Es una pregunta interesante. Desde mi experiencia, hay una pregunta fundamental a considerar cuando se debate este asunto por un código:
¿Hay alguna razón por la cual la variable tendría que ser global?
Tiene sentido declarar la variable solo una vez, globalmente, a diferencia de muchas veces localmente, porque es mejor para organizar el código y requiere menos líneas de código. Sin embargo, si solo fuera necesario declararlo localmente dentro de un método, lo inicializaría en ese método para que quede claro que la variable es exclusivamente relevante para ese método. Tenga cuidado de no llamar a esta variable fuera del método en el que se inicializa si elige la última opción; su código no sabrá de lo que está hablando e informará un error.
Además, como nota al margen, no duplique nombres de variables locales entre diferentes métodos, incluso si sus propósitos son casi idénticos; simplemente se vuelve confuso.
Este es un gotcha en VB.NET. El resultado de Visual Basic no reinicializará la variable en este ejemplo:
For i as Integer = 1 to 100
Dim j as Integer
Console.WriteLine(j)
j = i
Next
'' Output: 0 1 2 3 4...
Esto imprimirá 0 la primera vez (¡las variables de Visual Basic tienen valores predeterminados cuando se declaran!), Pero cada vez lo hago.
Sin embargo, si agrega a = 0
, obtiene lo que podría esperar:
For i as Integer = 1 to 100
Dim j as Integer = 0
Console.WriteLine(j)
j = i
Next
''Output: 0 0 0 0 0...
Hay una diferencia en C # si está utilizando la variable en un lambda, etc. Pero en general el compilador básicamente hará lo mismo, asumiendo que la variable solo se usa dentro del bucle.
Dado que son básicamente los mismos: tenga en cuenta que la versión b hace que sea mucho más obvio para los lectores que la variable no es, y no puede, usarse después del bucle. Además, la versión b es mucho más fácil de refaccionar. Es más difícil extraer el cuerpo del bucle en su propio método en la versión a. Además, la versión b le asegura que no hay ningún efecto secundario en dicha refactorización.
Por lo tanto, la versión a me molesta hasta el final, porque no tiene ningún beneficio y hace que sea mucho más difícil razonar sobre el código ...
Hice una prueba simple:
int b;
for (int i = 0; i < 10; i++) {
b = i;
}
vs
for (int i = 0; i < 10; i++) {
int b = i;
}
Compilé estos códigos con gcc - 5.2.0. Y luego desmonté el main () de estos dos códigos y ese es el resultado:
1º:
0x00000000004004b6 <+0>: push rbp
0x00000000004004b7 <+1>: mov rbp,rsp
0x00000000004004ba <+4>: mov DWORD PTR [rbp-0x4],0x0
0x00000000004004c1 <+11>: jmp 0x4004cd <main+23>
0x00000000004004c3 <+13>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004004c6 <+16>: mov DWORD PTR [rbp-0x8],eax
0x00000000004004c9 <+19>: add DWORD PTR [rbp-0x4],0x1
0x00000000004004cd <+23>: cmp DWORD PTR [rbp-0x4],0x9
0x00000000004004d1 <+27>: jle 0x4004c3 <main+13>
0x00000000004004d3 <+29>: mov eax,0x0
0x00000000004004d8 <+34>: pop rbp
0x00000000004004d9 <+35>: ret
vs
2º
0x00000000004004b6 <+0>: push rbp
0x00000000004004b7 <+1>: mov rbp,rsp
0x00000000004004ba <+4>: mov DWORD PTR [rbp-0x4],0x0
0x00000000004004c1 <+11>: jmp 0x4004cd <main+23>
0x00000000004004c3 <+13>: mov eax,DWORD PTR [rbp-0x4]
0x00000000004004c6 <+16>: mov DWORD PTR [rbp-0x8],eax
0x00000000004004c9 <+19>: add DWORD PTR [rbp-0x4],0x1
0x00000000004004cd <+23>: cmp DWORD PTR [rbp-0x4],0x9
0x00000000004004d1 <+27>: jle 0x4004c3 <main+13>
0x00000000004004d3 <+29>: mov eax,0x0
0x00000000004004d8 <+34>: pop rbp
0x00000000004004d9 <+35>: ret
Cuales son exactamente el mismo resultado. ¿No es una prueba de que los dos códigos producen lo mismo?
Incluso si sé que mi compilador es lo suficientemente inteligente, no me gustaría confiar en él, y usaré la variante a).
La variante b) tiene sentido para mí solo si necesita desesperadamente hacer que el resultado intermedio no esté disponible después del cuerpo del bucle. Pero no puedo imaginar una situación tan desesperada, de todos modos ...
EDIT: Jon Skeet hizo un muy buen punto, mostrando que la declaración de variables dentro de un bucle puede hacer una diferencia semántica real.
Lo siguiente es lo que escribí y compilé en .NET.
double r0;
for (int i = 0; i < 1000; i++) {
r0 = i*i;
Console.WriteLine(r0);
}
for (int j = 0; j < 1000; j++) {
double r1 = j*j;
Console.WriteLine(r1);
}
Esto es lo que obtengo de .NET Reflector cuando CIL se vuelve a convertir en código.
for (int i = 0; i < 0x3e8; i++)
{
double r0 = i * i;
Console.WriteLine(r0);
}
for (int j = 0; j < 0x3e8; j++)
{
double r1 = j * j;
Console.WriteLine(r1);
}
Así que ambos se ven exactamente iguales después de la compilación. En los idiomas administrados, el código se convierte en código CL / byte y en el momento de la ejecución se convierte en lenguaje de máquina. Por lo tanto, en el lenguaje de máquina no se puede crear un doble en la pila. Puede que solo sea un registro, ya que el código refleja que es una variable temporal para la función WriteLine
. Hay un conjunto de reglas de optimización solo para bucles. Así que el hombre promedio no debería preocuparse por eso, especialmente en los idiomas administrados. Hay casos en los que puede optimizar el código de administración, por ejemplo, si tiene que concatenar un gran número de cadenas utilizando solo la string a; a+=anotherstring[i]
string a; a+=anotherstring[i]
vs utilizando StringBuilder
. Hay una gran diferencia en el rendimiento entre ambos. Hay muchos de estos casos en los que el compilador no puede optimizar su código, ya que no puede entender qué se pretende en un ámbito más amplio. Pero puede optimizar bastante las cosas básicas para ti.
Mi práctica es la siguiente:
si el tipo de variable es simple (int, double, ...) prefiero la variante b (adentro).
Motivo: reduciendo el alcance de la variable.si el tipo de variable no es simple (algún tipo de
class
ostruct
) prefiero la variante a (fuera).
Motivo: reducir el número de llamadas ctor-dtor.
Probé JS con Node 4.0.0 si alguien está interesado. La declaración fuera del bucle dio como resultado una mejora del rendimiento de ~ .5 ms en promedio en más de 1000 intentos con 100 millones de iteraciones de bucle por ensayo. Así que diré, adelante, y escríbalo de la manera más legible / mantenible que es B, imo. Yo pondría mi código en un violín, pero usé el módulo Nodo de rendimiento ahora. Aquí está el código:
var now = require("../node_modules/performance-now")
// declare vars inside loop
function varInside(){
for(var i = 0; i < 100000000; i++){
var temp = i;
var temp2 = i + 1;
var temp3 = i + 2;
}
}
// declare vars outside loop
function varOutside(){
var temp;
var temp2;
var temp3;
for(var i = 0; i < 100000000; i++){
temp = i
temp2 = i + 1
temp3 = i + 2
}
}
// for computing average execution times
var insideAvg = 0;
var outsideAvg = 0;
// run varInside a million times and average execution times
for(var i = 0; i < 1000; i++){
var start = now()
varInside()
var end = now()
insideAvg = (insideAvg + (end-start)) / 2
}
// run varOutside a million times and average execution times
for(var i = 0; i < 1000; i++){
var start = now()
varOutside()
var end = now()
outsideAvg = (outsideAvg + (end-start)) / 2
}
console.log(''declared inside loop'', insideAvg)
console.log(''declared outside loop'', outsideAvg)
Probé lo mismo en Go, y comparé la salida del compilador usando la go tool compile -S
with go 1.9.4
Diferencia cero, según la salida del ensamblador.
Siempre he pensado que si declara sus variables dentro de su bucle, entonces está perdiendo memoria. Si tienes algo como esto:
for(;;) {
Object o = new Object();
}
Entonces, no solo es necesario crear el objeto para cada iteración, sino que debe haber una nueva referencia asignada para cada objeto. Parece que si el recolector de basura es lento, entonces tendrá un montón de referencias pendientes que deben limpiarse.
Sin embargo, si tienes esto:
Object o;
for(;;) {
o = new Object();
}
Entonces solo estás creando una única referencia y asignándole un nuevo objeto cada vez. Claro, puede tardar un poco más en salir del alcance, pero solo hay una referencia pendiente con la que lidiar.
Siempre usaría A (en lugar de confiar en el compilador) y también podría volver a escribir para:
for(int i=0, double intermediateResult=0; i<1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
Esto todavía restringe el resultado intermediateResult
al alcance del bucle, pero no se vuelve a declarar durante cada iteración.
Sospecho que algunos compiladores podrían optimizar ambos para ser el mismo código, pero ciertamente no todos. Así que diría que estás mejor con el primero. La única razón para esto último es si quiere asegurarse de que la variable declarada se use solo dentro de su bucle.
Un compañero de trabajo prefiere la primera forma, diciéndole que es una optimización, que prefiere reutilizar una declaración.
Prefiero el segundo (y trato de persuadir a mi compañero de trabajo! ;-)), habiendo leído eso:
- Reduce el alcance de las variables a donde se necesitan, lo que es bueno.
- Java optimiza lo suficiente como para no hacer una diferencia significativa en el rendimiento. IIRC, quizás la segunda forma sea aún más rápida.
De todos modos, cae en la categoría de optimización prematura que se basa en la calidad del compilador y / o JVM.
esta es la mejor forma
double intermediateResult;
int i = byte.MinValue;
for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}
1) de esta manera se declara una vez, tanto la variable, como no cada una para el ciclo. 2) la asignación es más completa que cualquier otra opción. 3) Entonces, la regla de mejor práctica es cualquier declaración fuera de la iteración para.