2010-12-07 30 views
8

Me doy cuenta de que la respuesta probablemente sea específica del hardware, pero tengo curiosidad por saber si me faltaba una intuición más general.En C++, ¿qué es más rápido? (2 * i + 1) o (i << 1 | 1)?

me preguntó this pregunta & dado la respuesta, ahora me pregunto si debería cambiar mi enfoque en general a utilizar "(i < < 1 | 1)" en lugar de "(2 * i + 1)"? ?

+4

No lo sé con certeza, pero probablemente funcione según las mismas instrucciones de la máquina ... así que diría que escoja la que sea más legible. –

+2

@Jon Seigel: Y "legible" significa lo que expresa más claramente la intención del código. ¿Está (el OP) multiplicándose por dos y agregando uno, o está cambiando a la izquierda y configurando el LSB? – jason

+2

Está intentando hacer un trabajo que el compilador haría. Así que será mejor que no. ^^ – pinichi

Respuesta

8

Solo un experimento con respuestas dadas sobre "...que va a utilizar LEA ":
El siguiente código:

int main(int argc, char **argv) 
{ 
#ifdef USE_SHIFTOR 
return (argc << 1 | 1); 
#else 
return (2 * argc + 1); 
#endif 
} 

voluntad, con gcc -fomit-frame-pointer -O8 -m{32|64} (para 32 o 64 bits) compilar en el siguiente código de montaje:

  1. x86, de 32 bits:
    080483a0 <main>: 
    80483a0: 8b 44 24 04    mov 0x4(%esp),%eax 
    80483a4: 8d 44 00 01    lea 0x1(%eax,%eax,1),%eax 
    80483a8: c3      ret
  2. x86, de 64 bits:
    00000000004004c0 <main>: 
    4004c0: 8d 44 3f 01    lea 0x1(%rdi,%rdi,1),%eax 
    4004c4: c3      retq
  3. x86 de 64 bits en -DUSE_SHIFTOR:
    080483a0 <main>: 
    80483a0: 8b 44 24 04    mov 0x4(%esp),%eax 
    80483a4: 01 c0     add %eax,%eax 
    80483a6: 83 c8 01    or  $0x1,%eax 
    80483a9: c3      ret
  4. x86, de 32 bits, -DUSE_SHIFTOR:
    00000000004004c0 <main>: 
    4004c0: 8d 04 3f    lea (%rdi,%rdi,1),%eax 
    4004c3: 83 c8 01    or  $0x1,%eax 
    4004c6: c3      retq

De hecho, es cierto que la mayoría de los casos usarán LEA. Sin embargo, el código es no lo mismo para los dos casos. Hay dos razones para ello:

  1. Además puede desbordarse y se envuelven alrededor, mientras que las operaciones de bits como << o | no puede
  2. (x + 1) == (x | 1) sólo es cierto si !(x & 1) demás la adición se traslada a la siguiente bit. En general, agregar uno solo hace que el bit más bajo se establezca en la mitad de los casos.

Si bien nosotros (y el compilador, probablemente) sepamos que el segundo es necesariamente aplicable, el primero sigue siendo una posibilidad. Por lo tanto, el compilador crea código diferente, ya que la "o la versión" requiere forzar bit cero a 1.

+0

¿Qué compilador usaste? –

+0

gcc (Ubuntu/Linaro 4.4.4-14ubuntu5) 4.4.5 –

+1

Es bueno ver que alguien ponga realmente especulaciones y suposiciones salvajes a prueba. Pero tu explicación de por qué gcc no optimiza la versión de turno es incorrecta: tu punto 1 no es válido, un x << 1 se ajusta exactamente de la misma manera que x + x para cada x. También un compilador suficientemente reciente optimizará la versión de cambio a la misma instrucción lea. – hirschhornsalz

5

Cualquier pero el compilador más con muerte cerebral verán esas expresiones como equivalentes y compilarlos con el mismo código ejecutable.

lo general no es realmente vale la pena preocuparse demasiado acerca de la optimización de expresiones aritméticas simples como estos, ya que es el tipo de cosas que los compiladores son los mejores en la optimización. (A diferencia de muchos otros casos en los que un "compilador inteligente" podría hacer lo correcto, pero un compilador real se cae).

Esto funcionará con el mismo par de instrucciones en PPC, Sparc y MIPS, por el camino: un cambio seguido de un complemento. En el ARM, se reducirá a una sola instrucción fusionada de desplazamiento-cambio, y en x86 probablemente sea un solo LEA op.

