Como siempre, depende del contexto del código circundante : por ejemplo, ¿está utilizando x<<1
como índice de matriz? ¿O agregarlo a otra cosa? En cualquier caso, los recuentos de cambios pequeños (1 o 2) a menudo pueden optimizar incluso más que si el compilador acabara teniendo que cambiar. Sin mencionar el intercambio total de rendimiento vs. latencia vs. cuellos de botella de front-end. El rendimiento de un pequeño fragmento no es unidimensional.
Las instrucciones de cambio de hardware no son la única opción de un compilador para compilar x<<1
, pero las otras respuestas suponen principalmente eso.
x << 1
es exactamente equivalente ax+x
para enteros sin signo y complemento a 2 con signo. Los compiladores siempre saben a qué hardware se dirigen mientras compilan, por lo que pueden aprovechar trucos como este.
En Intel Haswell , add
tiene un rendimiento de 4 por reloj, pero shl
con un recuento inmediato tiene solo 2 por rendimiento de reloj. (Consulte http://agner.org/optimize/ para obtener tablas de instrucciones y otros enlaces en elx86etiqueta wiki). Los cambios de vector SIMD son 1 por reloj (2 en Skylake), pero las adiciones de enteros vectoriales SIMD son 2 por reloj (3 en Skylake). Sin embargo, la latencia es la misma: 1 ciclo.
También hay una codificación especial shift-by-one de shl
dónde está implícita la cuenta en el código de operación. 8086 no tenía turnos de conteo inmediato, solo por uno y por cl
registro. Esto es sobre todo relevante para los desplazamientos a la derecha, porque solo puede agregar para desplazamientos a la izquierda a menos que esté desplazando un operando de memoria. Pero si el valor se necesita más tarde, es mejor cargar primero en un registro. Pero de todos modos, shl eax,1
o add eax,eax
es un byte más corto que shl eax,10
, y el tamaño del código puede afectar directamente (decodificar / cuellos de botella de front-end) o indirectamente (fallas de caché de código L1I) afectar el rendimiento.
De manera más general, los recuentos de cambios pequeños a veces se pueden optimizar en un índice escalado en un modo de direccionamiento en x86. La mayoría de las otras arquitecturas de uso común en estos días son RISC y no tienen modos de direccionamiento de índice escalado, pero x86 es una arquitectura lo suficientemente común como para que valga la pena mencionarlo. (huevo si está indexando una matriz de elementos de 4 bytes, hay espacio para aumentar el factor de escala en 1 para int arr[]; arr[x<<1]
).
La necesidad de copiar + desplazamiento es común en situaciones en las que x
aún se necesita el valor original de . Pero la mayoría de las instrucciones de enteros x86 funcionan in situ. (El destino es una de las fuentes para instrucciones como add
o shl
). La convención de llamadas de System V x86-64 pasa args en registros, con el primer argumento edi
y el valor de retorno eax
, por lo que una función que devuelve x<<10
también hace que el compilador emita copy + shift código.
La LEA
instrucción le permite cambiar y agregar (con un recuento de cambios de 0 a 3, porque utiliza codificación de máquina en modo de direccionamiento). Pone el resultado en un registro separado.
gcc y clang optimizan estas funciones de la misma manera, como puede ver en el explorador del compilador Godbolt :
int shl1(int x) { return x<<1; }
lea eax, [rdi+rdi] # 1 cycle latency, 1 uop
ret
int shl2(int x) { return x<<2; }
lea eax, [4*rdi] # longer encoding: needs a disp32 of 0 because there's no base register, only scaled-index.
ret
int times5(int x) { return x * 5; }
lea eax, [rdi + 4*rdi]
ret
int shl10(int x) { return x<<10; }
mov eax, edi # 1 uop, 0 or 1 cycle latency
shl eax, 10 # 1 uop, 1 cycle latency
ret
LEA con 2 componentes tiene 1 ciclo de latencia y 2 por reloj en CPU recientes de Intel y AMD. (Familia Sandybridge y Bulldozer / Ryzen). En Intel, es solo 1 rendimiento por reloj con latencia de 3c para lea eax, [rdi + rsi + 123]
. (Relacionado: ¿Por qué este código C ++ es más rápido que mi ensamblaje escrito a mano para probar la conjetura de Collatz? Entra en esto en detalle).
De todos modos, copiar + desplazar por 10 necesita una mov
instrucción separada . Puede ser una latencia cero en muchas CPU recientes, pero aún requiere ancho de banda de front-end y tamaño de código. ( ¿Puede el MOV de x86 ser realmente "gratuito"? ¿Por qué no puedo reproducirlo? )
También relacionado: ¿Cómo multiplicar un registro por 37 usando solo 2 instrucciones leales consecutivas en x86? .
El compilador también es libre de transformar el código circundante para que no haya un cambio real o se combine con otras operaciones .
Por ejemplo, if(x<<1) { }
podría usar an and
para verificar todos los bits excepto el bit alto. En x86, usaría una test
instrucción, como test eax, 0x7fffffff
/ en jz .false
lugar de shl eax,1 / jz
. Esta optimización funciona para cualquier recuento de turnos, y también funciona en máquinas donde los turnos de recuento grande son lentos (como Pentium 4) o inexistentes (algunos microcontroladores).
Muchas ISA tienen instrucciones de manipulación de bits más allá del simple cambio. por ejemplo, PowerPC tiene muchas instrucciones de extracción / inserción de campos de bits. O ARM tiene cambios de operandos fuente como parte de cualquier otra instrucción. (Por lo tanto, las instrucciones de cambio / rotación son solo una forma especial de move
usar una fuente desplazada).
Recuerde, C no es lenguaje ensamblador . Siempre observe la salida optimizada del compilador cuando esté ajustando su código fuente para compilar de manera eficiente.