c# java c++ c compiler-optimization

c# - Eficiencia del retorno prematuro en una función



java c++ (11)

Aunque esto no es una gran respuesta, un compilador de producción va a ser mucho mejor para optimizar que tú. Yo preferiría la legibilidad y el mantenimiento sobre este tipo de optimizaciones.

Esta es una situación que encuentro frecuentemente como programador inexperto y me estoy preguntando sobre todo por un proyecto mío ambicioso y de alta velocidad que estoy tratando de optimizar. Para los principales lenguajes tipo 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, ¿hay alguna vez un bono / penalización de eficiencia directa al break o return temprano? ¿Cómo está involucrado el marco de pila? ¿Hay casos especiales optimizados? ¿Hay algún factor (como la creación de líneas o el tamaño de "Hacer cosas") que pueda afectarlo significativamente?

Siempre soy un defensor de la legibilidad mejorada frente a optimizaciones menores (veo mucho foo1 con la validación de parámetros), pero esto aparece 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 ... ugh, esos son algunos recuerdos dolorosos.

EDITAR: Acepté una respuesta, pero la respuesta de EJP explica muy sucintamente por qué el uso de un return es prácticamente insignificante (en el ensamblado, el return crea una ''rama'' al final de la función, que es extremadamente rápido. La rama altera la PC registrarse y también puede afectar el caché y la canalización, que es bastante minúsculo). Para este caso en particular, literalmente no hace diferencia porque tanto el if/else como el return crean la misma rama hasta el final de la función.


Del Código Limpio: Un Manual de Artesanía Agile Software

Los argumentos de bandera son feos. Pasar un booleano a una función es una práctica realmente terrible. Inmediatamente complica la firma del método, proclamando en voz alta que esta función hace más de una cosa. ¡Hace una cosa si la bandera es verdadera y otra si la bandera es falsa!

foo(true);

en el código solo hará que el lector navegue a la función y pierda el tiempo leyendo foo (indicador booleano)

Una base de código mejor estructurada le dará una mejor oportunidad para optimizar el código.


En su ejemplo, la devolución es notable. ¿Qué le sucede a la persona que está depurando cuando el resultado es una o dos páginas arriba / abajo donde // ocurre algo diferente? Mucho más difícil de encontrar / ver cuando hay más código.

