¿Deberían las declaraciones condicionales no triviales moverse a la sección de inicialización de los bucles?

21

Tengo esta idea de esta pregunta en stackoverflow.com

El siguiente patrón es común:

final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
  //...do something
}

El punto que estoy tratando de hacer es que la declaración condicional es algo complicado y no cambia.

¿Es mejor declararlo en la sección de inicialización del ciclo, como tal?

final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
  //...do something
}

¿Está esto más claro?

¿Qué pasa si la expresión condicional es simple como

final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
  //...do something
}
Celeritas
fuente
47
¿Por qué no simplemente moverlo a la línea antes del bucle, también puede darle un nombre razonable?
jonrsharpe
2
@Mehrdad: No son equivalentes. Si xes grande en magnitud, Math.floor(Math.sqrt(x))+1es igual a Math.floor(Math.sqrt(x)). :-)
R ..
55
@jonrsharpe Porque eso ampliaría el alcance de la variable. No digo necesariamente que sea una buena razón para no hacerlo, pero esa es la razón por la que algunas personas no lo hacen.
Kevin Krumwiede
3
@KevinKrumwiede Si el alcance es una preocupación, limítelo colocando el código en su propio bloque, por ejemplo, { x=whatever; for (...) {...} }o, mejor aún, considere si hay suficiente actividad que necesita ser una función separada.
Blrfl
2
@jonrsharpe También puede darle un nombre razonable al declararlo en la sección init. No digo que lo pondría allí; aún es más fácil de leer si está separado.
JollyJoker

Respuestas:

62

Lo que haría es algo como esto:

void doSomeThings() {
    final x = 10;//whatever constant value
    final limit = Math.floor(Math.sqrt(x)) + 1;
    for(int i = 0; i < limit; i++) {
         //...do something
    }
}

Honestamente, la única buena razón para meter la inicialización j(ahora limit) en el encabezado del bucle es mantenerlo con el alcance correcto. Todo lo que se necesita para hacer que no sea un problema es un buen alcance cerrado.

Puedo apreciar el deseo de ser rápido pero no sacrificar la legibilidad sin una buena razón.

Claro, el compilador puede optimizar, inicializar varios vars puede ser legal, pero los bucles son lo suficientemente difíciles de depurar como es. Por favor, sé amable con los humanos. Si esto realmente nos está frenando, es bueno entenderlo lo suficiente como para solucionarlo.

naranja confitada
fuente
Buen punto. Supongo que no se molestará con otra variable si la expresión es algo simple, por ejemplo, for(int i = 0; i < n*n; i++){...}¿no le asignaría n*nuna variable, verdad?
Celeritas
1
Lo haría y tengo. Pero no por la velocidad. Por legibilidad. El código legible tiende a ser rápido por sí solo.
candied_orange
1
Incluso la cuestión del alcance desaparece si marca como una constante ( final). ¿A quién le importa si una constante que tiene la aplicación del compilador que evita que cambie sea accesible más adelante en la función?
jpmc26
Creo que una gran cosa es lo que esperas que pase. Sé que el ejemplo usa el sqrt pero ¿y si fuera otra función? ¿Es pura la función? ¿Siempre esperas los mismos valores? ¿Hay efectos secundarios? ¿Los efectos secundarios son algo que tiene la intención de suceder en cada iteración?
Pieter B
38

Un buen compilador generará el mismo código de cualquier manera, por lo que si va por el rendimiento, solo haga un cambio si está en un ciclo crítico y realmente lo ha perfilado y descubrió que hace la diferencia. Incluso si el compilador no puede optimizarlo, como la gente ha señalado en los comentarios sobre el caso de las llamadas a funciones, en la gran mayoría de las situaciones, la diferencia de rendimiento será demasiado pequeña para que valga la pena la consideración de un programador.

Sin embargo...

No debemos olvidar que el código es principalmente un medio de comunicación entre humanos, y ambas opciones no se comunican muy bien con otros humanos. El primero da la impresión de que la expresión debe calcularse en cada iteración, y el segundo en la sección de inicialización implica que se actualizará en algún lugar dentro del ciclo, donde es realmente constante en todo momento.

