ASM & DESMOSCENE
eLectronic @ magaZine #0
Curso de Demos. Nivel Básico I
INTRODUCCION
Hola all!
Aquí estamos con la primera entraga del curso básico. Para seguirlo
basta con haber hecho algún COM-EXE en ensamblador, saber lo que es una
interrupción, los segmentos y tener nociones generales de las
instrucciones. Si no es así pasaros antes por el curso de ASM de
Pablo Barrón ;)
Voy a dividir este capítulo en dos, optimizaciones del 386, y las
del 486/Pentium.
Otra cosa, no es lo mismo optimizar para velocidad que para tamaño,
aquí voy a poner optimizaciones para velocidad.
OPTIMIZACION EN EL 386
La mayor ventaja que nos ofrece el ensamblador es su velocidad de
ejecución. Los compiladores de C++ y Pascal han logrado un alto grado
de optimización, pero nada automático puede sustituir al programador
(aunque últimamente el WatCom est dejando pasmado a todo el mundo).
Por ejemplo, si asignamos 0 a dos segmentos el compilador hará esto:
xor ax,ax ;Esto está bien optimizado, es más rápido
mov es,ax ;xor que 'mov ax,0'
xor ax,ax ;Esto es lo que sobra y una persona nunca pondría.
mov ds,ax
Por esta razón no queda más remedio que programar en ensamblador las
partes del programa que requieran mayor velocidad. Las optimizaciones
más generales son éstas:
COMPARACIONES
a.- OR y AND en vez de CMP
lento rápido
cmp ax,0 ---> or ax,ax
je label ---> jz label
cmp ax,bx ---> and ax,bx
je label ---> jz label
La comparación de ax con 0 es más rápida con or, y con bx
es más rápida el and. Siempre que tengamos que comparar un registro y
no nos importe perder su valor utilizaremos OR y AND. Tened en cuenta
que no lo podemos usar siempre porque con el AND y el OR perdemos el
valor que hay en AX, y con CMP no.
En el primer caso también podría ser usado para comprobar el signo:
or ax,ax
js label
b.- COMPARACIONES DE CADENAS: jcxz
Para comparar cadenas utilizaremos JCXZ:
Comparación de cadenas
dirección cad1 -> ES:DI
dirección cad2 -> DS:DI
longitud -> CX
repe cmpsb Sigue hasta que cx = 0 o cad1 distinto de cad2
jcxz label
Es decir, se cargan los punteros en ES:DI (segmento:dirección)
y en DS:DI y la instrucción JCXZ saltará a label cuando cx sea 0.
El funcionamiento sería así:
- Se cargan punteros
- repe va comparando hasta que las cadenas presenten
diferencias o finalicen (CX=0).
- Si las cadenas son diferentes CX no es 0, jcxz no salta.
- Si las cadenas son iguales CX=0, jcxz salta.
c.- VARIABLES DE SEGMENTO DE CODIGO MEZCLANDO C/PASCAL/ASM.
En principio se pretende usar todas las variables que el asm
nos ofrece, pero en el caso de la variable bp hay un problema:
el bp se utiliza para el direccionamiento de las variables locales
de un procedimiento.
Tampoco se pueden usar variables globales porque en los procedimientos
grÁficos DS deja de apuntar al segmento de datos para poner la dirección
de inicio de pantalla o se¤alar dónde has guardado los sprites, por ejemplo.
Así que si queremos usar BP, tendremos que crear las variables dentro de
los procedimientos, en el código.
Para los que estais acostumbrados al Borland (dir‚ Borland para decir
Pascal y C++ a la vez) se complica el asunto un poco más, porque no deja
crear variables en el código, pero se puede emplear un pequeño truco con
JMP:
Pascal C++
Procedure test;ensamblador; * void test(void){
asm * asm{
jmp @los * jmp los
*
@var1: dw 0 * @var1: dw 0
@var2: dw 0 * @var2: dw 0 };
@los: * los:
{Resto del proc} * //Resto del proc
End; * }
Aquí en cada acceso habrá que usar word ptr o byte ptr ya
que Borland identificará @var1 y @var2 como etiquetas y no como
variables.
d.- TABLAS
Para utilizar operaciones matemÁticas como seno, coseno, etc,
la forma mÁs rÁpida y sin necesidad de utilizar copro es el
uso de tablas.
Para senos, cosenos, etc -> Utilización de array
Para hacer que se repitan los valores se utilizar un array circular
(para que pueda dar el seno de 700, por ejemplo), se hace un array de 16,
64, 256... lo que sea. El seno ser por ejemplo cx=sen(700):
mov bx,700
and 63,bx ;si es de 64 el array
mov cx,array[bx]
Para definir las tablas se puede usar el Borland en lugar de
meter a mano todos los valores:
(Supongamos que la longitud de la tabla es PER, la tabla es TABLA,
la altura en la variable es AMP, y el punto 0 lo da OFFSET)
Pascal:
procedure sin_gen(var tabla:Array of word;per,amp,offset:word);
Var i:Word;
Begin
for i:=0 to per-1 do
tabla[i]:=round(sin(i*2*pi/per)*amp)+offset;
End;
C++:
void sin_gen(int *tabla, int per, int amp,int offset)
{
for(int i=0;i"<"per;i++)
tabla[i]=(int)(sin(i*2*3.1415/per)*amp)+offset;
}
Estos procs nos generaría una tabla de senos que podríamos usar
cómodamente en ASM, y funcionaría mucho más rápido.
e.- MALDITO LOOP
Lento Rápido
loop label dec cx
jne label
La orden loop ha sido abandonada por los desarroladores de los
micros. Como resultado es más rápido decrementar y saltar
que hacer el loop (un 10% más rápido en los 386 y hasta un 40%
en 486).
Cuando se aniden bucles hay que tener en cuenta varias reglas
fundamentales:
- Utilizar registros como contadores, no variables de memoria.
- Cuenta descendente (de x a 0).
- Si se requiere que el caso 0 se ejecute se usa el flag signo
en vez del flag zero (js en vez de jz).
También hay que tener en cuenta que cuando se quiere hacer una
comparación el 'set' es muy rápido, y hay uno para cada
tipo de salto (setz->jz, setnz->jnz, sets->js...).
Por ejemplo:
dec cx ;Aquí set compara CX y AL, si se cumple -> AL pasará a
set al ;valer 1, sino 0.
g.- NUEVAS INSTRUCCIONES DEL 386
Muchas veces, acostumbrados al 286 pasamos por alto instrucciones
del 386 muy útiles. Por ejemplo:
MOVER 8 BITS A 16
Para mover BL a AX
largo corto
xor bh,bh movzx ax,bl (con signo sería movsx)
mov ax,bx
A pesar de ser más cómodo el MOVZX es muy lento.
MULTIPLICACION RAPIDA
IMUL permite multiplicar cualquier registro, no solo ax.
imul dx,3 ;Multiplica dx por 3 y pone el resultado en dx
imul ax,dx,3 ;Multiplica dx por 3 y pone el resultado en ax
h.- EL ORDEN DE LAS INSTRUCCIONES
Después de efectuar un salto cada componente de la primera instrucción
que se encuentre tardará un ciclo de reloj más (en modo 32 bits).
Será más rápido:
loopme: inc esi ; opcode
mov [edi+1234h],eax ; opcode, mod/rm , immediate offset
dec ecx ; opcode
jne short loopme ; opcode short_displacement
que
loopme: mov [edi+1234h],eax
inc esi
dec ecx
jne short loopme
En el primer caso se a¤ade un ciclo, en el segundo 3. Si por ejemplo
este loop fuese para copiar una imagen a pantalla de 320x200 tendríamos
caso 2: 64000*3ciclos=192000
caso 1: 64000*1ciclo=64000 -> 128000 ciclos más en el segundo caso
OPTIMIZACION EN 486/PENTIUM
Primero voy a explicar cómo funcionan y luego cómo acelerarlos.
La mayor diferencia para nosotros a la hora de programar entre un
386 y un 486 es el pipeline.
La ejecución de una instrucción se realiza en varias fases:
1. FETCH: lee el código de la instrucción y los datos.
2. DECODE: interpreta la instrucción y guarda los datos en registros
temporales.
3. C.ADRESS: calcula las direcciones de los operandos y lo carga en
memoria.
4. EXECUTE: ejecuta la instrucción y escribe los resultados en registros
internos.
5. WRITEBACK: escribe los resultados en memoria.
Cada etapa dura uno o más ciclos de reloj, así que para conseguir
ejecutar los 5 pasos en un solo ciclo la CPU tiene 5 unidades internas,
cada una especializada en uno de los pasos, una memoria interna
superrápida para los valores de los registros (REGISTER FILE), y una
unidad de control que coordina a los demás.
Al dividir el trabajo en cinco partes independientes el CPU
trabaja como si fuera una fábrica en cadena:
Imagínate que se tienen que procesar las instrucciones A,B,C,D,E.
Mientras el WRITEBACK realiza la operación 5 de la instrucción A
el EXECUTION realiza la 4 de la B
el ADDRESS CALCULATE la 3 de la C
el DECODE la 2 de la D
y el FETCH la 1 de la E.
Se están realizando las 5 operaciones en un ciclo de reloj. Tras 5
ciclos se habrán completado 5 instrucciones: cada instrucción dura
1 ciclo. A esta forma de trabajar se le llama cascada o PIPELINE.
En una CPU que no fuese PIPELINE la primera unidad no podría
empezar con la siguiente instrucción hasta que la quinta unidad
hubiese terminado.
Cosas que la unidad de control debe comprobar para que el PIPELINE
funcione correctamente:
a.- En unidad 1: FETCH
Si hay un JMP en alguna unidad de la 2 a la 4, la 1 espera a que el
salto se complete (mínimo 2 ciclos) y descarta los valores de la 2
y la 3, y después comienza otra vez la instrucción. Si no se cumple
el salto mantiene los datos, por eso el JMP son 3 ciclos si salta
y sólo 1 si no salta.
El Pentium predice si el salto se va a llevar a cabo o no antes
de ejecutarlo, así que no pierde ningún ciclo.
b.- En unidad 3: ADDRESS CALCULATION
Si en la unidad 4 se está calculando un registro que es necesario
para que el 3 calcule la dirección se genera una secuencia de espera
de 1 ciclo.
c.- Conflictos en el acceso a memoria
Cuando una unidad necesita el valor que se está calculando en otra unidad
se genera una espera de un ciclo, que a su vez provoca una espera en las
siguientes unidades en forma de cascada. A esto se le llama PIPELINE STALL
o PIPELINE FLUSH. Esto se genera en cada JMP, por eso cuanto menos se
salte mejor ;)
EL 486 también incluye una memoria de cach‚ interna de 4Ks que acelera
los accesos a memoria, y el BURST READ que acelera la lectura para
actualización del caché interno.
EL PENTIUM
Además de ser PIPELINE como el 486 es SUPERSCALAR (más de un PIPELINE)
y BRANCH PREDITING (anota la posición y el resultado de los últimos saltos
para intentar 'adivinar' qué instrucción cargar después del salto.
Si acierta el PIPELINE continúa sin parar. Esto hace que se aceleren
los bucles anidados.
Como el PENTIUM tiene 2 PIPELINES puede ejecutar 2 instrucciones en
un ciclo y tiene dos cachés de 8Ks independientes.
ACELERAR EL CODIGO EN 486/586
ADDRESS GENERATION INTERLOCK (AGI)
Cuando un registro que se usa como base o índice es adem s el destino
de una instrucción anterior se genera una parada (AGI).
Por ejemplo:
add edx, 4
mov esi, [edx]
En el 486 esto sólo ocurre con instrucciones adyacentes. En el PENTIUM
el AGI ocurre incluso con instrucciones separadas 3 puestos:
add esi, 4 ;Esto lo realiza el PIPE1
pop ebx
dec ebx
mov edx, [esi] ;El PIPE2 que hace ‚sta espera a que termine el PIPE1
El AGI también se provoca cada vez que se lee o escribe implícitamente
en los registros:
mov esp,ebp
pop ebp.
es ejecutado como:
mov esp,ebp
[agi] ; pop usa el ESP como puntero
pop ebp
También se pierde un ciclo de reloj en el 486 (en el Pentium no)
cuando metemos un valor inmediato y un offset indexado, porque sólo
puede leer 1 valor inmediato de una vez, y un valor con un offset
son 2 valores inmediatos.
mov spawn, 248
mov dword ptr [esp+4], 1
El 486 pierde 1 ciclo (el 586 no) cuando modifica la parte baja de
un registro DWORD que es usado inmediatamente después:
mov al,0
mov [ebp], eax
Si se pone el ALIGN del código a 16 en el 486 y a 32 en el 586 se
aceleran todas la operaciones, porque coincidir el comienzo de la
página de caché con el de tu rutina, y mientras se esté ejecutando
ésta dará tiempo a que se cargue la siguiente página. Esto acelera
sobre todo el 486.
En el caso del ALIGN con DATA, el ponerlo mal provocar un retraso
de 3 ciclos, tanto en el 486 como en el 586.
Las reglas a seguir para que vaya lo más rápido posible son:
- Para DWORD data -> ALIGN= 4 bytes
- Para WORD data -> ALIGN= 2 bytes en 486 y 4 bytes en 586.
- Para 64bits data-> ALIGN= 8 bytes
Acelerar las operaciones:
- Usar EAX y DS todo lo posible.
- Referirse a la pila con el ESP: es más rápido que push/pop y deja
EBP libre para otros usos.
- Evitar el uso de instrucciones complejas como LEAVE, ENTER, LOOP...
- Usar operaciones de 8 bits si no se necesita más.
- Intentar no usar LEA -> puede ser usada para incluir varias
instrucciones en una: LEA ECX, [EAX+EBX*4+ARRAY_NAME]. El uso
de estos registros hace parase si hay alg£n otro paso que los
necesita. Será más rápido si se separa cada una de las operaciones
que lleva dentro:
LEA A,[B+C*INDEX+DISPLACEMENT] es m s lento que...
MOV X,C
MOV A,B
MUL X,INDEX
ADD A,DISPLACEMENT
ADD A,X
- Evitar el uso de MOVZX fuera de un LOOP, y usarlo dentro.
- UsarTEST para comparar con 0.
- Evitar las divisiones enteras. Normalmente un IDIV va precedido de
un CDQ para dividir EDX:EAX. Es m s rápido si se copia EAX a EDX y
se hace un SHR EDX,31 que hacer el CDQ. Los dos métodos duran los
mismos ciclos, pero el segundo son dos instrucciones y el 586 permite
ejecutar dos instrucciones a la vez. Si se sabe que el resultado va
a ser positivo hacer un XOR EDX,EDX.
- Evitar multiplicaciones enteras por una constante sustituyéndola
por una combinación de SHL, SUB, ADD y LEA. Como regla es mejor no
usar el imul en el 486 cuando la constante sea de 6 o menos bits,
o de 8 o menos bits en el 586.
El uso de LEA es mucho m s rápido:
LEA ECX,[EDX*2] ; multiplica EDX * 2 y lo guarda en ECX
LEA ECX,[EDX+EDX*2] ;
LEA ECX,[ECX+ECX*8] ; ECX <-- EDX*27
Y mejor si pones 3 instrucciones entre los dos LEAs para evitar un
AGI en el 586
- Poner a 0 los registros con XOR.
- Evitar ENTER, en su lugar usar:
PUSH EBP
mov EBP, ESP
sub ESP, BYTE_COUNT
- Usar JMP SHORT en lugar de JMP siempre que sea posible.
- Evitar el Task Switching
- Evitar cargar registros de segmento y usar punteros lejanos.
OPTIMIZACION ESPECIFICA PARA EL 586
Para que el 586 consiga ejecutar 2 instrucciones en un ciclo de reloj
se necesita que esas instrucciones sean 'simples', que son éstas:
(ALU quiere decir cualquier operación ALU: ADD, SUB, etc)
1. Mov reg, reg/mem/immed
2. mov mem, reg/imm
3. ALU reg, reg/mem/immed
4. ALU mem, reg/immed
5. inc reg/mem
6. dec reg/mem
7. push reg/mem
8. pop reg
9. nop
10. lea reg/mem
11. Jmp/ Call / Jcc near
Hay muchos trucos para acelerar la ejecución del código, como poner
instrucciones de cadenas con REP al principio de un LOOP, o sacar
el máximo rendimiento del caché, pero las iré poniendo más adelante.
Os dejo aquí una fuentes optimizadas al máximo en tamaño para 386,
es la típica firma ;)
code segment public 'CODE'
assume CS:code,DS:code;,ES:code,SS:code
org 100h
begin:
.386
mov al,13h
int 10h
;Comienzo de paleta 60,40,40, color 230
mov cl,255
mov bx,3f27h ;60,40
@pal0:
mov dx,03C8H
mov al,cl
cli
out dx,al
inc dx
mov al,bh
out dx,al
mov al,bl
out dx,al
out dx,al
sti
dec cx
cmp cl,69
ja short @pal0
mov bl,bh
sar bl,1
dec bh
jns short @pal0
;Fin de paleta
mov ah,9
mov dx,OFFSET Nombre
int 21h
@inicio:
mov bh,9h;950h
inc dx
@l1:
mov ax,0a000h
mov es,ax
mov ds,ax
mov di,bx
scasb
je short @term
imul di,bx,5
sub di,dx
mov ax,10
sub al,bh
sal al,3
mov ds:[di+42245],ax ;42245
mov ds:[di+42885],ax ;42885
add di,dx
mov cx,7
add di,22410
@dib:
imul si,si,371
ror si,3
mov ax,si
and ax,0010000000100000b ;;rand de 32
add ax,4b4bh ;75
stosw
stosw
stosb
sub di,325
loop short @dib
@term:
dec bx
jnz short @l1
;*************************************** call fire
mov bh,0fh
@loop0:
mov ax,ds:[bx]
add al,ah
xor ah,ah
sal ax,4
mov cl,34
div cl
cmp al,7
jb short @skip
mov ah,al
mov ds:[bx-1],ax
mov ds:[bx-321],ax
mov ds:[bx-641],ax
@skip:
inc bx
inc bx
cmp bh,130
jbe short @loop0
;********************************** End fire
in ax,60h ;Leo el teclado
cmp al,1
jnz @inicio
mov ax,03h
int 10h
ret
Nombre db 'BitSpawn$'
code ENDS
END begin
Fichero Demo compilado. (ZIP)
OPTIMIZACION By Bitspawn * Roberto Domínguez
SubNet 93:341/736.0 * FidoNet 2:341/136.0 * @70:343/14*28 Virtual Mail
©eLectronic @ magaZine - 1997.
32 BITS VISUAL CREATIONS CROUP