2009-09-22 10 views
22

Consideremos un ejemplo como este:¿Pueden los compiladores de C++ optimizar las instrucciones "if" dentro de los bucles "for"?

if (flag) 
    for (condition) 
    do_something(); 
else 
    for (condition) 
    do_something_else(); 

Si flag no cambia dentro de los bucles for, esto debe ser semánticamente equivalente a:

for (condition) 
    if (flag) 
    do_something(); 
    else 
    do_something_else(); 

Sólo en el primer caso, el código podría ser mucho más tiempo (por ejemplo, si se usan varios for bucles o si do_something() es un bloque de código que es en su mayoría idéntico a do_something_else()), mientras que en el segundo caso, la bandera se verifica muchas veces.

Tengo curiosidad por saber si los compiladores de C++ actuales (más importante, g ++) podrían optimizar el segundo ejemplo para deshacerse de las repetidas pruebas dentro del ciclo for. Si es así, ¿en qué condiciones es esto posible?

Respuesta

18

Sí, si se determina que el indicador no cambia y do_something o do_something_else no lo pueden cambiar, se puede sacar del bucle. He oído hablar de esto llamado elevación de bucle, pero Wikipedia tiene un entry llamado "movimiento de código invariante de bucle".

Si flags es una variable local, el compilador debería poder hacer esta optimización ya que está garantizado que no tendrá ningún efecto en el comportamiento del código generado.

Si flags es una variable global, y usted llama a funciones dentro de su bucle, es posible que no realice la optimización, es posible que no pueda determinar si esas funciones modifican el global.

Esto también puede verse afectado por el tipo de optimización que realiza: optimizar el tamaño favorecería a la versión sin cable, mientras que la optimización de la velocidad probablemente favorecería la versión con cable.

En general, este no es el tipo de cosas que debe preocuparse, a menos que los perfiles le digan que la función es un punto de acceso y ve que el código que no es eficiente se está generando revisando el compilador salidas. Las micro optimizaciones como esta siempre deberían dejarse al compilador a menos que sea absolutamente necesario.

+5

Gracias por la respuesta. Desde su enlace de wikipedia, encontré la página de "loopwitch", que parece ser un término aún más preciso para este tipo de optimización. –

+0

Aunque los términos no son realmente cortados y secos, el levantamiento de bucles y el movimiento de código invariante de bucle no se usan realmente para describir esta optimización. Son más para instrucciones individuales. –

+0

Enlace a 'loop unswitching' al que hace referencia OP: https://en.wikipedia.org/wiki/Loop_unswitching – BrodieG

1

Estoy seguro que si el compilador puede determinar que la bandera se mantendrá constante, se puede hacer un poco de shufflling:

const bool flag = /* ... */; 
for (..;..;..;) 
{ 
    if (flag) 
    { 
     // ... 
    } 
    else 
    { 
     // ... 
    } 
} 

Si el flag no es const, el compilador puede no necesariamente optimizar el bucle, porque No puedo estar seguro de que flag no cambie. Puede si se hace un análisis estático, pero no todos los compiladores lo hacen, creo. const es la forma segura de decirle al compilador que la bandera no cambiará, después de eso depende del compilador.

Como de costumbre, haga un perfil y descubra si realmente es un problema.

+2

'const' es una condición comprobada por el compilador, pero no tiene ningún efecto en la optimización. – peterchen

+0

El alcance de la variable es probablemente más importante, pero la constness puede influir en la optimización. Es un comportamiento indefinido cambiar un objeto que es 'const' (esto es diferente de usar' const_cast' para cambiar un objeto que no es un 'const' obejct, pero la referencia a través de la cual se conoce el objeto es' const 'referencia) para que un compilador * pueda * usar esta información para almacenar su valor en caché. –

+2

peter, 'const' tiene todo que ver con la optimización. – GManNickG

0

Se llama lazo invariante y la optimización de bucle se llama código invariante movimiento y también código de elevación. El hecho de que esté en un condicionamiento definitivamente hará que el análisis del código sea más complejo y el compilador puede o no invertir el ciclo y el condicional dependiendo de qué tan inteligente sea el optimizador.

Hay una respuesta general para cualquier caso específico de este tipo de pregunta, y eso es compilar su programa y mirar el código generado.

0

Tendría cuidado de decir que lo hará. ¿Puede garantizarse que el valor no será modificado por esto u otro hilo?

Dicho esto, la segunda versión del código es generalmente más legible y probablemente sería lo último para optimizar en un bloque de código.

