¿Es realmente más rápido usar say (i << 3) + (i << 1) para multiplicar por 10 que usar i * 10 directamente?
Puede o no estar en su máquina; si le importa, mida su uso en el mundo real.
Un estudio de caso: del 486 al Core i7
La evaluación comparativa es muy difícil de hacer de manera significativa, pero podemos ver algunos hechos. De http://www.penguin.cz/~literakl/intel/s.html#SAL y http://www.penguin.cz/~literakl/intel/i.html#IMUL tenemos una idea de los ciclos de reloj x86 necesario para el cambio aritmético y la multiplicación. Digamos que nos atenemos a "486" (el más nuevo en la lista), registros de 32 bits e inmediatos, IMUL toma 13-42 ciclos e IDIV 44. Cada SAL toma 2 y agrega 1, por lo que incluso con algunos de ellos juntos cambian superficialmente como un ganador
En estos días, con el Core i7:
(de http://software.intel.com/en-us/forums/showthread.php?t=61481 )
La latencia es 1 ciclo para una suma entera y 3 ciclos para una multiplicación entera . Puede encontrar las latencias y el rendimiento en el Apéndice C del "Manual de referencia de optimización de arquitecturas Intel® 64 e IA-32", que se encuentra en http://www.intel.com/products/processor/manuals/ .
(de alguna propaganda de Intel)
Usando SSE, el Core i7 puede emitir instrucciones simultáneas de sumar y multiplicar, lo que resulta en una tasa máxima de 8 operaciones de punto flotante (FLOP) por ciclo de reloj
Eso te da una idea de cuán lejos han llegado las cosas. La trivia de optimización, como el cambio de bit versus *
, que se tomó en serio incluso en los años 90, ahora es obsoleta. El cambio de bits es aún más rápido, pero para mul / div sin potencia de dos para el momento en que realiza todos sus cambios y agrega los resultados, es más lento nuevamente. Luego, más instrucciones significan más fallas de caché, más problemas potenciales en la canalización, más uso de registros temporales puede significar más ahorro y restauración del contenido del registro de la pila ... rápidamente se vuelve demasiado complicado cuantificar todos los impactos definitivamente, pero son predominantemente negativo
funcionalidad en código fuente vs implementación
En términos más generales, su pregunta está etiquetada con C y C ++. Como lenguajes de tercera generación, están diseñados específicamente para ocultar los detalles del conjunto de instrucciones de CPU subyacente. Para satisfacer sus estándares de idioma, deben admitir operaciones de multiplicación y desplazamiento (y muchas otras) incluso si el hardware subyacente no lo hace . En tales casos, deben sintetizar el resultado requerido utilizando muchas otras instrucciones. Del mismo modo, deben proporcionar soporte de software para operaciones de coma flotante si la CPU carece de ella y no hay FPU. Las CPU modernas son compatibles *
y<<
, por lo que esto puede parecer absurdamente teórico e histórico, pero lo importante es que la libertad de elegir la implementación va en ambos sentidos: incluso si la CPU tiene una instrucción que implementa la operación solicitada en el código fuente en el caso general, el compilador es libre de elige otra cosa que prefiera porque es mejor para el caso específico al que se enfrenta el compilador.
Ejemplos (con un lenguaje ensamblador hipotético)
source literal approach optimised approach
#define N 0
int x; .word x xor registerA, registerA
x *= N; move x -> registerA
move x -> registerB
A = B * immediate(0)
store registerA -> x
...............do something more with x...............
Las instrucciones como exclusive o ( xor
) no tienen relación con el código fuente, pero al hacer cualquier cosa en sí mismo se borran todos los bits, por lo que se puede usar para establecer algo en 0. El código fuente que implica que las direcciones de memoria no implican que se use.
Este tipo de hacks se han utilizado durante tanto tiempo como las computadoras han existido. En los primeros días de los 3GLs, para asegurar la aceptación del desarrollador, la salida del compilador tenía que satisfacer al desarrollador de lenguaje ensamblador hardcore optimizado a mano. comunidad que el código producido no era más lento, más detallado o peor. Los compiladores adoptaron rápidamente muchas optimizaciones excelentes: se convirtieron en una tienda mejor centralizada de lo que podría ser cualquier programador de lenguaje ensamblador individual, aunque siempre existe la posibilidad de que pierdan una optimización específica que resulta crucial en un caso específico; los humanos a veces pueden enloquece y busca algo mejor mientras los compiladores simplemente hacen lo que se les ha dicho hasta que alguien les transmita esa experiencia.
Entonces, incluso si cambiar y agregar aún es más rápido en algún hardware en particular, es probable que el escritor del compilador haya funcionado exactamente cuando es seguro y beneficioso.
Mantenibilidad
Si su hardware cambia, puede volver a compilar y mirará la CPU de destino y tomará otra mejor decisión, mientras que es poco probable que desee volver a visitar sus "optimizaciones" o enumerar qué entornos de compilación deberían usar la multiplicación y cuáles deberían cambiar. ¡Piense en todas las "optimizaciones" desplazadas en bits sin potencia de dos escrito hace más de 10 años que ahora están ralentizando el código en el que se encuentran mientras se ejecuta en procesadores modernos ...!
Afortunadamente, los buenos compiladores como GCC generalmente pueden reemplazar una serie de cambios de bits y aritmética con una multiplicación directa cuando se habilita cualquier optimización (es decir, ...main(...) { return (argc << 4) + (argc << 2) + argc; }
-> imull $21, 8(%ebp), %eax
), por lo que una recompilación puede ayudar incluso sin corregir el código, pero eso no está garantizado.
El extraño código de cambio de bits que implementa la multiplicación o división es mucho menos expresivo de lo que intentaba lograr conceptualmente, por lo que otros desarrolladores se sentirán confundidos por eso, y es más probable que un programador confuso introduzca errores o elimine algo esencial en un esfuerzo por restaurar la aparente cordura. Si solo haces cosas no obvias cuando son realmente beneficiosas y luego las documentas bien (pero no documentas otras cosas que son intuitivas de todos modos), todos serán más felices.
Soluciones generales versus soluciones parciales
Si usted tiene algún conocimiento adicional, como que su int
voluntad en realidad sólo puede almacenar valores x
, y
y z
, a continuación, puede ser capaz de trabajar a cabo algunas instrucciones que el trabajo de esos valores y se obtiene el resultado más rápidamente que cuando el compilador de no tiene esa idea y necesita una implementación que funcione para todos los int
valores. Por ejemplo, considere su pregunta:
La multiplicación y la división se pueden lograr utilizando operadores de bits ...
Ilustras la multiplicación, pero ¿qué tal la división?
int x;
x >> 1; // divide by 2?
De acuerdo con el estándar C ++ 5.8:
-3- El valor de E1 >> E2 es E1 posiciones de bit E2 desplazadas a la derecha. Si E1 tiene un tipo sin signo o si E1 tiene un tipo con signo y un valor no negativo, el valor del resultado es la parte integral del cociente de E1 dividido por la cantidad 2 elevada a la potencia E2. Si E1 tiene un tipo con signo y un valor negativo, el valor resultante está definido por la implementación.
Por lo tanto, su cambio de bit tiene un resultado definido de implementación cuando x
es negativo: es posible que no funcione de la misma manera en diferentes máquinas. Pero, /
funciona mucho más predecible. (Puede que tampoco sea perfectamente consistente, ya que diferentes máquinas pueden tener diferentes representaciones de números negativos y, por lo tanto, diferentes rangos incluso cuando hay el mismo número de bits que componen la representación).
Puede decir "No me importa ... eso int
es almacenar la edad del empleado, nunca puede ser negativo". Si tiene ese tipo de información especial, entonces sí, >>
el compilador podría pasar por alto su optimización segura a menos que lo haga explícitamente en su código. Pero es arriesgado y rara vez útil la mayor parte del tiempo, no tendrá este tipo de información, y otros programadores que trabajan en el mismo código no sabrán que ha apostado la casa por algunas expectativas inusuales de los datos que usted ' estaré manejando ... lo que parece un cambio totalmente seguro para ellos podría ser contraproducente debido a su "optimización".
¿Hay algún tipo de entrada que no se pueda multiplicar o dividir de esta manera?
Sí ... como se mencionó anteriormente, los números negativos tienen un comportamiento definido de implementación cuando se "divide" por desplazamiento de bits.