En realidad, preferiría que se extrajera por encima del bucle y se hiciera finalpara que quede claro de forma inmediata y abundante para cualquiera que lea el código. Eso tampoco es ideal porque aumenta el alcance de la variable, pero su función de cierre no debe contener mucho más que ese ciclo de todos modos.

Karl Bielefeldt
fuente
55
Una vez que comienza a incluir llamadas de función, el compilador se vuelve mucho más difícil de optimizar. El compilador tendría que tener un conocimiento especial de que Math.sqrt no tiene efectos secundarios.
Peter Green
@PeterGreen Si esto es Java, la JVM puede resolverlo, pero puede llevar un tiempo.
chrylis -on strike-
La pregunta está etiquetada tanto en C ++ como en Java. No sé qué tan avanzado es JVM de Java y cuándo puede y no puede resolverlo, pero sí sé que los compiladores de C ++ en general no pueden resolverlo, pero una función es pura a menos que tenga una anotación no estándar que indique al compilador entonces, o el cuerpo de la función es visible y todas las funciones llamadas indirectamente pueden ser detectadas como puras por el mismo criterio. Nota: sin efectos secundarios no es suficiente para sacarlo de la condición de bucle. Las funciones que dependen del estado global tampoco se pueden mover fuera del bucle si el cuerpo del bucle puede modificar el estado.
hvd
Se vuelve interesante una vez que Math.sqrt (x) se reemplaza por Mymodule.SomeNonPureMethodWithSideEffects (x).
Pieter B
9

Como dijo @Karl Bielefeldt en su respuesta, esto generalmente no es un problema.

Sin embargo, en un momento fue un problema común en C y C ++, y surgió un truco para evitar el problema sin reducir la legibilidad del código: iterar hacia atrás, hacia abajo0 .

final x = 10;//whatever constant value
for(int i = Math.floor(Math.sqrt(x)); i >= 0; i--) {
  //...do something
}

Ahora, el condicional en cada iteración es el >= 0que cada compilador compilará en 1 o 2 instrucciones de ensamblaje. Cada CPU fabricada en las últimas décadas debería tener controles básicos como estos; haciendo una verificación rápida en mi máquina x64, veo que esto se convierte previsiblemente en cmpl $0x0, -0x14(%rbp)(valor de comparación larga int 0 vs. registro rbp compensado -14) y jl 0x100000f59(salte a las instrucciones que siguen al ciclo si la comparación anterior era verdadera para "2nd-arg <1er argumento ") .

Tenga en cuenta que quité el + 1de Math.floor(Math.sqrt(x)) + 1; para que las matemáticas funcionen, el valor inicial debe ser int i = «iterationCount» - 1. También vale la pena señalar que su iterador debe estar firmado; unsigned intno funcionará y probablemente avisará al compilador.

Después de programar en lenguajes basados ​​en C durante ~ 20 años, ahora solo escribo bucles de iteración de índice inverso a menos que haya una razón específica para iterar de índice directo. Además de verificaciones más simples en los condicionales, la iteración inversa a menudo también deja de lado lo que de otro modo serían problemáticas mutaciones de matriz mientras itera.