+0

@patros - Si se trata de una variable local, no es necesario que entren en juego los multihilos. Incluso si la variable es visible para otros hilos, el compilador aún puede realizar la optimización, a menos que marque como volátil el compilador no es necesario volver a buscar en cada acceso y el codegen es válido. – Michael

+0

@Michael: de acuerdo, pero no vemos la definición de marca en este fragmento, por lo que puede no ser local. También estoy de acuerdo en que el compilador puede legalmente o ptimize esto, solo que no siempre lo hará. Si lo hace, es probablemente una función del alcance de la variable y los indicadores del compilador. – patros

1

En general, sí. Pero no hay garantía, y los lugares donde el compilador lo hará son probablemente raros.

Lo que hacen la mayoría de los compiladores sin problemas es levantar las evaluaciones inmutables fuera de circuito, p. si su condición es

if (a<b) .... 

cuando a y b no se ven afectados por el bucle, la comparación se realizará una vez antes del bucle.

Esto significa que si el compilador puede determinar que la condición no cambia, la prueba es barata y el salto se predijo. Esto a su vez significa que la prueba en sí cuesta un ciclo o ningún ciclo en absoluto (realmente).

¿En qué casos dividir el bucle sería beneficioso?

a) un lazo muy estrecho donde el 1 ciclo es un costo significativo
b) todo el circuito con las dos partes no se ajusta al código de caché

Ahora, el compilador sólo puede hacer suposiciones sobre el código caché y, por lo general, puede solicitar el código de forma que una rama se ajuste al caché.

Sin ninguna prueba, I'dexpect a) el único caso en el que se aplicaría tal optimización, robaba es nto siempre la mejor opción:

¿En qué casos dividir el bucle serían malos?

Al dividir el bucle aumenta el tamaño del código más allá de la memoria caché de código, recibirá un golpe significativo. Ahora, eso solo te afecta si el bucle se llama dentro de otro bucle, pero eso es algo que el compilador generalmente no puede determinar.

[editar]
no pude conseguir VC9 para dividir el siguiente bucle (uno de los pocos casos en los que en realidad podría ser beneficioso)

extern volatile int vflag = 0; 

int foo(int count) 
{ 
    int sum = 0; 
    int flag = vflag; 
    for(int i=0; i<count; ++i) 
    { 
     if (flag) 
     sum += i; 
     else 
     sum -= i; 
    } 

    return sum; 
} 

[editar 2]
tenga en cuenta que con int flag = true; la segunda rama se optimiza. (y no, const no hace una diferencia aquí;))

¿Qué significa eso? O no es compatible con eso, no importa, mi análisis es incorrecto ;-)

En general, supongo que es una optimización que es valiosa solo en unos pocos casos, y se puede hacer a mano fácilmente en la mayoría de los escenarios.

+0

Peter, ¿qué indicadores usaste para compilar tu fragmento de código? – Michael

+0

sin información de depuración, siempre en línea (/ Ob2),/Ox con cualquiera/Os,/Ot o ninguno de los dos últimos. (He visto solo la salida de los compiladores, pero/GL no debería afectar esto) – peterchen

1

Como muchos han dicho: depende.

Si quiere estar seguro, debe intentar forzar una decisión en tiempo de compilación. Plantillas menudo son útiles para esto:

for (condition) 
    do_it<flag>(); 
17

intentado con GCC y -O3:

void foo(); 
void bar(); 

int main() 
{ 
    bool doesnt_change = true; 
    for (int i = 0; i != 3; ++i) { 
     if (doesnt_change) { 
      foo(); 
     } 
     else { 
      bar(); 
     } 
    } 
} 

Resultado de principal:

_main: 
pushl %ebp 
movl %esp, %ebp 
andl $-16, %esp 
call ___main 
call __Z3foov 
call __Z3foov 
call __Z3foov 
xorl %eax, %eax 
leave 
ret 

por lo que optimizar la elección de distancia (y desenrolla bucles más pequeños).

Esta optimización no se realiza si doesnt_change es global.

+2

+1: Para realizar la prueba y mostrar el código generado. – Clifford

+3

¿podría probar el fragmento que usé a continuación? En su fragmento, el compilador simplemente está eliminando la segunda rama porque nunca se ejecutará. (El truco es inicializar 'doesnt_change' desde un volátil externo, por lo que el compilador no puede determinar qué valor tendrá) – peterchen

Cuestiones relacionadas