2010-06-07 13 views
10
int aNumber; 

aNumber = aValue/2; 
aNumber = aValue >> 1; 

aNumber = aValue * 2; 
aNumber = aValue << 1; 

aNumber = aValue/4; 
aNumber = aValue >> 2; 

aNumber = aValue * 8; 
aNumber = aValue << 3; 

// etc. 

¿Cuál es la "mejor" forma de hacer operaciones? ¿Cuándo es mejor usar el cambio de bit?¿Cuál es la diferencia entre el cambio de bit y las operaciones aritméticas?

+1

Pregunta adicional: ¿se comportan igual en caso de desbordamiento aritmético? –

+2

No creo que el cambio de bits funcione muy bien con números negativos (o podría no funcionar de la manera en que se pretendía). – Default

+0

a menudo (¿siempre?) Los procesadores tienen desplazamiento aritmético, pero no sé si los compiladores de C se compilan de forma diferente si los operandos están firmados o no. – ShinTakezou

Respuesta

17

Los dos son funcionalmente equivalentes en los ejemplos que usted dio (excepto el último, que debería leer aValue * 8 == aValue << 3), si está usando enteros positivos. Esto es solo el caso al multiplicar o dividir por potencias de 2.

El cambio de bit nunca es más lento que la aritmética. Dependiendo de su compilador, la versión aritmética se puede compilar hasta la versión de cambio de bit, en cuyo caso ambos serán tan eficientes. De lo contrario, el cambio de bit debería ser significativamente más rápido que la aritmética.

La versión aritmética es a menudo más legible, sin embargo. En consecuencia, yo uso la versión aritmética en casi todos los casos, y sólo uso desplazamiento de bits si perfiles revela que la declaración está en un cuello de botella:

programas deben ser escritos para que la gente lea, y sólo incidentalmente para máquinas para ejecutar .

+1

de acuerdo. Comience con la aritmética, hasta que su código funcione y esté libre de errores. luego desentierre y use el cambio de bit como una de las herramientas en su bolsa de herramientas para optimizar el rendimiento. – pxl

+0

También es posible que desee ver la salida de ensamblaje optimizada. Creo que algunos compiladores traducirán la aritmética simple en cambios al optimizar. –

+0

para cosas simples, un compilador puede encontrarlo y optimizarlo. pero lo que no hará es alterar tu algoritmo con el cambio de bits en mente. y para eso, debes poner tu sombrero binario. – pxl

0

Si tiene grandes cálculos en un entorno de circuito cerrado donde la velocidad de cálculo tiene un impacto --- use operaciones de bits. (se considera más rápido que las operaciones aritméticas)

+0

Esto es una optimización prematura (que es la raíz de todo mal). Usa la expresión natural de lo que estás haciendo. Si divide lógicamente por 8 (por ejemplo), use la operación aritmética. Si lógicamente está tratando de cambiar los cambios de bit de uso de byte bajo. La razón es que los compiladores modernos convertirán automáticamente multiplicar y dividir por potencias de dos en operaciones de cambio para usted. – JeremyP

+1

Las operaciones de bits se pueden considerar más rápidas que las operaciones de máquina en algunas plataformas de hardware. No se consideran "* más rápidos" ni como C ni como operaciones de nivel de lenguaje C++. Los que hacen este tipo de "considerar" simplemente confunden las operaciones de la máquina con las operaciones del lenguaje. – AnT

0

Cuando se trata de números de potencia 2 (2^x), es mejor usar turnos, es solo 'empujar' los bits. (1 operación de ensamblaje en lugar de 2 en división).

¿Hay algún lenguaje que su compilador realice esta optimización?

+0

¿En qué plataforma está int dividir dos instrucciones de ensamblaje? –

+0

Ver mi comentario a la respuesta de AJ. – JeremyP

+0

gcc x86 incluso en -O0. –

4

El cambio de bit es una operación "cercana al metal" que la mayoría de las veces no contiene información sobre lo que realmente desea lograr.

Si desea dividir un número entre dos, escriba x/2. Ocurre que se logra por x >> 1, pero este último oculta el intento.

Cuando eso se convierta en un cuello de botella, revise el código.

8

