Puede usar <<
para multiplicar y >>
dividir números en python cuando los cronometrado y encuentro que usar la forma de desplazamiento binario es 10 veces más rápido que dividir o multiplicar la forma regular.
¿Por qué está usando <<
y >>
mucho más rápido que *
y /
?
¿Cuáles son los procesos detrás de escena que se producen *
y son /
tan lentos?
operators
bitwise-operators
Crizly
fuente
fuente
Respuestas:
Veamos dos pequeños programas en C que cambian un poco y se dividen.
Estos se compilan
gcc -S
para ver cuál será el ensamblaje real.Con la versión bit shift, desde la llamada
atoi
hasta la devolución:Mientras que la versión dividida:
Con solo mirar esto, hay varias instrucciones más en la versión dividida en comparación con el cambio de bits.
La clave es ¿qué hacen?
En la versión de cambio de bit, la instrucción clave es
shll $2, %eax
el cambio lógico a la izquierda: existe la división, y todo lo demás es solo mover valores.En la versión de división, puede ver el
idivl %r8d
- pero justo encima de eso hay uncltd
(convertir de largo a doble) y algo de lógica adicional alrededor del derrame y la recarga. Este trabajo adicional, sabiendo que estamos tratando con una matemática en lugar de bits, a menudo es necesario para evitar varios errores que pueden ocurrir al hacer solo una matemática de bits.Hagamos una multiplicación rápida:
En lugar de pasar por todo esto, hay una línea diferente:
Aquí el compilador pudo identificar que las matemáticas podían hacerse con un cambio, sin embargo, en lugar de un cambio lógico, hace un cambio aritmético. La diferencia entre estos sería obvia si los ejecutamos:
sarl
conserva el signo. Para que-2 * 4 = -8
mientras elshll
no lo haga.Veamos esto en un script rápido de perl:
Salida:
Um ...
-4 << 2
es lo18446744073709551600
que no es exactamente lo que esperas cuando se trata de multiplicación y división. Es correcto, pero no es una multiplicación entera.Y por lo tanto, tenga cuidado con la optimización prematura. Deje que el compilador se optimice por usted: sabe lo que realmente está tratando de hacer y probablemente lo hará mejor, con menos errores.
fuente
<< 2
con* 4
y>> 2
con/ 4
para mantener las direcciones de cambio iguales en cada ejemplo.Las respuestas existentes realmente no abordaron el lado del hardware de las cosas, así que aquí hay un poco en ese ángulo. La sabiduría convencional es que la multiplicación y la división son mucho más lentas que los cambios, pero la historia real de hoy es más matizada.
Por ejemplo, es cierto que la multiplicación es una operación más compleja de implementar en hardware, pero no siempre termina más lentamente . Como resultado,
add
también es significativamente más complejo de implementar quexor
(o en general cualquier operación bit a bit), peroadd
(ysub
) generalmente obtienen suficientes transistores dedicados a su operación que terminan siendo tan rápidos como los operadores bit a bit. Por lo tanto, no puede simplemente mirar la complejidad de la implementación de hardware como una guía para la velocidad.Así que veamos en detalle el desplazamiento frente a los operadores "completos" como la multiplicación y el desplazamiento.
Cambiando
En casi todo el hardware, el cambio en una cantidad constante (es decir, una cantidad que el compilador puede determinar en tiempo de compilación) es rápido . En particular, generalmente sucederá con una latencia de un solo ciclo y con un rendimiento de 1 por ciclo o mejor. En algunos equipos (p. Ej., Algunos chips Intel y ARM), ciertos cambios por una constante pueden incluso ser "gratuitos", ya que pueden integrarse en otra instrucción (
lea
en Intel, las capacidades especiales de cambio de la primera fuente en ARM).El desplazamiento en una cantidad variable es más un área gris. En hardware antiguo, esto a veces era muy lento y la velocidad cambiaba de generación en generación. Por ejemplo, en el lanzamiento inicial de Intel P4, el cambio en una cantidad variable fue notoriamente lento, ¡lo que requiere un tiempo proporcional a la cantidad de cambio! En esa plataforma, el uso de multiplicaciones para reemplazar los turnos podría ser rentable (es decir, el mundo se ha vuelto al revés). En chips Intel anteriores, así como en generaciones posteriores, el cambio en una cantidad variable no fue tan doloroso.
En los chips Intel actuales, el cambio en una cantidad variable no es particularmente rápido, pero tampoco es terrible. La arquitectura x86 está limitada en lo que respecta a los cambios variables, porque definieron la operación de una manera inusual: las cantidades de cambios de 0 no modifican los indicadores de condición, pero todos los demás cambios sí. Esto inhibe el cambio de nombre eficiente del registro de banderas, ya que no se puede determinar hasta que se ejecute el turno si las instrucciones posteriores deben leer los códigos de condición escritos por el turno, o alguna instrucción previa. Además, los turnos solo escriben en parte del registro de banderas, lo que puede causar un bloqueo parcial de banderas.
El resultado es que en las arquitecturas recientes de Intel, el cambio en una cantidad variable requiere tres "microoperaciones", mientras que la mayoría de las otras operaciones simples (agregar, operaciones bit a bit, incluso multiplicación) solo toman 1. Tales cambios pueden ejecutarse como máximo una vez cada 2 ciclos .
Multiplicación
La tendencia en el hardware moderno de computadoras de escritorio y portátiles es hacer que la multiplicación sea una operación rápida. En los recientes chips Intel y AMD, de hecho, se puede emitir una multiplicación cada ciclo (a esto le llamamos rendimiento recíproco ). La latencia , sin embargo, de una multiplicación es de 3 ciclos. Eso significa que obtienes el resultado de cualquier multiplicación dada 3 ciclos después de comenzar, pero puedes comenzar una nueva multiplicación en cada ciclo. El valor (1 ciclo o 3 ciclos) es más importante depende de la estructura de su algoritmo. Si la multiplicación es parte de una cadena de dependencia crítica, la latencia es importante. De lo contrario, el rendimiento recíproco u otros factores pueden ser más importantes.
La conclusión clave es que en los chips de portátiles modernos (o mejores), la multiplicación es una operación rápida y es probable que sea más rápida que la secuencia de instrucciones 3 o 4 que emitiría un compilador para "obtener el redondeo" correcto para los cambios de fuerza reducida. Para los cambios variables, en Intel, la multiplicación también se preferiría generalmente debido a los problemas mencionados anteriormente.
En plataformas de factor de forma más pequeñas, la multiplicación aún puede ser más lenta, ya que construir un multiplicador completo y rápido de 32 bits o especialmente de 64 bits requiere muchos transistores y potencia. Si alguien puede completar los detalles del rendimiento de la multiplicación en chips móviles recientes, sería muy apreciado.
Dividir
La división es una operación más compleja, en cuanto a hardware, que la multiplicación y también es mucho menos común en el código real, lo que significa que es probable que se le asignen menos recursos. La tendencia en los chips modernos sigue siendo hacia divisores más rápidos, pero incluso los chips modernos de primera línea tardan entre 10 y 40 ciclos en dividirse, y solo están parcialmente canalizados. En general, las divisiones de 64 bits son incluso más lentas que las de 32 bits. A diferencia de la mayoría de las otras operaciones, la división puede tomar un número variable de ciclos dependiendo de los argumentos.
¡Evite las divisiones y reemplácelas con turnos (o deje que el compilador lo haga, pero es posible que deba verificar el ensamblaje) si puede!
fuente
BINARY_LSHIFT y BINARY_RSHIFT son procesos más simples algorítmicamente que BINARY_MULTIPLY y BINARY_FLOOR_DIVIDE y pueden tomar menos ciclos de reloj. Es decir, si tiene algún número binario y necesita cambiar bits por N, todo lo que tiene que hacer es desplazar los dígitos sobre esos espacios y reemplazarlos con ceros. La multiplicación binaria es en general más complicada , aunque técnicas como el multiplicador Dadda lo hacen bastante rápido.
De acuerdo, es posible que un compilador optimizador reconozca casos cuando multiplica / divide por potencias de dos y reemplaza con el desplazamiento apropiado izquierda / derecha. Al observar el código de byte desmontado, python aparentemente no hace esto:
Sin embargo, en mi procesador, encuentro que la multiplicación y el desplazamiento hacia la izquierda / derecha tienen un tiempo similar, y la división del piso (por una potencia de dos) es aproximadamente un 25% más lenta:
fuente