+0

¿No se compilará esto en un solo LEA en x86? –

+0

@Axel Gneiting: ¡Ah, tienes razón! Repararé la respuesta. – Crashworks

+2

Sí, probablemente es el 'LEA EAX, EAX + EAX + 1' el camino de ayuno en x86. –

13

Dado que la norma ISO realidad no exigir requisitos de desempeño, esto dependerá de la aplicación, las opciones del compilador elegido, la CPU de destino y, posiblemente, la fase de la luna.

Este tipo de optimizaciones (ahorro de un par de ciclos) casi siempre se vuelven insignificantes en términos de retorno de la inversión, en contra de optimizaciones a nivel macro como la selección de algoritmo.

Objetivo para facilitar la lectura del código en primer lugar. Si su intención es cambiar los bits y OR, use la versión de desplazamiento de bit. Si tu intención es multiplicar, usa la versión *. Solo preocúpate por el rendimiento una vez que hayas establecido que hay un problema.

Cualquier compilador decente optimizará mucho mejor que se puede de todos modos :-)

+1

Esperemos que el compilador no dependa de la fase lunar, aunque ahora que lo pienso, he trabajado con algunas que parecen depender de las características de las mareas. –

+0

como cuando se inundan con la marea alta? Podría recomendar mover el servidor a una mayor altitud ...;) – jalf

+0

Me han decepcionado bastante los compiladores que no han utilizado los cambios/adiciones de bits para optimizar la multiplicación. –

4

salida de gcc con la opción -S (no hay opciones del compilador dado):

.LCFI3: 
     movl 8(%ebp), %eax 
     addl %eax, %eax 
     orl  $1, %eax 
     popl %ebp 
     ret 

.LCFI1: 
     movl 8(%ebp), %eax 
     addl %eax, %eax 
     addl $1, %eax 
     popl %ebp 
     ret 

No estoy seguro cuál es cuál, pero no creo que importe.

Si el compilador no hay optimizaciones en absoluto, entonces el segundo probablemente a traducir las instrucciones de montaje más rápidos. El tiempo que toma cada instrucción depende completamente de la arquitectura. La mayoría de los compiladores los optimizarán para que sean las mismas instrucciones de nivel de ensamblaje.

+0

En realidad, no se puede decir que, en general, el segundo será el más rápido, ya que es posible tener una arquitectura donde las adiciones son diez veces mayores que los cambios (poco probable, pero mi punto es que depende de la plataforma). Si te estás limitando a una plataforma específica, ese puede ser el caso, pero probablemente debas dejar eso en claro en la respuesta. – paxdiablo

+1

Y recuerde el proverbio: Benchmarking sin -O3 es como comparar los controladores de F1 en qué tan rápido pueden ir en monopatines. – Kos

0

A nadie le importa. Tampoco deberían ellos.
Deje de preocuparse por eso y obtenga su código correcto, simple y listo.

+1

¿Podemos ser menos negativos, o al menos respaldar su afirmación diciendo "el compilador tratará los dos formularios de manera equivalente"? –

+0

ok, ok, lo siento. ¿Qué tal "probablemente deberías escribir assembler artesanal si te preocupa la velocidad en este detalle"? ¿no? En general, cuando escribo cpp me esfuerzo por la corrección, la simplicidad y HECHO. Si la optimización no se sigue de la simplicidad, entonces usted está suplicando al siguiente vago pobre que elija este código para perseguirlo y dispararle ... –

0

i + i + 1 puede ser más rápido que otros dos, porque la adición es más rápida que la multiplicación y puede ser más rápida que el cambio.

+0

Esta respuesta no es útil porque es una suposición infundada, sin siquiera una pista de perfilar o desensamblar para respaldarlo. Alienta a las personas a "micro-optimizar", lo cual, como han dicho otras respuestas, es incorrecto. –

-2

Cuanto más rápido es la primera forma (la que tiene el desplazamiento a la derecha), de hecho, la instrucción shr toma 4 ciclos de reloj para completar en el peor de los casos, mientras que mul 10 en el mejor de los casos. Sin embargo, la mejor forma la debe decidir el compilador, ya que tiene una vista completa de las demás instrucciones (ensamblaje).

1

Acabo de probar esto con gcc-4.7.1 usando la fuente de Frankh, el código generado es

lea 0x1(%rdi,%rdi,1),%eax 
retq 

sin importar si se usa la versión de cambio o multiplicación.

Cuestiones relacionadas