La multiplicación y la división se pueden lograr utilizando operadores de bits, por ejemplo
i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)
y así.
¿Es realmente más rápido usar say (i<<3)+(i<<1)
para multiplicar por 10 que usar i*10
directamente? ¿Hay algún tipo de entrada que no se pueda multiplicar o dividir de esta manera?
Respuestas:
Respuesta corta: no es probable.
Respuesta larga: su compilador tiene un optimizador que sabe cómo multiplicarse tan rápido como su arquitectura de procesador de destino es capaz. Su mejor opción es decirle claramente al compilador su intención (es decir, i * 2 en lugar de i << 1) y dejar que decida cuál es la secuencia de código de ensamblaje / máquina más rápida. Incluso es posible que el procesador mismo haya implementado la instrucción de multiplicación como una secuencia de cambios y adiciones en microcódigo.
En pocas palabras: no pase mucho tiempo preocupándose por esto. Si quieres cambiar, cambia. Si quieres multiplicar, multiplica. Haga lo que sea semánticamente más claro: sus compañeros de trabajo se lo agradecerán más tarde. O, más probablemente, te maldiga más tarde si no lo haces.
fuente
gcc -O3
en x86 conreturn i*10
que en la versión de turno . Como alguien que mira mucho la salida del compilador (vea muchas de mis respuestas de asm / optimización), no me sorprende. Hay momentos en los que puede ayudar sostener el compilador a mano en una forma de hacer las cosas , pero esta no es una de ellas. gcc es bueno en matemáticas de enteros, porque es importante.millis() >> 2
; ¿Hubiera sido demasiado pedir simplemente dividir?i / 32
vsi >> 5
yi / 4
vsi >> 2
en gcc para cortex-a9 (que no tiene división de hardware) con optimización -O3 y el ensamblaje resultante fue exactamente el mismo. No me gustó usar divisiones primero, pero describe mi intención y el resultado es el mismo.Solo un punto de medida concreto: muchos años atrás, comparé dos versiones de mi algoritmo de hash:
y
En cada máquina en la que lo comparé, la primera fue al menos tan rápida como la segunda. Sorprendentemente, a veces era más rápido (por ejemplo, en un Sun Sparc). Cuando el hardware no admitía la multiplicación rápida (y la mayoría no lo hacía en ese entonces), el compilador convertía la multiplicación en las combinaciones apropiadas de turnos y suma / sub. Y debido a que conocía el objetivo final, a veces podría hacerlo en menos instrucciones que cuando escribiste explícitamente los turnos y los add / subs.
Tenga en cuenta que esto fue algo así como hace 15 años. Con suerte, los compiladores solo han mejorado desde entonces, por lo que puede contar con que el compilador haga lo correcto, probablemente mejor de lo que podría. (Además, la razón por la que el código parece tan C'ish es porque fue hace más de 15 años. Obviamente usaría
std::string
e iteradores hoy).fuente
Además de todas las otras buenas respuestas aquí, permítanme señalar otra razón para no usar shift cuando se refiere a dividir o multiplicar. Nunca he visto a alguien introducir un error al olvidar la precedencia relativa de la multiplicación y la suma. He visto errores introducidos cuando los programadores de mantenimiento olvidaron que "multiplicar" a través de un turno es lógicamente una multiplicación pero no sintácticamente de la misma precedencia que la multiplicación.
x * 2 + z
yx << 1 + z
son muy diferentes!Si está trabajando en números , utilice operadores aritméticos como
+ - * / %
. Si está trabajando en matrices de bits, use operadores de giro de bits como& ^ | >>
. No los mezcles; Una expresión que tiene tanto bit twiddling como aritmética es un error que espera suceder.fuente
Esto depende del procesador y el compilador. Algunos compiladores ya optimizan el código de esta manera, otros no. Por lo tanto, debe verificar cada vez que su código debe optimizarse de esta manera.
A menos que necesite optimizar desesperadamente, no codificaría mi código fuente solo para guardar una instrucción de ensamblaje o un ciclo de procesador.
fuente
>>
operador es más rápido/
y, si los valores con signo pueden ser negativos, a menudo también es semánticamente superior. Si uno necesita el valor quex>>4
produciría, eso es mucho más claro quex < 0 ? -((-1-x)/16)-1 : x/16;
eso, y no puedo imaginar cómo un compilador podría optimizar esa última expresión para algo agradable.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 )
(de alguna propaganda de Intel)
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 negativofuncionalidad 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)
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 valoresx
,y
yz
, 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 losint
valores. Por ejemplo, considere su pregunta:Ilustras la multiplicación, pero ¿qué tal la división?
De acuerdo con el estándar C ++ 5.8:
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".Sí ... como se mencionó anteriormente, los números negativos tienen un comportamiento definido de implementación cuando se "divide" por desplazamiento de bits.
fuente
intVal>>1
tendrá la misma semántica que difiere de las deintVal/2
una manera que a veces es útil. Si uno necesita calcular de manera portátil el valor que generarían las arquitecturas comunesintVal >> 1
, la expresión debería ser bastante más complicada y más difícil de leer, y probablemente generaría un código sustancialmente inferior al producido paraintVal >> 1
.Acabo de probar en mi máquina compilando esto:
Al desmontarlo produce salida:
Esta versión es más rápida que su código optimizado a mano con puro cambio y adición.
Realmente nunca se sabe con qué se va a encontrar el compilador, por lo que es mejor simplemente escribir una multiplicación normal y dejar que optimice de la manera que quiera, excepto en casos muy precisos en los que sabe que el compilador no puede optimizar.
fuente
vector<T>::size()
. ¡Mi compilador era bastante antiguo! :)El cambio es generalmente mucho más rápido que multiplicar en un nivel de instrucción, pero es posible que esté perdiendo el tiempo haciendo optimizaciones prematuras. El compilador puede realizar estas optimizaciones en tiempo de compilación. Hacerlo usted mismo afectará la legibilidad y posiblemente no tenga ningún efecto en el rendimiento. Probablemente solo valga la pena hacer cosas como esta si ha realizado un perfil y ha encontrado que se trata de un cuello de botella.
En realidad, el truco de la división, conocido como 'división mágica', en realidad puede generar grandes ganancias. Una vez más, debe hacer un perfil primero para ver si es necesario. Pero si lo usa, existen programas útiles para ayudarlo a descubrir qué instrucciones se necesitan para la misma semántica de división. Aquí hay un ejemplo: http://www.masm32.com/board/index.php?topic=12421.0
Un ejemplo que he sacado del hilo del OP en MASM32:
Generaría:
fuente
Las instrucciones de multiplicación de números enteros y de cambio tienen un rendimiento similar en la mayoría de las CPU modernas: las instrucciones de multiplicación de números enteros fueron relativamente lentas en la década de 1980, pero en general esto ya no es cierto. Las instrucciones de multiplicación de enteros pueden tener una latencia más alta , por lo que aún puede haber casos en los que sea preferible un cambio. Lo mismo ocurre con los casos en que puede mantener ocupadas más unidades de ejecución (aunque esto puede reducir en ambos sentidos).
Sin embargo, la división entera todavía es relativamente lenta, por lo que usar un cambio en lugar de la división por una potencia de 2 sigue siendo una victoria, y la mayoría de los compiladores implementarán esto como una optimización. Sin embargo, tenga en cuenta que para que esta optimización sea válida, el dividendo no debe estar firmado o debe ser positivo. ¡Para un dividendo negativo, el desplazamiento y la división no son equivalentes!
Salida:
Entonces, si desea ayudar al compilador, asegúrese de que la variable o expresión en el dividendo esté explícitamente sin signo.
fuente
Depende completamente del dispositivo de destino, el idioma, el propósito, etc.
¿Crujido de píxeles en un controlador de tarjeta de video? Muy probablemente sí!
¿Aplicación comercial .NET para su departamento? Absolutamente no hay razón para siquiera mirarlo.
Para un juego de alto rendimiento para un dispositivo móvil, puede valer la pena analizarlo, pero solo después de que se hayan realizado optimizaciones más fáciles.
fuente
No lo haga a menos que sea absolutamente necesario y la intención de su código requiera cambios en lugar de multiplicación / división.
En un día típico, podría ahorrar potentemente algunos ciclos de la máquina (o perder, ya que el compilador sabe mejor qué optimizar), pero el costo no vale la pena: pasa tiempo en detalles menores en lugar del trabajo real, mantener el código se vuelve más difícil y tus compañeros de trabajo te maldecirán.
Es posible que deba hacerlo para cálculos de alta carga, donde cada ciclo guardado significa minutos de tiempo de ejecución. Pero, debe optimizar un lugar a la vez y hacer pruebas de rendimiento cada vez para ver si realmente lo hizo más rápido o si rompió la lógica de los compiladores.
fuente
Hasta donde yo sé en algunas máquinas, la multiplicación puede necesitar hasta 16 a 32 ciclos de máquina. Entonces , sí , dependiendo del tipo de máquina, los operadores de desplazamiento de bits son más rápidos que la multiplicación / división.
Sin embargo, ciertas máquinas tienen su procesador matemático, que contiene instrucciones especiales para la multiplicación / división.
fuente
Estoy de acuerdo con la respuesta marcada por Drew Hall. Sin embargo, la respuesta podría usar algunas notas adicionales.
Para la gran mayoría de los desarrolladores de software, el procesador y el compilador ya no son relevantes para la pregunta. La mayoría de nosotros estamos mucho más allá del 8088 y MS-DOS. Tal vez solo sea relevante para aquellos que aún están desarrollando procesadores integrados ...
En mi empresa de software, Math (add / sub / mul / div) debería usarse para todas las matemáticas. Mientras que Shift debe usarse al convertir entre tipos de datos, por ejemplo. ushort al byte como n >> 8 y no n / 256.
fuente
En el caso de los enteros con signo y el desplazamiento a la derecha frente a la división, puede marcar la diferencia. Para números negativos, el desplazamiento redondea hacia el infinito negativo mientras que la división se redondea hacia cero. Por supuesto, el compilador cambiará la división a algo más barato, pero generalmente lo cambiará a algo que tenga el mismo comportamiento de redondeo que la división, porque no puede probar que la variable no será negativa o simplemente no cuidado. Entonces, si puede probar que un número no será negativo o si no le importa de qué manera se redondeará, puede hacer esa optimización de una manera que sea más probable que marque la diferencia.
fuente
unsigned
Prueba de Python que realiza la misma multiplicación 100 millones de veces contra los mismos números aleatorios.
Entonces, al hacer un cambio en lugar de multiplicación / división por una potencia de dos en python, hay una ligera mejora (~ 10% para la división; ~ 1% para la multiplicación). Si es un no poder de dos, es probable que haya una desaceleración considerable.
Nuevamente, estos # cambiarán dependiendo de su procesador, su compilador (o intérprete, lo hizo en python por simplicidad).
Como con todos los demás, no optimices prematuramente. Escriba un código muy legible, perfil si no es lo suficientemente rápido, y luego intente optimizar las partes lentas. Recuerde, su compilador es mucho mejor en optimización que usted.
fuente
Hay optimizaciones que el compilador no puede hacer porque solo funcionan para un conjunto reducido de entradas.
Debajo hay un código de muestra de c ++ que puede hacer una división más rápida haciendo una "Multiplicación por el recíproco" de 64 bits. Tanto el numerador como el denominador deben estar por debajo de cierto umbral. Tenga en cuenta que debe compilarse para usar instrucciones de 64 bits para que sea realmente más rápido que la división normal.
fuente
Creo que en el caso de que desee multiplicar o dividir por una potencia de dos, no puede equivocarse con el uso de operadores de desplazamiento de bits, incluso si el compilador los convierte a MUL / DIV, porque algunos procesadores microcódigo (realmente, un macro) de todos modos, por lo que para esos casos logrará una mejora, especialmente si el cambio es más de 1. O más explícitamente, si la CPU no tiene operadores de desplazamiento de bits, será un MUL / DIV de todos modos, pero si la CPU tiene operadores bithift, evitas una rama de microcódigo y estas son algunas instrucciones menos.
Estoy escribiendo un código en este momento que requiere muchas operaciones de duplicación / reducción a la mitad porque está trabajando en un árbol binario denso, y sospecho que hay una operación más que podría ser más óptima que una adición: una izquierda (la potencia de dos se multiplica ) cambio con una adición. Esto se puede reemplazar con un desplazamiento a la izquierda y un xor si el desplazamiento es más ancho que el número de bits que desea agregar, un ejemplo fácil es (i << 1) ^ 1, que agrega uno a un valor duplicado. Por supuesto, esto no se aplica a un desplazamiento a la derecha (potencia de dos divisiones) porque solo un desplazamiento a la izquierda (little endian) llena el vacío con ceros.
En mi código, estos se multiplican / dividen por dos y las potencias de dos operaciones se usan de manera muy intensiva y debido a que las fórmulas ya son bastante cortas, cada instrucción que se puede eliminar puede ser una ganancia sustancial. Si el procesador no admite estos operadores de desplazamiento de bits, no se producirá ninguna ganancia, pero tampoco habrá una pérdida.
Además, en los algoritmos que estoy escribiendo, representan visualmente los movimientos que ocurren para que, en ese sentido, sean más claros. El lado izquierdo de un árbol binario es más grande y el derecho es más pequeño. Además de eso, en mi código, los números pares e impares tienen un significado especial, y todos los hijos de la izquierda en el árbol son impares y todos los hijos de la derecha, y la raíz, son pares. En algunos casos, que aún no he encontrado, pero que, oh, en realidad, ni siquiera pensé en esto, x & 1 puede ser una operación más óptima en comparación con x% 2. x & 1 en un número par producirá cero, pero producirá 1 para un número impar.
Yendo un poco más allá de la identificación impar / par, si obtengo cero para x & 3, sé que 4 es un factor de nuestro número, y lo mismo para x% 7 para 8, y así sucesivamente. Sé que estos casos probablemente tengan una utilidad limitada, pero es bueno saber que puede evitar una operación de módulo y usar una operación lógica bit a bit en su lugar, porque las operaciones bit a bit son casi siempre las más rápidas y es menos probable que sean ambiguas para el compilador.
Estoy inventando el campo de los árboles binarios densos, por lo que espero que las personas no comprendan el valor de este comentario, ya que muy raramente quieren realizar factorizaciones solo con poderes de dos, o solo poderes de multiplicar / dividir de dos.
fuente
Si es realmente más rápido depende del hardware y el compilador realmente utilizado.
fuente
Si compara la salida para x + x, x * 2 yx << 1 sintaxis en un compilador gcc, obtendrá el mismo resultado en el ensamblado x86: https://godbolt.org/z/JLpp0j
Por lo tanto, puede considerar que gcc es lo suficientemente inteligente como para determinar su mejor solución independientemente de lo que escribió.
fuente
Yo también quería ver si podía vencer a la casa. Este es un bit a bit más general para cualquier número por cualquier multiplicación de números. Las macros que hice son aproximadamente un 25% más o dos veces más lentas de lo normal * multiplicación. como lo han dicho otros, si es cercano a un múltiplo de 2 o está compuesto por algunos múltiplos de 2, puede ganar. como X * 23 compuesto por (X << 4) + (X << 2) + (X << 1) + X va a ser más lento que X * 65 compuesto por (X << 6) + X.
fuente