La diferencia es que las operaciones aritméticas tienen resultados claramente definidos (a menos que se encuentren con desbordamiento firmado). Las operaciones de cambio no tienen resultados definidos en muchos casos. Están claramente definidos para los tipos sin firmar tanto en C como en C++, pero con tipos firmados, las cosas se vuelven complicadas rápidamente.

En lenguaje C++, el significado aritmético del desplazamiento a la izquierda << para tipos con signo no está definido. Simplemente cambia bits, llenando con ceros a la derecha. Lo que significa en sentido aritmético depende de la representación firmada utilizada por la plataforma. Prácticamente lo mismo es cierto para el operador de desplazamiento a la derecha >>. Los valores negativos que cambian a la derecha llevan a resultados definidos por la implementación.

En el lenguaje C, las cosas se definen de forma ligeramente diferente. Los valores negativos que cambian a la izquierda son imposibles: conduce a un comportamiento indefinido. Los valores negativos que cambian a la derecha llevan a resultados definidos por la implementación.

En la mayoría de las implementaciones prácticas, cada desplazamiento a la derecha realiza la división por 2 con redondeo hacia el infinito negativo.Esto, por cierto, es notablemente diferente de la división aritmética / por 2, ya que típicamente (y siempre en C99) del tiempo se redondeará hacia 0.

En cuanto a cuándo debe usar el cambio de bit ... Bit- el desplazamiento es para operaciones que funcionan en bits. Los operadores de desplazamiento de bits rara vez se utilizan como reemplazo de los operadores aritméticos (por ejemplo, nunca se deben usar los cambios para realizar la multiplicación/división por constante).

+0

Especialmente porque es muy probable que el compilador optimice su operación aritmética con operadores de cambios de bit si utiliza constantes. –

+0

@Matthieu M .: Es aún más probable que el compilador lo optimice con operaciones que no son aritméticas ni cambios de forma inmediata. – AnT

1

Mientras se multiplique o se divida dentro de las 2ª potencias, es más rápido operar con un cambio porque es una operación única (solo necesita un ciclo de proceso).
Uno se acostumbra a leer < < 1 como * 2 y >> 2 como/4 con bastante rapidez, así que no estoy de acuerdo con que la legibilidad desaparezca al usar el cambio pero esto depende de cada persona.

Si desea conocer más detalles acerca de cómo y por qué, tal vez Wikipedia puede ayudar o si quiere pasar por el dolor aprender ensamblador ;-)

2

¿Cuál es la "mejor" manera de hacer las operaciones?

Usa operaciones aritméticas cuando se trata de números. Use operaciones de bits al tratar con bits. Período. Esto es sentido común. Dudo que alguien alguna vez piense en usar operaciones de cambio de bit para los ints o los dobles, ya que una cosa normal del día a día es una buena idea.

¿Cuándo es mejor usar el cambio de bit?

Cuando se trata de bits?

Pregunta adicional: ¿se comportan igual en caso de desbordamiento aritmético?

Sí. Las operaciones aritméticas apropiadas son (a menudo, pero no siempre) simplificadas para sus homólogos de cambio de bit por la mayoría de los compiladores modernos.

Editar: La respuesta fue aceptada, pero solo quiero agregar que hay un montón de malos consejos en esta pregunta. Nunca debería (leer: casi nunca) usar operaciones de cambio de bit al tratar con ints. Es una práctica horrible.

2

Cuando su objetivo es multiplicar algunos números, usar operadores aritméticos tiene sentido.

Cuando sus objetivos son desplazar los bits de forma lógica, utilice los operadores de desplazamiento.

Por ejemplo, digamos que usted está dividiendo los componentes RGB de una palabra RGB, este código tiene sentido:

int r,g,b; 
short rgb = 0x74f5; 
b = rgb & 0x001f; 
g = (rgb & 0x07e0) >> 5; 
r = (rgb & 0xf800) >> 11; 

por el contrario cuando se desea multiplicar algún valor con 4 que realmente debe codificar su intención y no hacer turnos.

1

Como un ejemplo de las diferencias, este es el montaje x86 creado usando gcc 4.4 con -O3

int arithmetic0 (int aValue) 
{ 
    return aValue/2; 
} 

