x86 - lenguaje - ¿Por qué mov ah, bh y mov al, bl juntos son mucho más rápidos que una sola instrucción mov ax, bx?
offset ensamblador (4)
En el código de 32 bits, mov ax, bx
necesita un prefijo de tamaño de operando, mientras que los movimientos de tamaño byte no lo hacen. Aparentemente, los diseñadores de procesadores modernos no gastan mucho esfuerzo en lograr que el prefijo del tamaño del operando se decodifique rápidamente, aunque me sorprende que la penalización sea suficiente para hacer movimientos de dos bytes de tamaño en su lugar.
He encontrado que
mov al, bl
mov ah, bh
es mucho más rápido que
mov ax, bx
¿Alguien me puede explicar por qué? Me estoy ejecutando en Core 2 Duo 3 Ghz, en modo de 32 bits en Windows XP. Compilando usando NASM y luego enlazando con VS2010. Comando de compilación de Nasm:
nasm -f coff -o triangle.o triangle.asm
Aquí está el bucle principal que estoy usando para representar un triángulo:
; some variables on stack
%define cr DWORD [ebp-20]
%define dcr DWORD [ebp-24]
%define dcg DWORD [ebp-32]
%define dcb DWORD [ebp-40]
loop:
add esi, dcg
mov eax, esi
shr eax, 8
add edi, dcb
mov ebx, edi
shr ebx, 16
mov bh, ah
mov eax, cr
add eax, dcr
mov cr, eax
mov ah, bh ; faster
mov al, bl
;mov ax, bx
mov DWORD [edx], eax
add edx, 4
dec ecx
jge loop
Puedo proporcionar todo el proyecto de VS con fuentes para pruebas.
También es más rápido en mi CPU Core 2 Duo L9300 1.60GHz. Como escribí en un comentario, creo que esto está relacionado con el uso de registros parciales ( ah
, al
, ax
). Ver más, por ejemplo, here , here y here (pág. 88).
He escrito un pequeño paquete de prueba para intentar mejorar el código, y aunque no uso la versión de ax
presenta en el OP es el más inteligente, tratar de eliminar el uso parcial del registro mejora la velocidad (incluso más que mi rápido intento a liberar otro registro).
Para obtener más información sobre por qué una versión es más rápida que otra, creo que requiere una lectura más cuidadosa del material de origen y / o el uso de algo como Intel VTune o AMD CodeAnalyst. (Podría resultar que estoy equivocado)
ACTUALIZACIÓN, mientras que la siguiente salida de oprofile no prueba nada, muestra que hay muchas paradas de registro parciales en ambas versiones, pero aproximadamente el doble en la versión más lenta (triAsm2) que en la versión "rápida" ( triAsm1).
$ opreport -l test
CPU: Core 2, speed 1600 MHz (estimated)
Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (Unhalted core cycles) count 800500
Counted RAT_STALLS events (Partial register stall cycles) with a unit mask of 0x0f (All RAT) count 1000000
samples % samples % symbol name
21039 27.3767 10627 52.3885 triAsm2.loop
16125 20.9824 4815 23.7368 triC
14439 18.7885 4828 23.8008 triAsm1.loop
12557 16.3396 0 0 triAsm3.loop
12161 15.8243 8 0.0394 triAsm4.loop
Resultados:
triC: 7410.000000 ms, a5afb9 (implementación C del código asm)
triAsm1: 6690.000000 ms, a5afb9 (Código de OP, usando al
y ah
)
triAsm2: 9290.000000 ms, a5afb9 (Código de OP, usando el ax
)
triAsm3: 5760.000000 ms, a5afb9 (Traducción directa del código OP a uno sin uso de registro parcial)
triAsm4: 5640.000000 ms, a5afb9 (Intento rápido para hacerlo más rápido)
Aquí está mi conjunto de pruebas, compilado con -std=c99 -ggdb -m32 -O3 -march=native -mtune=native
:
prueba.c:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
extern void triC(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm1(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm2(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm3(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
extern void triAsm4(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb);
uint32_t scanline[640];
#define test(tri) /
{/
clock_t start = clock();/
srand(60);/
for (int i = 0; i < 5000000; i++) {/
tri(scanline, rand() % 640, 10<<16, 20<<16, 30<<16, 1<<14, 1<<14, 1<<14);/
}/
printf(#tri ": %f ms, %x/n",(clock()-start)*1000.0/CLOCKS_PER_SEC,scanline[620]);/
}
int main() {
test(triC);
test(triAsm1);
test(triAsm2);
test(triAsm3);
test(triAsm4);
return 0;
}
tri.c:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
void triC(uint32_t* dest, uint32_t cnt, uint32_t cr, uint32_t cg, uint32_t cb, uint32_t dcr, uint32_t dcg, uint32_t dcb) {
while (cnt--) {
cr += dcr;
cg += dcg;
cb += dcb;
*dest++ = (cr & 0xffff0000) | ((cg >> 8) & 0xff00) | ((cb >> 16) & 0xff);
}
}
atri.asm:
bits 32
section .text
global triAsm1
global triAsm2
global triAsm3
global triAsm4
%define cr DWORD [ebp+0x10]
%define dcr DWORD [ebp+0x1c]
%define dcg DWORD [ebp+0x20]
%define dcb DWORD [ebp+0x24]
triAsm1:
push ebp
mov ebp, esp
pusha
mov edx, [ebp+0x08] ; dest
mov ecx, [ebp+0x0c] ; cnt
mov esi, [ebp+0x14] ; cg
mov edi, [ebp+0x18] ; cb
.loop:
add esi, dcg
mov eax, esi
shr eax, 8
add edi, dcb
mov ebx, edi
shr ebx, 16
mov bh, ah
mov eax, cr
add eax, dcr
mov cr, eax
mov ah, bh ; faster
mov al, bl
mov DWORD [edx], eax
add edx, 4
dec ecx
jge .loop
popa
pop ebp
ret
triAsm2:
push ebp
mov ebp, esp
pusha
mov edx, [ebp+0x08] ; dest
mov ecx, [ebp+0x0c] ; cnt
mov esi, [ebp+0x14] ; cg
mov edi, [ebp+0x18] ; cb
.loop:
add esi, dcg
mov eax, esi
shr eax, 8
add edi, dcb
mov ebx, edi
shr ebx, 16
mov bh, ah
mov eax, cr
add eax, dcr
mov cr, eax
mov ax, bx ; slower
mov DWORD [edx], eax
add edx, 4
dec ecx
jge .loop
popa
pop ebp
ret
triAsm3:
push ebp
mov ebp, esp
pusha
mov edx, [ebp+0x08] ; dest
mov ecx, [ebp+0x0c] ; cnt
mov esi, [ebp+0x14] ; cg
mov edi, [ebp+0x18] ; cb
.loop:
mov eax, cr
add eax, dcr
mov cr, eax
and eax, 0xffff0000
add esi, dcg
mov ebx, esi
shr ebx, 8
and ebx, 0x0000ff00
or eax, ebx
add edi, dcb
mov ebx, edi
shr ebx, 16
and ebx, 0x000000ff
or eax, ebx
mov DWORD [edx], eax
add edx, 4
dec ecx
jge .loop
popa
pop ebp
ret
triAsm4:
push ebp
mov ebp, esp
pusha
mov [stackptr], esp
mov edi, [ebp+0x08] ; dest
mov ecx, [ebp+0x0c] ; cnt
mov edx, [ebp+0x10] ; cr
mov esi, [ebp+0x14] ; cg
mov esp, [ebp+0x18] ; cb
.loop:
add edx, dcr
add esi, dcg
add esp, dcb
;*dest++ = (cr & 0xffff0000) | ((cg >> 8) & 0xff00) | ((cb >> 16) & 0xff);
mov eax, edx ; eax=cr
and eax, 0xffff0000
mov ebx, esi ; ebx=cg
shr ebx, 8
and ebx, 0xff00
or eax, ebx
;mov ah, bh
mov ebx, esp
shr ebx, 16
and ebx, 0xff
or eax, ebx
;mov al, bl
mov DWORD [edi], eax
add edi, 4
dec ecx
jge .loop
mov esp, [stackptr]
popa
pop ebp
ret
section .data
stackptr: dd 0
Porque es lento
La razón por la que el uso de un registro de 16 bits es costoso en comparación con el uso de un registro de 8 bits es que las instrucciones del registro de 16 bits se decodifican en microcódigo. Esto significa un ciclo adicional durante la descodificación y la incapacidad de emparejarse mientras se descodifica.
Además, debido a que ax es un registro parcial, se necesitará un ciclo adicional para ejecutarse porque la parte superior del registro debe combinarse con la escritura en la parte inferior.
Las escrituras de 8 bits tienen un hardware especial para acelerar esto, pero las escrituras de 16 bits no. De nuevo, en muchos procesadores, las instrucciones de 16 bits toman 2 ciclos en lugar de uno y no permiten el emparejamiento.
Esto significa que en lugar de poder procesar 12 instrucciones (3 por ciclo) en 4 ciclos, ahora solo puede ejecutar 1, porque tiene un bloqueo al descodificar la instrucción en microcódigo y un bloqueo al procesar el microcódigo.
¿Cómo puedo hacerlo más rápido?
mov al, bl
mov ah, bh
(Este código requiere un mínimo de 2 ciclos de CPU y puede bloquearse en la segunda instrucción porque en algunas CPU x86 (más antiguas) se obtiene un bloqueo en EAX)
Esto es lo que sucede:
- EAX se lee. (ciclo 1)
- Se cambia el byte inferior de EAX (todavía ciclo 1)
- y el valor completo se vuelve a escribir en EAX. (ciclo 1)
- EAX está bloqueado para escritura hasta que la primera escritura se resuelva por completo. (espera potencial para ciclos múltiples)
- El proceso se repite para el byte alto en EAX. (ciclo 2)
En la última CPU Core2, esto no es tanto un problema, porque se ha implementado un hardware adicional que sabe que bl
y bh
nunca se interponen entre sí.
mov eax, ebx
Lo que mueve 4 bytes a la vez, esa única instrucción se ejecutará en 1 ciclo de cpu (y se puede emparejar con otras instrucciones en paralelo).
- Si desea un código rápido, utilice siempre los registros de 32 bits (EAX, EBX, etc.) .
- Intente evitar el uso de los subregistros de 8 bits, a menos que tenga que hacerlo.
- Nunca utilice los registros de 16 bits. Incluso si tiene que usar 5 instrucciones en modo de 32 bits, aún será más rápido.
- Use las instrucciones movzx reg, ... (o movsx reg, ...)
Acelerando el código
Veo algunas oportunidades para acelerar el código.
; some variables on stack
%define cr DWORD [ebp-20]
%define dcr DWORD [ebp-24]
%define dcg DWORD [ebp-32]
%define dcb DWORD [ebp-40]
mov edx,cr
loop:
add esi, dcg
mov eax, esi
shr eax, 8
add edi, dcb
mov ebx, edi
shr ebx, 16 ;higher 16 bits in ebx will be empty.
mov bh, ah
;mov eax, cr
;add eax, dcr
;mov cr, eax
add edx,dcr
mov eax,edx
and eax,0xFFFF0000 ; clear lower 16 bits in EAX
or eax,ebx ; merge the two.
;mov ah, bh ; faster
;mov al, bl
mov DWORD [epb+offset+ecx*4], eax ; requires storing the data in reverse order.
;add edx, 4
sub ecx,1 ;dec ecx does not change the carry flag, which can cause
;a false dependency on previous instructions which do change CF
jge loop
Resumen : las instrucciones de 16 bits no son el problema directamente. El problema es leer registros más amplios después de escribir registros parciales, lo que provoca un bloqueo parcial del registro en Core2. Esto es un problema mucho menor en Sandybridge y más adelante, ya que se fusionan mucho más barato. mov ax, bx
causa una fusión extra, pero incluso la versión "rápida" del OP tiene algunos bloqueos.
Vea el final de esta respuesta para un bucle interno escalar alternativo que debería ser más rápido que las otras dos respuestas, usando shld
para barajar bytes entre registros. Los cambios previos a los cambios dejados por 8b fuera del bucle colocan el byte que queremos en la parte superior de cada registro, lo que hace que esto sea realmente barato. Debe ejecutarse a una velocidad ligeramente mejor que una iteración por 4 ciclos de reloj en 32bit core2, y saturar los tres puertos de ejecución sin bloqueos. Debe ejecutarse en una iteración por 2.5c en Haswell.
Sin embargo, para hacer esto realmente rápido, mire la salida del compilador auto-vectorizada , y tal vez redúzcala o re-implemente con vectores intrínsecos.
Contrariamente a las afirmaciones de que las instrucciones de tamaño de operando de 16 bits son lentas, Core2 puede en teoría mantener 3 insns por reloj alternando mov ax, bx
y mov ecx, edx
. No hay ningún "interruptor de modo" de ningún tipo. (Como todos han señalado, el "cambio de contexto" es una elección terrible del nombre inventado, porque ya tiene un significado técnico específico).
El problema es que el registro parcial se detiene cuando lees un registro del que previamente escribiste solo una parte. En lugar de obligar a una escritura a esperar a que el contenido anterior de eax
esté listo (dependencia falsa), las CPU de la familia Intel P6 rastrean las dependencias para registros parciales por separado. La lectura del registro más amplio obliga a una combinación, que se detiene durante 2 a 3 ciclos de acuerdo con Agner Fog . El otro gran problema con el uso de un tamaño de operandos de 16 bits es con los operandos inmediatos, donde puede LCP se detiene en los decodificadores de las CPU de Intel para obtener información inmediata que no cabe en un imm8.
La familia SnB es mucho más eficiente, simplemente insertando un uop extra para hacer la fusión sin detenerse mientras lo hace. AMD e Intel Silvermont (y P4) no cambian el nombre de los registros parciales por separado, por lo que tienen dependencias "falsas" sobre el contenido anterior. En este caso, más tarde leeremos el registro completo, por lo que es una verdadera dependencia porque queremos la fusión, por lo que esas CPU tienen una ventaja. (Intel Haswell / Skylake (y quizás IvB) no cambia el nombre de AL por separado de RAX; solo cambia el nombre de AH / BH / CH / DH por separado. Y la lectura de registros high8 tiene una latencia adicional. Consulte estas preguntas y respuestas acerca de los registros parciales en HSW / SKL para los detalles .)
Ninguno de los bloqueos de registros parciales forman parte de una larga cadena de dependencia, ya que el registro combinado se sobrescribe en la siguiente iteración. ¿Aparentemente Core2 solo detiene el front-end, o incluso todo el núcleo de ejecución fuera de orden? Quería hacer una pregunta acerca de cuán caras son las demoras de registros parciales en Core2 y cómo medir el costo en SnB. La respuesta de oprofile de @ user786653 arroja algo de luz sobre ella. (Y también tiene algunos C realmente útiles de ingeniería inversa a partir del ASM OP para ayudar a aclarar lo que esta función realmente está tratando de lograr).
La compilación de esa C con un gcc moderno puede producir un asm vectorizado que hace el bucle 4 palabras a la vez, en un registro de xmm. Sin embargo, hace un trabajo mucho mejor cuando puede usar SSE4.1. (Y clang no auto-vectoriza esto en absoluto con -march=core2
, pero se desenrolla mucho, probablemente intercalando múltiples iteraciones para evitar cosas de registro parcial). Si no le dices a gcc que dest
está alineado, genera una gran cantidad de prólogo / epílogo escalar alrededor del bucle vectorizado para alcanzar un punto donde se alinea.
Convierte los argumentos de enteros en constantes vectoriales (en la pila, ya que el código de 32 bits solo tiene 8 registros vectoriales). El bucle interno es
.L4:
movdqa xmm0, XMMWORD PTR [esp+64]
mov ecx, edx
add edx, 1
sal ecx, 4
paddd xmm0, xmm3
paddd xmm3, XMMWORD PTR [esp+16]
psrld xmm0, 8
movdqa xmm1, xmm0
movdqa xmm0, XMMWORD PTR [esp+80]
pand xmm1, xmm7
paddd xmm0, xmm2
paddd xmm2, XMMWORD PTR [esp+32]
psrld xmm0, 16
pand xmm0, xmm6
por xmm0, xmm1
movdqa xmm1, XMMWORD PTR [esp+48]
paddd xmm1, xmm4
paddd xmm4, XMMWORD PTR [esp]
pand xmm1, xmm5
por xmm0, xmm1
movaps XMMWORD PTR [eax+ecx], xmm0
cmp ebp, edx
ja .L4
Tenga en cuenta que hay una tienda en todo el bucle. Todas las cargas son solo vectores que calculó anteriormente, almacenados en la pila como locales.
Hay varias formas de acelerar el código del OP . Lo más obvio es que no necesitamos hacer un marco de pila, liberando ebp
. El uso más obvio para ello es mantener cr
, que el OP derrama sobre la pila. triAsm4 de triAsm4
hace esto, excepto que usa la variación lógica del troll: ¡crea un marco de pila y configura ebp
como siempre, pero luego guarda esp
en una ubicación estática y lo usa como un registro de rasguño! Esto obviamente se romperá horriblemente si su programa tiene algún controlador de señal, pero de lo contrario está bien (excepto para hacer más difícil la depuración).
Si te vas a volver tan loco que quieres usar esp
como un rasguño, copia también la función args en ubicaciones estáticas, por lo que no necesitas un registro para guardar ningún puntero para apilar la memoria. (Guardar el antiguo esp
en un registro MMX también es una opción, por lo que puede hacer esto en las funciones de reingreso usadas desde múltiples subprocesos a la vez. Pero no si copia los argumentos en algún lugar estático, a menos que sea en un almacenamiento local de subprocesos con un anulación del segmento o algo así. No tiene que preocuparse por la reincorporación desde el mismo hilo, porque el puntero de la pila está en un estado inutilizable. Cualquier cosa como un controlador de señales que podría volver a ingresar su función en el mismo hilo accidente.>. <)
Spilling cr
no es la opción más óptima: en lugar de usar dos registros para bucles (contador y puntero), podemos mantener un puntero dst en un registro. Haga el límite del bucle calculando un puntero final (uno más allá del final: dst+4*cnt
), y use un cmp
con un operando de memoria como condición de bucle.
La comparación con un puntero final con cmp
/ jb
es realmente más óptima en Core2 que dec
/ jge
todos modos. Las condiciones no firmadas pueden macro-fusionarse con cmp
. Hasta SnB, solo cmp
y test
pueden macro-fusionarse. (Esto también es válido para AMD Bulldozer, pero cmp y la prueba pueden fusionarse con cualquier jcc en AMD). Las CPU de la familia SnB pueden macro-fusionar dec
/ jge
. Curiosamente, Core2 solo puede realizar comparaciones con macro-fusibles (como jge
) con test
, no con cmp
. (Una comparación sin firma es la opción correcta para una dirección de todos modos, ya que 0x8000000
no es especial, pero 0
es. No 0x8000000
jb
solo como una optimización de riesgo).
No podemos hacer un cambio previo de cb
y dcb
hasta el byte bajo, porque necesitan mantener una mayor precisión interna. Sin embargo, podemos cambiar a la izquierda los otros dos, por lo que están contra el borde izquierdo de sus registros. Desplazándolos hacia la derecha a su posición de destino no dejará ningún bit de alta basura de un posible desbordamiento.
En lugar de fusionarnos con eax
, podríamos hacer tiendas superpuestas. Almacene 4B de eax
, luego almacene el bajo 2B de bx
. Eso ahorraría el puesto de registro parcial en eax, pero generaría uno para fusionar bh
en ebx
, por lo que tiene un valor limitado. Posiblemente, una escritura 4B y dos tiendas 1B superpuestas son realmente buenas aquí, pero eso está empezando a ser una gran cantidad de tiendas. Aún así, podría extenderse sobre otras instrucciones suficientes para no causar cuellos de botella en el puerto de la tienda.
triAsm3 de user786653 utiliza enmascaramiento e instrucciones para fusionar, lo que parece ser un enfoque sensato para Core2. Para AMD, Silvermont o P4, el uso de las instrucciones de movimiento 8b y 16b para combinar registros parciales probablemente sea realmente bueno. También puedes aprovecharla en Ivybridge / Haswell / Skylake si solo escribes low8 o low16 para evitar la fusión de multas. Sin embargo, se me ocurrieron varias mejoras sobre eso para requerir menos enmascaramiento.
; use defines you can put [] around so it''s clear they''re memory refs ; %define cr ebp+0x10 %define cr esp+something that depends on how much we pushed %define dcr ebp+0x1c ;; change these to work from ebp, too. %define dcg ebp+0x20 %define dcb ebp+0x24 ; esp-relative offsets may be wrong, just quickly did it in my head without testing: ; we push 3 more regs after ebp, which was the point at which ebp snapshots esp in the stack-frame version. So add 0xc (i.e. mentally add 0x10 and subract 4) ; 32bit code is dumb anyway. 64bit passes args in regs. %define dest_arg esp+14 %define cnt_arg esp+18 ... everything else tri_pjc: push ebp push edi push esi push ebx ; only these 4 need to be preserved in the normal 32bit calling convention mov ebp, [cr] mov esi, [cg] mov edi, [cb] shl esi, 8 ; put the bits we want at the high edge, so we don''t have to mask after shifting in zeros shl [dcg], 8 shl edi, 8 shl [dcb], 8 ; apparently the original code doesn''t care if cr overflows into the top byte. mov edx, [dest_arg] mov ecx, [cnt_arg] lea ecx, [edx + ecx*4] ; one-past the end, to be used as a loop boundary mov [dest_arg], ecx ; spill it back to the stack, where we only need to read it. ALIGN 16 .loop: ; SEE BELOW, this inner loop can be even more optimized add esi, [dcg] mov eax, esi shr eax, 24 ; eax bytes = { 0 0 0 cg } add edi, [dcb] shld eax, edi, 8 ; eax bytes = { 0 0 cg cb } add ebp, [dcr] mov ecx, ebp and ecx, 0xffff0000 or eax, ecx ; eax bytes = { x cr cg cb} where x is overflow from cr. Kill that by changing the mask to 0x00ff0000 ; another shld to merge might be faster on other CPUs, but not core2 ; merging with mov cx, ax would also be possible on CPUs where that''s cheap (AMD, and Intel IvB and later) mov DWORD [edx], eax ; alternatively: ; mov DWORD [edx], ebp ; mov WORD [edx], eax ; this insn replaces the mov/and/or merging add edx, 4 cmp edx, [dest_arg] ; core2 can macro-fuse cmp/unsigned condition, but not signed jb .loop pop ebx pop esi pop edi pop ebp ret
Terminé con un registro más de lo que necesitaba, después de hacer el puntero omit-frame y poner el límite del bucle en la memoria. Puede almacenar algo extra en los registros o evitar guardar / restaurar un registro. Tal vez mantener la frontera del bucle en ebx
es la mejor apuesta. Básicamente guarda una instrucción de prólogo. Mantener dcb
o dcg
en un registro requeriría una inserción adicional en el prólogo para cargarlo. (Los turnos con un destino de memoria son feos y lentos, incluso en Skylake, pero el tamaño del código es pequeño. No están en el bucle, y core2 no tiene un caché uop. Cargar / cambiar / almacenar por separado sigue siendo 3 uops, por lo que no puede superarlo a menos que lo guarde en un registro en lugar de almacenarlo.)
shld
es una inserción 2-uop en P6 (Core2). Afortunadamente, es fácil ordenar el bucle, por lo que es la quinta instrucción, precedida por cuatro instrucciones de un solo uop. Debería llegar a los decodificadores como el primer uop en el segundo grupo de 4, para que no cause un retraso en la interfaz. ( Core2 puede decodificar 1-1-1-1, 2-1-1-1, 3-1-1-1 o 4-1-1-1 uops-per-insn patrones. SnB y luego rediseñaron los decodificadores, y agregó un caché uop que hace que la decodificación no sea usualmente el cuello de botella, y solo puede manejar grupos de 1-1-1-1, 2-1-1, 3-1 y 4.)
shld
es horrible en AMD K8, K10, Bulldozer-family y Jaguar . 6 m-ops, 3c de latencia y uno por rendimiento de 3c. Es genial en Atom / Silvermont con un tamaño de operando de 32 bits, pero horrible con registros de 16 o 64b.
Esta ordenación interna puede decodificarse con el cmp
como la última inserción de un grupo, y luego jb
por sí mismo, haciendo que no sea un macro-fusible. Esto podría otorgar una ventaja adicional al método de fusión de las tiendas superpuestas, más que solo guardar un uop, si los efectos frontales son un factor para este bucle. (Y sospecho que lo serían, dado el alto grado de paralelismo y que las cadenas de depresiones en bucle son cortas, por lo que se pueden realizar varias iteraciones al mismo tiempo).
Por lo tanto: uops de dominio fusionado por iteración: 13 en Core2 (asumiendo una macro-fusión que podría no ocurrir), 12 en la familia SnB. Por lo tanto, IvB debe ejecutar esto en una iteración por 3c (asumiendo que ninguno de los 3 puertos ALU es un cuello de botella. El mov r,r
no necesita puertos ALU, y tampoco la tienda. add
y booleans pueden usar cualquier puerto. shr
and son los únicos que no pueden ejecutarse en una amplia variedad de puertos, y solo hay dos turnos por tres ciclos.) Core2 tomará 4c por iteración para emitirlo, incluso si logra evitar los cuellos de botella frontend, e incluso más para ejecutarlo.
Tal vez todavía estemos ejecutando lo suficientemente rápido en Core2 para que la descarga / recarga de cr
en la pila, cada iteración sea un cuello de botella si todavía estuviéramos haciendo eso. Agrega una memoria de ida y vuelta (5c) a una cadena de dependencia transportada por bucle, haciendo una longitud total de la cadena de depuración de 6 ciclos (incluido el complemento).
Hmm, en realidad incluso Core2 podría ganar usando dos inscripciones shld para fusionar. También guarda otro registro!
ALIGN 16 ;mov ebx, 111 ; IACA start ;db 0x64, 0x67, 0x90 .loop: add ebp, [dcr] mov eax, ebp shr eax, 16 ; eax bytes = { 0 0 x cr} where x is overflow from cr. Kill that pre-shifting cr and dcr like the others, and use shr 24 here add esi, [dcg] shld eax, esi, 8 ; eax bytes = { 0 x cr cg} add edx, 4 ; this goes between the `shld`s to help with decoder throughput on pre-SnB, and to not break macro-fusion. add edi, [dcb] shld eax, edi, 8 ; eax bytes = { x cr cg cb} mov DWORD [edx-4], eax cmp edx, ebx ; use our spare register here jb .loop ; core2 can macro-fuse cmp/unsigned condition, but not signed. Macro-fusion works in 32-bit mode only on Core2. ;mov ebx, 222 ; IACA end ;db 0x64, 0x67, 0x90
Per-iteración: SnB: 10 uops de dominio fusionado. Core2: 12 uops de dominio fusionado, por lo que es más corto que la versión anterior en las CPU Intel (pero horrible en AMD). El uso de shld
guarda las instrucciones de mov
porque podemos usarlo para extraer de forma no destructiva el byte alto de la fuente.
Core2 puede emitir el bucle en una iteración por 3 relojes. (Fue la primera CPU de Intel con un pipeline ancho de 4 uop).
De la tabla de Agner Fog para Merom / Conroe (primera generación Core2) (note que el diagrama de bloques de David Kanter tiene p2 y p5 invertidos):
-
shr
: se ejecuta en p0 / p5 -
shld
: 2 uops para p0 / p1 / p5? La mesa de Agner para pre-Haswell no dice cuáles uops pueden ir a dónde. -
mov r,r
,add
,and
: p0 / p1 / p5 - cmp fundido y rama: p5
- store: p3 and p4 (estos micro-fusibles en 1 uop de tienda de dominio fusionado)
- cada carga: p2. (todas las cargas están microfusionadas con operaciones de ALU en el dominio fusionado).
De acuerdo con IACA, que tiene un modo para Nehalem pero no para Core2, la mayoría de los uops shld van a p1, con solo menos de 0.6 en promedio de cada entrada que se ejecuta en otros puertos. Nehalem tiene esencialmente las mismas unidades de ejecución que Core2. Todas las instrucciones involucradas aquí tienen los mismos costos de uop y requisitos de puerto en NHM y Core2. El análisis de IACA me parece bien, y no quiero revisar todo por mi cuenta para esta respuesta a una pregunta de 5 años. Sin embargo, fue divertido contestar. :)
De todos modos, según IACA, uops debería distribuirse bien entre puertos. Calcula que Nehalem puede ejecutar el bucle en una iteración por 3.7 ciclos, saturando los tres puertos de ejecución. Su análisis me parece bien. (Tenga en cuenta que tuve que eliminar el operando de memoria de cmp
para que IACA no dé resultados estúpidos.) Claramente, esto es necesario, ya que pre-SnB solo puede hacer una carga por ciclo: nos atascaríamos en el puerto 2 con cuatro cargas en el lazo.
IACA no está de acuerdo con las pruebas de Agner Fog para IvB y SnB (piensa que shld sigue siendo 2 uops, cuando en realidad es una, de acuerdo con mis pruebas en SnB). Así que sus números son tontos.
IACA parece correcta para Haswell, donde dice que el cuello de botella es la interfaz. Piensa que HSW puede ejecutarse a una por 2.5c. (El búfer de bucle en Haswell al menos puede emitir bucles en un número no entero de ciclos por iteración. Sandybridge puede limitarse a números enteros de ciclos, donde la rama de bucle tomada termina un grupo de problemas ).
También encontré que necesitaba usar iaca.sh -no_interiteration
, o de lo contrario pensaría que había una dependencia transmitida por el bucle de interiteración y creo que el bucle tomaría 12c en el NHM.