Esta es una situación con la que me encuentro con frecuencia como programador sin experiencia y me pregunto sobre todo para un proyecto mío ambicioso y de alta velocidad que estoy tratando de optimizar. Para los principales lenguajes similares a C (C, objC, C ++, Java, C #, etc.) y sus compiladores habituales, ¿estas dos funciones se ejecutarán con la misma eficacia? ¿Hay alguna diferencia en el código compilado?
void foo1(bool flag)
{
if (flag)
{
//Do stuff
return;
}
//Do different stuff
}
void foo2(bool flag)
{
if (flag)
{
//Do stuff
}
else
{
//Do different stuff
}
}
Básicamente, ¿existe alguna vez una bonificación / penalización directa por eficiencia cuando se break
inicia o se return
adelanta? ¿Cómo está involucrado el stackframe? ¿Hay casos especiales optimizados? ¿Hay algún factor (como la inserción o el tamaño de "Hacer cosas") que pueda afectar esto de manera significativa?
Siempre soy un defensor de la legibilidad mejorada sobre las optimizaciones menores (veo mucho foo1 con la validación de parámetros), pero esto surge con tanta frecuencia que me gustaría dejar de lado todas las preocupaciones de una vez por todas.
Y soy consciente de las trampas de la optimización prematura ... uf, esos son algunos recuerdos dolorosos.
EDITAR: Acepté una respuesta, pero la respuesta de EJP explica de manera bastante sucinta por qué el uso de a return
es prácticamente insignificante (en el ensamblaje, return
crea una 'rama' al final de la función, que es extremadamente rápido. La rama altera el registro de la PC y también puede afectar la caché y la canalización, lo cual es bastante minúsculo). Para este caso en particular, literalmente no hace ninguna diferencia porque tanto el if/else
como el return
crean la misma rama hasta el final de la función.
Respuestas:
No hay ninguna diferencia:
Lo que significa que no hay diferencia en el código generado en absoluto, incluso sin optimización en dos compiladores
fuente
something()
siempre se ejecutará. En la pregunta original, OP tieneDo stuff
yDo diffferent stuff
depende de la bandera. No estoy seguro de que el código generado sea el mismo.La respuesta corta es, no hay diferencia. Hazte un favor y deja de preocuparte por esto. El compilador de optimización es casi siempre más inteligente que usted.
Concéntrese en la legibilidad y la facilidad de mantenimiento.
Si desea ver qué sucede, compárelos con optimizaciones y observe la salida del ensamblador.
fuente
x = <some number>
queif(<would've changed>) x = <some number>
las ramas innecesarias pueden doler? Por otro lado, a menos que esto esté dentro del ciclo principal de una operación extremadamente intensiva, tampoco me preocuparía por eso.Respuestas interesantes: aunque estoy de acuerdo con todas ellas (hasta ahora), hay posibles connotaciones para esta pregunta que hasta ahora se han ignorado por completo.
Si el ejemplo simple anterior se amplía con la asignación de recursos y luego la verificación de errores con una posible liberación de recursos resultante, el panorama podría cambiar.
Considere el enfoque ingenuo que pueden adoptar los principiantes:
Lo anterior representaría una versión extrema del estilo de regresar prematuramente. Observe cómo el código se vuelve muy repetitivo y no se puede mantener con el tiempo cuando aumenta su complejidad. Hoy en día, la gente puede usar el manejo de excepciones para detectarlos.
Philip sugirió, después de mirar el ejemplo de goto a continuación, usar un interruptor / caja sin interrupciones dentro del bloque de captura de arriba. Uno podría cambiar (typeof (e)) y luego pasar por
free_resourcex()
alto las llamadas, pero esto no es trivial y necesita consideración de diseño . Y recuerde que un interruptor / caja sin interrupciones es exactamente como el goto con etiquetas encadenadas debajo ...Como señaló Mark B, en C ++ se considera de buen estilo seguir el principio de adquisición de recursos es inicialización , RAII en resumen. La esencia del concepto es utilizar la instanciación de objetos para adquirir recursos. Los recursos se liberan automáticamente tan pronto como los objetos salen del alcance y se llaman a sus destructores. Para los recursos interdependientes, se debe tener especial cuidado para asegurar el orden correcto de desasignación y diseñar los tipos de objetos de manera que los datos requeridos estén disponibles para todos los destructores.
O en días previos a la excepción podría hacer:
Pero este ejemplo demasiado simplificado tiene varios inconvenientes: se puede usar solo si los recursos asignados no dependen entre sí (por ejemplo, no se puede usar para asignar memoria, luego abrir un identificador de archivo y luego leer datos del identificador en la memoria ), y no proporciona códigos de error individuales distinguibles como valores de retorno.
Para mantener el código rápido (!), Compacto y fácilmente legible y extensible, Linus Torvalds impuso un estilo diferente para el código del kernel que se ocupa de los recursos, incluso usando el infame goto de una manera que tiene absolutamente sentido :
La esencia de la discusión sobre las listas de correo del kernel es que la mayoría de las características del lenguaje que son "preferidas" sobre la instrucción goto son gotos implícitos, tales como if / else enormes, en forma de árbol, manejadores de excepciones, declaraciones loop / break / continue, etc. Y los goto en el ejemplo anterior se consideran correctos, ya que están saltando solo una pequeña distancia, tienen etiquetas claras y liberan el código de otro desorden para realizar un seguimiento de las condiciones de error. Esta pregunta también se ha discutido aquí en stackoverflow .
Sin embargo, lo que falta en el último ejemplo es una buena forma de devolver un código de error. Estaba pensando en agregar un
result_code++
después de cadafree_resource_x()
llamada y devolver ese código, pero esto compensa algunas de las ganancias de velocidad del estilo de codificación anterior. Y es difícil devolver 0 en caso de éxito. Quizás soy poco imaginativo ;-)Entonces, sí, creo que hay una gran diferencia en la cuestión de codificar devoluciones prematuras o no. Pero también creo que es evidente solo en un código más complicado que es más difícil o imposible de reestructurar y optimizar para el compilador. Que suele ser el caso una vez que entra en juego la asignación de recursos.
fuente
catch
contiene unaswitch
declaración sin interrupciones en el código de error?Aunque esta no es una gran respuesta, un compilador de producción será mucho mejor optimizando que usted. Favorecería la legibilidad y la capacidad de mantenimiento sobre este tipo de optimizaciones.
fuente
Para ser específico sobre esto,
return
se compilará en una rama hasta el final del método, donde habrá unaRET
instrucción o lo que sea. Si lo deja fuera, el final del bloque antes deelse
se compilará en una rama hasta el final delelse
bloque. Así que puede ver que en este caso específico no hay ninguna diferencia.fuente
Si realmente quiere saber si hay una diferencia en el código compilado para su compilador y sistema en particular, tendrá que compilar y mirar el ensamblado usted mismo.
Sin embargo, en el gran esquema de las cosas, es casi seguro que el compilador puede optimizar mejor que su ajuste fino, e incluso si no puede, es muy poco probable que realmente importe para el rendimiento de su programa.
En su lugar, escriba el código de la manera más clara para que los humanos lo lean y mantengan, y deje que el compilador haga lo que mejor sabe hacer: generar el mejor ensamblado posible a partir de su fuente.
fuente
En su ejemplo, el retorno es notable. ¿Qué le sucede a la persona que realiza la depuración cuando la devolución es una página o dos arriba / abajo donde // ocurren cosas diferentes? Mucho más difícil de encontrar / ver cuando hay más código.
fuente
Estoy totalmente de acuerdo con blueshift: legibilidad y mantenibilidad primero. Pero si está realmente preocupado (o simplemente quiere saber qué está haciendo su compilador, lo que definitivamente es una buena idea a largo plazo), debería buscarlo usted mismo.
Esto significará usar un descompilador o mirar la salida del compilador de bajo nivel (por ejemplo, lenguaje de ensamblaje). En C #, o cualquier lenguaje .Net, las herramientas documentadas aquí le darán lo que necesita.
Pero como usted mismo ha observado, probablemente se trate de una optimización prematura.
fuente
De Clean Code: un manual de artesanía de software ágil
en el código solo hará que el lector navegue a la función y pierda el tiempo leyendo foo (bandera booleana)
Una base de código mejor estructurada le dará una mejor oportunidad de optimizar el código.
fuente
Una escuela de pensamiento (no recuerdo al cabeza de huevo que la propuso en este momento) es que todas las funciones solo deben tener un punto de retorno desde un punto de vista estructural para que el código sea más fácil de leer y depurar. Eso, supongo, es más para programar el debate religioso.
Una razón técnica por la que puede querer controlar cuándo y cómo sale una función que rompe esta regla es cuando está codificando aplicaciones en tiempo real y desea asegurarse de que todas las rutas de control a través de la función tomen la misma cantidad de ciclos de reloj para completarse.
fuente
Me alegra que hayas mencionado esta pregunta. Siempre debe usar las sucursales en lugar de una devolución anticipada. ¿Por qué detenerse ahí? Combine todas sus funciones en una sola si puede (al menos tanto como pueda). Esto es factible si no hay recursividad. Al final, tendrá una función principal masiva, pero eso es lo que necesita / desea para este tipo de cosas. Luego, cambie el nombre de sus identificadores para que sean lo más cortos posible. De esa manera, cuando se ejecuta su código, se dedica menos tiempo a leer nombres. Siguiente hacer ...
fuente