00000000 <arithmetic0>: 
    0: 55      push %ebp 
    1: 89 e5     mov %esp,%ebp 
    3: 8b 45 08    mov 0x8(%ebp),%eax 
    6: 5d      pop %ebp 
    7: 89 c2     mov %eax,%edx 
    9: c1 ea 1f    shr $0x1f,%edx 
    c: 8d 04 02    lea (%edx,%eax,1),%eax 
    f: d1 f8     sar %eax 
    11: c3      ret  

int arithmetic1 (int aValue) 
{ 
    return aValue >> 1; 
} 

00000020 <arithmetic1>: 
    20: 55      push %ebp 
    21: 89 e5     mov %esp,%ebp 
    23: 8b 45 08    mov 0x8(%ebp),%eax 
    26: 5d      pop %ebp 
    27: d1 f8     sar %eax 
    29: c3      ret  

int arithmetic2 (int aValue) 
{ 
    return aValue * 2; 
} 

00000030 <arithmetic2>: 
    30: 55      push %ebp 
    31: 89 e5     mov %esp,%ebp 
    33: 8b 45 08    mov 0x8(%ebp),%eax 
    36: 5d      pop %ebp 
    37: 01 c0     add %eax,%eax 
    39: c3      ret  

int arithmetic3 (int aValue) 
{ 
    return aValue << 1; 
} 

00000040 <arithmetic3>: 
    40: 55      push %ebp 
    41: 89 e5     mov %esp,%ebp 
    43: 8b 45 08    mov 0x8(%ebp),%eax 
    46: 5d      pop %ebp 
    47: 01 c0     add %eax,%eax 
    49: c3      ret  

int arithmetic4 (int aValue) 
{ 
    return aValue/4; 
} 

00000050 <arithmetic4>: 
    50: 55      push %ebp 
    51: 89 e5     mov %esp,%ebp 
    53: 8b 55 08    mov 0x8(%ebp),%edx 
    56: 5d      pop %ebp 
    57: 89 d0     mov %edx,%eax 
    59: c1 f8 1f    sar $0x1f,%eax 
    5c: c1 e8 1e    shr $0x1e,%eax 
    5f: 01 d0     add %edx,%eax 
    61: c1 f8 02    sar $0x2,%eax 
    64: c3      ret  

int arithmetic5 (int aValue) 
{ 
    return aValue >> 2; 
} 

00000070 <arithmetic5>: 
    70: 55      push %ebp 
    71: 89 e5     mov %esp,%ebp 
    73: 8b 45 08    mov 0x8(%ebp),%eax 
    76: 5d      pop %ebp 
    77: c1 f8 02    sar $0x2,%eax 
    7a: c3      ret  

int arithmetic6 (int aValue) 
{ 
    return aValue * 8; 
} 

00000080 <arithmetic6>: 
    80: 55      push %ebp 
    81: 89 e5     mov %esp,%ebp 
    83: 8b 45 08    mov 0x8(%ebp),%eax 
    86: 5d      pop %ebp 
    87: c1 e0 03    shl $0x3,%eax 
    8a: c3      ret  

int arithmetic7 (int aValue) 
{ 
    return aValue << 4; 
} 

00000090 <arithmetic7>: 
    90: 55      push %ebp 
    91: 89 e5     mov %esp,%ebp 
    93: 8b 45 08    mov 0x8(%ebp),%eax 
    96: 5d      pop %ebp 
    97: c1 e0 04    shl $0x4,%eax 
    9a: c3      ret  

Las divisiones son diferentes - con la representación de un complemento a dos, cambiando un número impar negativas adecuadas uno da como resultado una diferente valor a dividirlo por dos. Pero el compilador aún optimiza la división a una secuencia de cambios y adiciones.

La diferencia más obvia, sin embargo, es que este par no hace lo mismo: ¡cambiar por cuatro equivale a multiplicar por dieciséis, no por ocho! Probablemente no obtendrás un error de esto si dejas que el compilador suceda las pequeñas optimizaciones para ti.

aNumber = aValue * 8; 
aNumber = aValue << 4; 
0
int i = -11; 
std::cout << (i/2) << '\n'; // prints -5 (well defined by the standard) 
std::cout << (i >> 1) << '\n'; // prints -6 (may differ on other platform) 

Dependiendo del comportamiento de redondeo se desea, puede preferir uno sobre el otro.

Cuestiones relacionadas