1 .
¿Merece la pena usar el copro?
2 .
Estructura interna
3 .
Tipos de datos
4 .
Un poco de materia...
5 .
Instrucciones
6 .
Hints, o cómo acelerar
7 .
Ejemplo Práctico: Una rutina de proyección 3D a 2D
8 .
Cuidado con...
9 .
¿Que hay de verdad sobre las transferencias con copro?
10 .
Más Hints
11 .
Palabras de control y estado de coprocesador y registros de flag
12 .
Denormals
13 .
Para terminar.... de empezar.... :))
14 .
Agradecimientos y Saludos
Bueno, este documento es una breve iniciación al funcionamiento del coprocesador
matemático en los sistemas PC y, más concretamente, a la arquitectura
de los procesadores PENTIUM, puesto que hoy por hoy es lo más utilizado.
No es mi intencion dar una explicacion completa de su arquitectura interior,
simplemente trato de arrojar un poco de luz donde no la hay, porque
cuando decidí meterme con el coprocesador matemático, no encontre apenas documentación
y la poca (por no decir nula) estaba en inglés.
¿Merece la pena usar el copro?
Esta pregunta es algo ambigua, porque depende mucho de la aplicacion en sí.
Si lo que queremos realizar son calculos intensivos en 3D la respuesta es sí.
Esto depende mucho por la incapacidad que tiene el coprocesador de intercambiar
datos con los registros de proposito general, lo que obliga a costosas descargas
en memoria con conversion a entero y que provoca que, por ejemplo, un calculo en
el que tengamos que utilizar directamente el resultado de la operacion como un
puntero a una zona de memoria, o como valor para introducir en una zona de
memoria, sea algo lento.
Si por ejemplo lo que queremos es realizar un calculo sobre unos datos dados
(por ejemplo una multiplicacion de matrices) si que merece la pena su uso.
Estructura interna
El coprocesador trabaja internamente sólo en formato real, por lo que
cualquier carga en los registros de coprocesador provocará que dicho valor sea
convertido a coma flotante.
Sus registros están estructurados en forma de pila y se accede a ellos por
el numero de entrada que ocupan en la pila.
Los registros son R(0) hasta
R(7), en total ocho registros de 80bits,
como han de ser manejados en formato de pila, el coprocesador tiene un puntero de
control de pila llamado St, Toda interacción que
tengamos que hacer con los registros del coprocesador se realiza a traves del
puntero de pila St, donde el último valor
introducido es St o
St(0) y si hubieramos rellenado todos los
registros el ultimo seria St(7)... ¿ok? Por
ejemplo:
- Cargar en copro dato (1345)
- ahora St(0)=1345
- Cargar en copro dato (5431)
- ahora St(0)=5431 y St(1)=1345
Tipos de datos
El coprocesador puede obtener y escribir datos en memoria de los siguientes
tipos.
| Entero |
Words(16bits),Dword(32 bits),Qwords(64 bits) |
| Real |
Words(16 bits),Dword(32 bits),Qwords(64 bits ),Twords(80 bits) |
Un poco de materia...
Bien, ya hemos visto como ordena el coprocesador sus registros, ahora vamos a
ver alguna operacion sencilla.
Todos los ejemplos que aquí escriba estarán enteramente en ensamblador,
puesto que si lo que queremos es velocidad de poco nos sirve la math387.lib
del C.
Sumar un entero yun real
.386
.Model Flat
.Stack 800h
.DATA
Numero1 dd 25 ; Numero en formato entero
Numero2 dd 1.25 ; Numero en formato real
; (¡Ojo! este numero solo puede ser
; accedido correctamente por el copro
; a no ser que nos hagamos una rutina
; conversora de formato de FPU a entero
; cosa que no merece la pena, porque
; es mas facil cargarlo y convertirlo
; con el copro, y mas rapido)
Resul dd ? ; Variable para el resultado
Desca dd ? ; Variable para correccion de pila
.CODE
Start:
fild Dword Ptr ds:[Numero1] ;Cargar en el copro la
;variable Numero1 indicando
;que es un entero
fld Dword Ptr ds:[Numero2] ;Idem pero es un real
fadd St(0),St(1) ;Sumarlos
;St(0)=St(0)+St(1)
fstp Dword Pt ds:[Resul] ;Descargar el resultado.
fstp Dword Ptr ds:[Desca] ;Descartar el otro valor.
mov eax,4c00h
int 21h
End Start
Como hemos visto al funcionar a base de pila siempre tenemos que dejarla tal
y como se encontraba al iniciarse nuestra rutina, de ahi la operacion de
popeo con la variable Desca, aunque esto puede
realizarse más rapido, siempre teniendo en cuenta varias cosas.
En vez de usar fadd podemos usar
faddp que automaticamente suma
St(0) y St(1) y
elimina automáticamente el operando sobrante de la pila quedando en
St(0) el resultado listo para ser popeado.
¡Ojo, las instrucciones de cálculo con la particula "p" ej:
faddp o fdivp,
sólo trabajan con las posiciones de pila St(0) y
St(1)!
También podríamos haber usado en vez del fstp a
Descal un ffree St(1),
esta instrucción automáticamente libera ese componente de la pila, pero sólo
es efectiva para dejar la pila tal y como estaba cuando el operando a liberar
ocupa la última posicion en la pila (esto es, el primero introducido).
Start:
fild Dword Ptr ds:[Numero1] ;Cargar en el copro la
;variable Numero1 indicando
;que es un entero
fld Dword Ptr ds:[Numero2] ;Idem pero es un real
faddp ;Sumarlos y popear el
;operando sobrante.
;St(0)=St(0)+St(1)
;pop st(1) > nul
;Solo opera con estos
;punteros de pila!!
fstp Dword Ptr ds:[Resul] ;Descargar el resultado.
mov eax,4c00h
int 21h
End Start
o bien...
.CODE
Start:
fild Dword Ptr ds:[Numero1] ;Cargar en el copro
;la variable Numero1
;indicandole que es un entero
fld Dword Ptr ds:[Numero2] ;Idem pero es un real
fadd St(0),St(1) ;Sumarlos
;St(0)=St(0)+St(1)
fstp Dword Ptr ds:[Resul] ;Descargar el resultado.
;pop St(0) > Resul
ffree St(0) ;Descartar el otro valor.
;Free St(0) (stack cleared)
mov eax,4c00h
int 21h
End Start
Como habreis podido observar liberamos con ffree St(0) esto es por lo
anteriormente comentado. Al liberar el resultado de la pila St(1) pasa a ser
St(0), St(2)-St(1), etc. Debido a su estructura en forma de pila.
Instrucciones
Estas son las instrucciones mas comunes, he omitido algunas, por tratarse
por ejemplo de aritmetica BCD (cosa poco comun en el mundo del tiempo-real)
pero para los interesados, Intel tiene a su disposicion unos archivos en
formato de Acrobat Reader con la lista y la organizacion interna de la FPU.
Los ciclos de reloj de las instrucciones, son los declarados por Intel para
el Pentium basico, esto puede verse alterado en las CPU's MMX, PRO y Pentium
II. No voy a comentar las instrucciones porque en este momento carezco del
tiempo necesario para realizar una documentación sobre las instrucciones
de manera seria, pero de todas formas solo hay que ser un poco despierto
para con solo los nombres hacerse una idea basica del funcionamiento de cada
instrucción. Cualquier instruccion de cálculo (
fadd, fsub, etc)
toman por defecto si no se le especifica otros operandos
St(0) y St(1).
Leyenda: rm=Modo Real, vm=Modo Virtual, pm=Modo Protegido
| Instrucción |
Ejemplo |
Ciclos de reloj |
| FABS |
fabs |
1 |
| FADD [reg,reg] |
fadd |
3, 1 |
| FADD memreal |
fadd shortreal |
3, 1 |
| FADDP reg,ST |
faddp st(6),st |
3, 1 |
| FIADD memint |
fiadd int16 |
7, 4 |
| FCHS |
fchs |
1 |
| FCLEX |
fclex |
9+ |
| FNCLEX |
fnclex |
9 |
| FCOM |
fcom |
4, 1 |
| FCOMP |
fcomp |
4, 1 |
| FCOMPP |
fcompp |
4, 1 |
| FICOM memint |
ficom double |
8, 4 |
| FICOMP memint |
ficomp darray[di] |
8, 4 |
| FCOS |
fcos |
18-124 |
| FDECSTP |
fdecstp |
1 |
| FDIV [reg,reg] |
fdiv st(5),st |
39 |
| FDIV memreal |
fdiv longreal |
39 |
| FDIVP reg,ST |
fdivp st(6),st |
39 |
| FIDIV memint |
fidiv warray[di] |
42 |
| FDIVR [reg,reg] |
fdivr st(5),st |
39 |
| FDIVR memreal |
fdivr longreal |
39 |
| FDIVRP reg,ST |
fdivrp st(6),st |
39 |
| FIDIVR memint |
fidivr warray[di] |
42 |
| FFREE ST(i) |
ffree st(3) |
1 |
| FILD memint |
fild quads[si] |
3, 1 |
| FINCSTP |
fincstp |
1 |
| FINIT |
finit |
16 |
| FNINIT |
fninit |
12 |
| FIST memint |
fist doubles[8] |
6 |
| FISTP memint |
fistp longint |
6 |
| FLD reg |
fld st(3) |
1 |
| FLD mem32real |
fld longreal |
1 |
| FLD mem64real |
|
1 |
| FLD mem80real |
|
3 |
| FLD1 |
fld1 |
2 |
| FLDZ |
fldz |
2 |
| FLDPI |
fldpi |
5,3 |
| FLDL2E |
fldl2e |
5, 3 |
| FLDL2T |
fldl2t |
5, 2 |
| FLDLG2 |
fldlg2 |
5, 3 |
| FLDLN2 |
fldln2 |
5, 3 |
| FLDCW mem16 |
fldcw ctrlword |
7 |
| FLDENV mem |
fldenv [bp+10] |
37, 16-bit pm=32, 32-bit pm=33 |
| FMUL [reg,reg] |
fmul st(5),st |
3, 1 |
| FMULP reg,ST |
fmulp st(6),st |
3, 1 |
| FIMUL memint |
fimul warray[di] |
7, 4 |
| FNOP |
fnop |
1 |
| FPATAN |
fpatan |
17-173 |
| FPREM |
fprem |
16-64 |
| FPREM1 |
fprem1 |
20-70 |
| FPTAN |
fptan |
17-173 |
| FRNDINT |
frndint |
9-20 |
| FRSTOR mem |
frstor [bp-94] |
16-bit rm or vm=75; 32-bit rm or vm=95; pm=70 |
| FSAVE mem |
fsave [bp-94] |
16-bit rm or vm=127+; 32-bit rm or vm=151+; pm=124+ |
| FNSAVE mem |
fnsave [bp-94] |
16-bit rm or vm=127; 32-bit rm or vm=151; pm=124 |
| FSCALE |
fscale |
20-31 |
| FSIN |
fsin |
16-126 |
| FSINCOS |
fsincos |
17-137 |
| FSQRT |
fsqrt |
70 |
| FST reg |
fst st |
1 |
| FST memreal |
fst longs[bx] |
2 |
| FSTP reg |
fstp st(3) |
1 |
| FSTP mem32real |
fstp longreal |
2 |
| FSTP mem64real |
|
2 |
| FSTP mem80real |
|
3 |
| FSTCW mem16 |
fstcw ctrlword |
2+ |
| FNSTCW mem16 |
fnstcw ctrlword |
2 |
| FSTENV mem |
fstenv [bp-14] |
16-bit rm or vm=50+; 32-bit rm or vm=48+; 16-bit pm=49+; 32-bit pm=50+ |
| FNSTENV mem |
fnstenv [bp-14] |
16-bit rm or vm=50; 32-bit rm or vm=48; 16-bit pm=49; 32-bit pm=50 |
| FSTSW mem16 |
fstsw statword |
2+ |
| FSTSW AX |
fstsw ax |
2+ |
| FNSTSW mem16 |
fnstsw statword |
2 |
| FNSTSW AX |
fnstsw ax |
2 |
| FSUB [reg,reg] |
fsub st,st(2) |
3, 1 |
| FSUB memreal |
fsub longreal |
3, 1 |
| FSUBP reg,ST |
fsubp st(6),st |
3, 1 |
| FISUB memint |
fisub double |
7, 4 |
| FSUBR [reg,reg] |
fsubr st,st(2) |
3, 1 |
| FSUBR memreal |
fsubr longreal |
3, 1 |
| FSUBRP reg,ST |
fsubrp st(6),st |
3, 1 |
| FISUBR memint |
fisubr double |
7, 4 |
| FTST |
ftst |
4, 1 |
| FUCOM [reg] |
fucom st(2) |
4, 1 |
| FUCOMP [reg] |
fucomp st(7) |
4, 1 |
| FUCOMPP |
fucompp |
4, 1 |
| FWAIT |
fwait |
1-3 |
| FXAM |
fxam |
21 |
| FXCH [reg] |
fxchg st(3) |
1 |
| FXTRACT |
fxtract |
13 |
| FYL2X |
fyl2x |
22-111 |
| FYL2XP1 |
fyl2xp1 |
22-103 |
Hints, o cómo acelerar
Lo primero es que, como ya hemos visto con los ciclos de las instrucciones,
el volcado y carga de memoria es lento, con lo cual, la primera regla, es realizar
las menores cargas y volcados posibles, aprovechando al maximo las posiciones
de la pila para cuantas más operaciones mejor.
Por ejemplo, hemos de multiplicar un array por 5...
Multiplicar un array por 5
.DATA
Array dd 200 dup(?) ;Array con datos.. :D
Value dd 5
.CODE
fild Dword Ptr ds:[Value]
mov ecx,200 ;200 datos a multiplicar
mov edi,offset Array
Bucle:
fild Dword Ptr ds:[edi] ;fild suponiendo que son
;enteros..
fmul ;Multiplica St(0),St(1)
fistp Dword Ptr ds:[edi] ;descargamos el operando
add edi,4
dec ecx
jnz Bucle
ffree St(0) ;Liberar el dato "5" :)
o bien, para ahorrarnos el ffree... :)
fild Dword Ptr ds:[Value]
mov ecx,199 ;200 datos a multiplicar
mov edi,offset Array
Bucle:
fild Dword Ptr ds:[edi] ;fild suponiendo que son
;enteros..
fmul ;Multiplica St(0),St(1)
fistp Dword Ptr ds:[edi] ;descargamos el operando
add edi,4
dec ecx
jnz Bucle
fmulp ;St(0)=St(0)*St(1)
;pop St(1) > nul
fistp Dword Ptr ds:[edi]
Esto en realidad es un ejemplo tonto, pero como podemos observar la
enseñanza del mismo es directamente aplicable a muchas rutinas matemáticas de
ámbito 3D.
Otra cosa a tener en cuenta es que, si las cargas y descargas ya de por
sí son lentas (hay que tener en cuenta cache hits en las cargas)
es más lento todavía con la inclusión de la particula "i", ya que la FPU ha de
convertir ese dato entero a real, esto provoca que siempre perdamos unos pocos
ciclos en la operación. Aun así, lo expuesto arriba ya de por sí es más rápido
que lo mismo escrito en código de procesador (osea con imuls,etc).
Esto es fácilmente solucionable, si nuestra aplicacion necesita realizar
diversos cálculos matemáticos con un array de vectores, con lo cual lo
realmente óptimo es tener almacenados esos datos en real, operar con ellos,
y en la ultima operación a realizar para su utilización (una proyección
3D a 2D, por ejemplo) aprovechar para hacer la conversión a entero.
También hay otras optimizaciones, hay algunas instrucciones pairebles,
concretamente, el Fmul con el
Fxchg, pero de esto hablaré en una sucesiva
actualizacion de documento (¡maldito tiempo!).
Ejemplo Práctico: Una rutina de proyección 3D a 2D
Los arrays son de Dwords en formato X,Y,Z con 12 bytes cada vector.
El de entrada es real, el de salida sera entero.
Proyección 3D a 2D
;In Ecx=Number of vertex
; Edi=pointer to destination
; Esi=pointer of 3D vertex
;
;Out Action Performed!
;---------------------------------------
ProyCords Proc Near
fild ds:[Ycenter] ;World Centering on screen
fild ds:[Xcenter] ;Idem.
fild ds:[Pvi] ;Focal Distance.
c3d2d:
fld ds:[esi+8] ;Get "Z"
fld1 ;Load number "1"
fdivrp ;Calc Inverse and pop
;the not valuable operand
fld Dword Ptr ds:[esi+4] ;Get "Y"
fmul st,st(2) ;Multiply Y with Focal Distance
fmul st,st(1) ;Multiply with the inverse
;of the "Z"
fadd st,st(4) ;Add Y Screen centering
fistp Dword Ptr ds:[edi+4] ;Store final value
fld Dword Ptr ds:[esi] ;Get "X"
fmul st,st(2) ;Multiply X with Focal Distance
fmul st,st(1) ;Multiply with the inverse
;of the "Z"
fadd st,st(3) ;Add X Screen centering
fistp Dword Ptr ds:[edi] ;Store final value
fstp ds:[Dummy] ;Free the Z inverse
add esi,12 ;Increment pointers
add edi,12 ;Increment pointers
dec ecx ;decrement counter
jnz c3d2d ;if not 0 go to other vertex
fistp ds:[Dummy]
fistp ds:[Dummy]
fistp ds:[Dummy] ;Free all
ret
ProyCords Endp
Como se puede observar en esta rutina, hacemos uso de los dos ejemplos de
optimizacion expuestos anteriormente. Tomar los datos, desde una base de datos
de reales, utilizar la conversión 3D a 2D para pasarlos a enteros (para poder
realizar luego el pintado) y utilizar al máximo de lo que se pueda las
posiciones de la pila para guardar datos que han de ser reutilizados.
Otra optimización es el uso de inversos, para lo cual primero calculamos
ZI=1/Z y luego para obtener
X'=X*PVI/Z realizamos la siguiente operación:
X'=(X*PVI)*ZI.
Desde luego esto es más rápido que dos divisiones pero, de todas formas,
aunque se realizara con dos divisiones de copro, el resultado sigue siendo
todavía más rápido que su misma ecuación programada con el procesador.
Además, los inversos se pueden utilizar hasta en los sitios mas
insospechados. Por ejemplo, en nuestro sistema de sonido, en el player de Sb
hay una tabla de inversos para eliminar DIV's en la rutina de mezcla.
Cuidado con...
En primer lugar, mucho cuidado con la pila, cualquier desestabilizacion de
la pila puede provocar resultados insospechados y hasta, en muchos casos, el
cuelgue total del computador.
En segundo lugar, con los compiladores. Ignoro si hay algun compilador
perfecto para el inline en asm, porque realmente yo no uso ninguno, siempre
programo en 100% ensamblador, pero he visto casos como el de un chaval que una
noche a través del IRC me comento que un bucle con MUL's y otro con FMUL's le
funcionaban a la misma velocidad. Me paso el exe, y me di cuenta que el WATCOM
C le habia generado en vez de FMUL's, FMULP's, con lo cual siempre popeaba
datos y ocasionaba una excepción de pila, lo que hacia que el codigo de copro
se ejecutara más lento. Las excepciones de pila por arriba, esto es por
St(0) son controlables, pero por abajo
(St(7)) no lo son tanto.
No sé si esto es un bug declarado del compilador, pero tambien he oido que
existen algunos problemas con la conversion de real a entero de la citada
Math387 del Watcom C, por lo cual aconsejo, que después de haber escrito la
rutina, coger un debugger y asegurarse de que el codigo generado por el
compilador es exacto.
Nosotros aqui en TLOTB trabajamos tanto con TASM como con NASM y con ninguno
de los dos hemos tenido ningun problema con las instrucciones y sintaxis de
copro, con lo cual tambien aconsejo el ensamblar las rutinas con alguno de
estos dos ensambladores y despues enlazarlas con el programa principal.
¿Que hay de verdad sobre las transferencias con copro?
Bien, esto es un tema peliagudo, y lo evaluaremos en dos casos
particulares.
- Memoria->Memoria.
En este caso se obtiene una ligera mejoria por tres razones.
La primera es porque el procesador internamente esta orientado al trabajo con
Qwords (si no, mirar la instruccion MOVQ del MMX).
La segunda es el conexionado del procesador a la memoria del sistema, cuya
transmisión optima es 64bits (mejor si disponemos de modulos de memoria DIMM).
La tercera, el alineamiento a 64bits es lo mas óptimo en una CPU Pentium.
Aquí, en transferencia, hay que tener cuidado con los NaN.
- Memoria->Bus (SVGA,etc)
En este caso depende mucho del chipset utilizado en la placa base.
Despues de pruebas en varios equipos, se llega a la conclusion de que, aunque
gastemos transferencia por copro, en el peor de los casos tarda lo mismo que
con Dwords (osea un MOVSD) y en el mejor de los casos se gana en torno a un
5% o 10%. Esto oscila mucho dependiendo de la saturacion del BUS, de la SVGA
y su velocidad para tragar datos,etc. Con lo cual, de todas formas es
aconsejable su utilizacion, o al menos, probar que tal va! :)
Más Hints
Para borrar una zona de memoria (por ejemplo una pantalla virtual) el copro
bate records.
Alineamiento... hacer la siguiente prueba...
- Tomar una direccion alineada por ejemplo a Qword
- esperar al retrazo
- colocar el color de borde a verde por ejemplo
- volcar la pantalla.
- colocar el color de borde a negro por ejemplo
- goto al principio
Apuntar lo que tarda y luego hacer lo mismo con una direccion sin alinear..
¡veréis la grandiosa diferencia! :)
Palabras de control y estado de coprocesador y registros de flag
Aqui esta la especificación de bits de las palabras de estado y de
control, Hay que tener en cuenta que esta documentación la he podido
conseguir sólo del 387, y que hay bastantes diferencias con el 287,
asi que podría ser que en los actuales cambie alguna cosa.
PALABRA DE CONTROL
| Bit |
Nombre |
Descripción |
| 15-13 |
-- |
Reservados en el 387 |
| 12 |
-- |
Reservados en el 387; en el 287 era control de infinito. |
| 11-10 |
RC |
Rounding Control:
- 00 = redondear al mas cercano; si es XXX.5 entonces ir al numero par
más cercano.
- 01 = Redondear siempre por abajo (2.1 -> 2; -2.1 -> -3)
- 10 = Redondear siempre por arriba (2.1 -> 3; -2.1 -> -2)
- 11 = Quitar decimales (2.1 -> 2; -2.1 -> -2)
|
| 9-8 |
PC |
Precision Control:
- 00 = 24 bits (single precision)
- 01 = reservado en el 387
- 10 = 53 bits (double precision)
- 11 = 64 bits (extended precision)
|
| 7-6 |
-- |
Reservados en el 387 |
| 5 |
PM |
Máscara de excepción de Precisión |
| 4 |
UM |
Máscara de excepción de Underflow |
| 3 |
OM |
Máscara de excepción de Overflow |
| 2 |
ZM |
Máscara de excepción de Zero Divide |
| 1 |
DM |
Máscara de excepción de Denormalized Operand |
| 9 |
IM |
Máscara de excepción de Invalid Operation |
PALABRA DE ESTADO
| Bit |
Nombre |
Descripción |
| 15 |
B |
Flag de Busy |
| 14 |
C3(Z) |
Parte del Condition Code. |
| 13-11 |
TOP |
Puntero de pila (Top of Stack) |
| 10 |
C2(C) |
Parte del Condition Code. |
| 9 |
C1(A) |
Parte del Condition Code. |
| 8 |
C0(S) |
Parte del Condition Code. |
| 7 |
ES |
Error Summary flag |
| 6 |
SF |
Stack Flag |
| 5 |
PE |
Excepción de Precision |
| 4 |
UE |
Excepción de Underflow |
| 3 |
OE |
Excepción de Overflow |
| 2 |
ZE |
Excepción de Zero Divide |
| 1 |
DE |
Excepción de Denormalized Operand |
| 0 |
IE |
Excepción de Invalid Operation |
Los Condition Codes son bastante complicados de interpretar, ya que su
interpretación depende de la instrucción. Baste decir que tras la
comparación (FCOM y familia) resulta lo siguiente:
| C3 |
C2 |
C0 |
Significado |
| 0 |
0 |
0 |
TOP > Operand |
| 0 |
0 |
1 |
TOP < Operand |
| 1 |
0 |
0 |
TOP = Operand |
| 1 |
1 |
1 |
Unordered |
C1 es para la comparacion el flag de Zero, aunque no sé si eso
significa que debe ser interpretado igual que el flag de Zero del
registro Flags.
Si hay un Overflow o Underflow de pila, los bits IE y SF se pondrán a
1, y el flag C1 distinguira entre overflow (1) y underflow (0).
Denormals
(Esto no está en la documentacion de intel, y puedo equivocarme en algo).
La representación en coma flotante consiste en un bit de signo, un
exponente y una mantisa, como sabéis. Para permitir exponentes
negativos, en vez de almacenar el exponente en formato de complemento
a dos, se almacena sumándole una cantidad llamada Bias, que es 127
para el Single, 1023 para el Double y 16383 para el Extended.
Los numeros Single y Double suponen un 1,... implícito antes de
empezar la mantisa, pero en el Extended no es implícito, sino que lo
necesita explicitamente.
Parece una tontería, pero en los Single y Double, ¿cómo se almacena un
cero? Porque el siguiente caso, que se corresponde a todos los bits a
cero (pongamos que de Single):
- Exponente = 0
- Mantisa = (1,)000000000000...
- Signo = 0
significaria 1,0000... * 2^-127, pero no
cero (aunque esté cerca).
La solución que se ha adoptado es considerar como caso especial el de
Exponente = 0, en cuyo ese caso en vez de haber un 1 implícito en la
mantisa hay un 0 implícito.
Asi:
- Exponente = 0
- Mantisa = (0,)0001001110001000
- Signo = 0
se corresponde a 0,0001001110001000 * 2^-127,
o lo que es igual, 1,001110001000 * 2^-131.
Ademas, de esa manera el cero puede ser representado, ya que
0,00000000000... * 2^-127 = 0.
A toda la familia de numeros con Exponente = 0, excepto el cero, se
les llama Denormals. En el libro de Knuth (Seminumerical
Algorithms) menciona la utilidad de los Denormals para
no perder tanta precisión en ciertos cálculos.
El caso de los Extended no es diferente, salvo que el 1 explicito
puede dejar de estar ahi cuando el exponente es cero (en el resto de
numeros, el 1 era obligatorio que estuviera).
Ahora una nota del cuadernillo:
"FLD single/double cuando el operando es un Denormal convierte el
numero a Extended precision y lanza la excepcion de Denormalized
Operand. Al cargar un Signaling NaN, FLD single/double lanza una
excepcion de Invalid Operand."
NaN significa Not a Number, aunque no sé qué formato tienen esos.
La tabla de excepciones dice lo siguiente:
| Excepción |
Causa |
Acción por defecto (si la excepción está enmascarada) |
| Invalid Op. |
Operación sobre un Signaling NaN, formato no soportado, operacion
indeterminada (0*infinito, 0/0, infinito-infinito, etc), o
underflow/overflow de pila |
El resultado es un Quiet NaN, un entero indefinido o un BCD indefinido. |
| Denormalized Op. |
Al menos uno de los operandos es un Denormal, o sea, tiene el menor
exponente pero una mantisa no-cero. |
Continua el proceso normal. |
| Zero Divisor |
El divisor es cero mientras el dividendo es un numero no cero y no
infinito. |
El resultado es Infinito. |
| Overflow |
El resultado es demasiado grande para caber en el formato especificado. |
El resultado es el mayor numero posible o infinito. |
| Underflow |
El resultado es no-cero pero es demasiado pequeño para caber en el
formato especificado, y si está enmascarada la excepción de Underflow, la
denormalizacion causa pérdida de precisión. |
El resultado es un denormal o cero. |
| Resultado Inexacto (Precisión) |
El resultado EXACTO no es representable en el formato especificado
(p.ej. 1/3); el resultado es redondeado de acuerdo con el modo de redondeo. |
Continua el proceso normal. |
Para terminar.... de empezar.... :))
Lo aconsejable, es realizar al principio del programa un
finit para inicializar el copro, despues
descargar la palabra de control y enmascarar las excepciones y luego volver a
cargarlas, con lo cual, reduciremos el tiempo de proceso de las
instrucciones que puedan causar una excepcion.
Espero que esto os ayude en algo, la verdad para mi es suficiente para lo
que es el manejo habitual de copro.
Agradecimientos y Saludos
Ummm.... a.....
- Cranky/TloTb.. por su apoyo linguistico, y por documentacion.
- Unreal/Pulse.. por incitarme a escribir esto.
- A los habituales en #demos,#coders y en es.comp.demos
Saludos especiales para Jcab/Iguana por la charla que tuvimos sobre los
Denormals, los NaN y las transferencias de copro.
Todas las marcas registradas son propiedad de sus respectivos dueños, etc.. :)
Si alguien quiere una mejor revisión de este documento, o simplemente
quiere consultarme algo o bien mandarme una amenaza para que deje de
escribir...
Podeis dar conmigo en
Astha@tlotb.com
o a traves de
http://www.tlotb.com.