void foo1(bool flag) { if (flag) { //Do stuff return; } //Do different stuff } void foo2(bool flag) { if (flag) { //Do stuff } else { //Do different stuff } }


Estoy totalmente de acuerdo con el cambio de estilo: primero, legibilidad y mantenimiento. Pero si está realmente preocupado (o simplemente quiere saber qué está haciendo su compilador, que definitivamente es una buena idea a largo plazo), debe 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 en cualquier lenguaje .Net, las herramientas documentadas aquí le darán lo que necesita.

Pero como usted mismo ha observado, esta es probablemente una optimización prematura.


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 mantenibilidad.

Si desea ver qué sucede, compárelos con optimizaciones y observe la salida del ensamblador.


Me alegra que hayas planteado esta pregunta. Siempre debe usar las ramas en un regreso anticipado. ¿Por qué parar ahí? Combina todas tus funciones en una si puedes (al menos tanto como puedas). Esto es factible si no hay recursión. Al final, tendrá una función principal masiva, pero eso es lo que necesita / desea para este tipo de cosas. Después, cambie el nombre de sus identificadores para que sean lo más cortos posible. De esta forma, cuando se ejecuta el código, se gasta menos tiempo leyendo nombres. Siguiente hacer ...


No hay diferencia en absoluto:

=====> cat test_return.cpp extern void something(); extern void something2(); void test(bool b) { if(b) { something(); } else something2(); } =====> cat test_return2.cpp extern void something(); extern void something2(); void test(bool b) { if(b) { something(); return; } something2(); } =====> rm -f test_return.s test_return2.s =====> g++ -S test_return.cpp =====> g++ -S test_return2.cpp =====> diff test_return.s test_return2.s =====> rm -f test_return.s test_return2.s =====> clang++ -S test_return.cpp =====> clang++ -S test_return2.cpp =====> diff test_return.s test_return2.s =====>

Lo que significa que no hay diferencia en el código generado, incluso sin optimización en dos compiladores


Para ser específico al respecto, la return se compilará en una rama hasta el final del método, donde habrá una instrucción RET o lo que sea. Si lo dejas afuera, el final del bloque antes que el else se compilará en una rama al final del bloque else . Entonces puedes ver que en este caso específico no hace diferencia alguna.


Respuestas interesantes: aunque estoy de acuerdo con todos ellos (hasta ahora), hay posibles connotaciones para esta pregunta que hasta ahora han sido completamente ignoradas.

Si el ejemplo simple anterior se amplía con la asignación de recursos, y luego la comprobación de errores con una posible liberación de recursos resultante, la imagen podría cambiar.

Considere el enfoque ingenuo que pueden tomar los principiantes:

int func(..some parameters...) { res_a a = allocate_resource_a(); if (!a) { return 1; } res_b b = allocate_resource_b(); if (!b) { free_resource_a(a); return 2; } res_c c = allocate_resource_c(); if (!c) { free_resource_b(b); free_resource_a(a); return 3; } do_work(); free_resource_c(c); free_resource_b(b); free_resource_a(a); return 0; }

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 puede mantenerse con el tiempo cuando crece su complejidad. Hoy en día las personas pueden usar el manejo de excepciones para atraparlos.

int func(..some parameters...) { res_a a; res_b b; res_c c; try { a = allocate_resource_a(); # throws ExceptionResA b = allocate_resource_b(); # throws ExceptionResB c = allocate_resource_c(); # throws ExceptionResC do_work(); } catch (ExceptionBase e) { # Could use type of e here to distinguish and # use different catch phrases here # class ExceptionBase must be base class of ExceptionResA/B/C if (c) free_resource_c(c); if (b) free_resource_b(b); if (a) free_resource_a(a); throw e } return 0; }

Philip sugirió, después de mirar el siguiente ejemplo, usar un interruptor / caja sin interrupción dentro del bloque catch anterior. Uno podría cambiar (typeof (e)) y luego pasar por las llamadas free_resourcex() , pero esto no es trivial y necesita consideración de diseño . Y recuerde que un interruptor / caja sin roturas es exactamente igual que el goto con etiquetas en cadena debajo ...

Como señaló Mark B, en C ++ se considera un buen estilo para seguir el principio de Inicialización de recursos, Inicialización , RAII en resumen. La esencia del concepto es usar instanciación de objetos para adquirir recursos. Los recursos se liberan automáticamente tan pronto como los objetos salgan del alcance y se invoquen sus destructores. Para los recursos interdependientes, se debe tener especial cuidado para asegurar el orden correcto de desasignación y para diseñar los tipos de objetos de forma tal que los datos requeridos estén disponibles para todos los destructores.

O en días previos a la excepción podría hacer:

int func(..some parameters...) { res_a a = allocate_resource_a(); res_b b = allocate_resource_b(); res_c c = allocate_resource_c(); if (a && b && c) { do_work(); } if (c) free_resource_c(c); if (b) free_resource_b(b); if (a) free_resource_a(a); return 0; }

Pero este ejemplo demasiado simplificado tiene varios inconvenientes: se puede usar solo si los recursos asignados no dependen el uno del otro (por ejemplo, no se puede usar para asignar memoria, luego abrir un identificador de archivo y luego leer los datos del identificador en la memoria) ), y no proporciona códigos de error individuales y distinguibles como valores de retorno.

Para mantener el código rápido (!), Compacto, fácil de leer y extensible, Linus Torvalds impone un estilo diferente para el código kernel que trata con los recursos, incluso utilizando el infame goto de una manera que tiene mucho sentido :

int func(..some parameters...) { res_a a; res_b b; res_c c; a = allocate_resource_a() || goto error_a; b = allocate_resource_b() || goto error_b; c = allocate_resource_c() || goto error_c; do_work(); error_c: free_resource_c(c); error_b: free_resource_b(b); error_a: free_resource_a(a); return 0; }

La esencia de la discusión en las listas de correo del kernel es que la mayoría de las características del lenguaje que son "preferidas" sobre la declaración goto son implícitos, tales como enormes, como si fuesen / else, manejadores de excepciones, instrucciones loop / break / continue, etc. Y los goto en el ejemplo anterior se consideran correctos, ya que solo saltan una pequeña distancia, tienen etiquetas claras y liberan el código de otro desorden para hacer un seguimiento de las condiciones de error. Esta pregunta también se ha discutido aquí en .

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 cada llamada a free_resource_x() , 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. Tal vez no soy imaginativo ;-)

Entonces, sí, creo que hay una gran diferencia en la cuestión de la codificación de los beneficios prematuros o no. Pero también creo que es evidente solo en el código más complicado que es más difícil o imposible de reestructurar y optimizar para el compilador. Lo cual suele ser el caso una vez que la asignación de recursos entra en juego.


Si realmente desea saber si existe una diferencia en el código compilado para su compilador y sistema en particular, tendrá que compilar y mirar el ensamble usted mismo.

Sin embargo, en el gran esquema de 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 lo mantengan, y deje que el compilador haga lo que mejor hace: Genere el mejor ensamblaje que pueda de su fuente.


Una escuela de pensamiento (no recuerdo el nombre original que lo 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 desea 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 tarden el mismo número de ciclos de reloj.