Slipp D. Thompson
fuente
1
Todo lo que escribes es técnicamente correcto. Sin embargo, el comentario sobre las instrucciones puede ser engañoso, ya que todas las CPU modernas también se han diseñado para funcionar bien con bucles iterativos hacia adelante, independientemente de si tienen una instrucción especial para ellos. En cualquier caso, la mayor parte del tiempo generalmente se pasa dentro del bucle, sin realizar la iteración.
Jørgen Fogh
44
Cabe señalar que las optimizaciones modernas del compilador están diseñadas teniendo en cuenta el código "normal". En la mayoría de los casos, el compilador generará un código igual de rápido, independientemente de si utiliza "trucos" de optimización o no. Pero algunos trucos pueden dificultar la optimización dependiendo de lo complicados que sean. Si el uso de ciertos trucos hace que su código sea más legible o ayuda a detectar errores comunes, eso es genial, pero no se engañe pensando "si escribo código de esta manera será más rápido". Escriba el código como lo haría normalmente, luego haga un perfil para encontrar los lugares donde necesita optimizar.
0x5453
También tenga en cuenta que los unsignedcontadores funcionarán aquí si modifica el cheque (la forma más fácil es agregar el mismo valor a ambos lados); por ejemplo, para cualquier decremento Dec, la verificación (i + Dec) >= Decsiempre debe tener el mismo resultado que la signedverificación i >= 0, con ambos signedy unsignedcontadores, siempre que el lenguaje tenga reglas envolventes bien definidas para las unsignedvariables (específicamente, -n + n == 0debe ser cierto para ambas signedy unsigned). Sin embargo, tenga en cuenta que esto puede ser menos eficiente que una >=0verificación firmada , si el compilador no tiene una optimización para ello.
Justin Time - Restablece a Mónica el
1
@JustinTime Sí, el requisito firmado es la parte más difícil; agregar una Decconstante tanto al valor inicial como al valor final funciona, pero lo hace mucho menos intuitivo, y si se usa icomo un índice de matriz, entonces también necesitaría hacer un unsigned int arrayI = i - Dec;en el cuerpo del bucle. Solo uso la iteración hacia adelante cuando estoy atascado con un iterador sin signo; a menudo con un i <= count - 1condicional para mantener la lógica paralela a los bucles de iteración inversa.
Slipp D. Thompson
1
@ SlippD.Thompson No me refiero a agregar Decespecíficamente a los valores iniciales y finales, sino a cambiar el control de condición Decen ambos lados. for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/ Esto le permite usar inormalmente dentro del ciclo, al tiempo que garantiza que el valor más bajo posible en el lado izquierdo de la condición es 0(para evitar que interfiera la envoltura). Sin embargo, definitivamente es mucho más simple usar la iteración hacia adelante cuando se trabaja con contadores sin firmar.
Justin Time - Restablece a Mónica el
3

Se vuelve interesante una vez que Math.sqrt (x) se reemplaza por Mymodule.SomeNonPureMethodWithSideEffects (x).

Básicamente mi modus operandi es: si se espera que algo siempre dé el mismo valor, solo evalúelo una vez. Por ejemplo, List.Count, si se supone que la lista no cambia durante la operación del ciclo, entonces obtenga el conteo fuera del ciclo en otra variable.

Algunos de estos "recuentos" pueden ser sorprendentemente caros, especialmente cuando se trata de bases de datos. Incluso si está trabajando en un conjunto de datos que no se supone que cambie durante la iteración de la lista.

Pieter B
fuente
Cuando el recuento es costoso, no deberías usarlo en absoluto. En su lugar, debería estar haciendo el equivalente defor( auto it = begin(dataset); !at_end(it); ++it )
Ben Voigt
@BenVoigt Usar un iterador es definitivamente la mejor manera para esas operaciones. Simplemente lo mencioné para ilustrar mi punto sobre el uso de métodos no puros con efectos secundarios.
Pieter B
0

En mi opinión, esto es muy específico del idioma. Por ejemplo, si usa C ++ 11, sospecharía que si la verificación de condición fuera una constexprfunción, es muy probable que el compilador optimice las ejecuciones múltiples, ya que sabe que producirá el mismo valor cada vez.

Sin embargo, si la llamada a la función es una función de biblioteca que no es constexprel compilador, es casi seguro que la ejecutará en cada iteración, ya que no puede deducir esto (a menos que esté en línea y, por lo tanto, pueda deducirse como puro).

Sé menos sobre Java, pero dado que está compilado JIT, supongo que el compilador tiene suficiente información en tiempo de ejecución para probablemente en línea y optimizar la condición. Pero esto dependería de un buen diseño del compilador y el compilador que decidiera que este bucle era una prioridad de optimización que solo podemos suponer.

En lo personal creo que es un poco más elegante para poner la condición dentro del bucle, si se puede, pero si es complejo voy a escribir en una constexpro inlinefunción, o sus lenguas equivalant dar a entender que la función es puro y optimisable. Esto hace que la intención sea obvia y mantiene el estilo de bucle idiomático sin crear una gran línea ilegible. También le da un nombre a la condición de verificación si es su propia función para que los lectores puedan ver inmediatamente de forma lógica para qué sirve la verificación sin leerla si es compleja.

La realidad
fuente