Aac Vector
Aac Vector
Aac Vector
Procesadores vectoriales
La figura 1.1 muestra la arquitectura tı́pica de una máquina vectorial con registros.
Los registros se utilizan para almacenar los operandos. Los cauces vectoriales fun-
cionales cogen los operandos, y dejan los resultados, en los registros vectoriales. Cada
registro vectorial está equipado con un contador de componente que lleva el seguimiento
del componente de los registros en ciclos sucesivos del cauce.
Procesador escalar
Cauces
funcionales
escalares
Vectoriales
Almacenamiento Computador
masivo anfitrión Cauce func. vectorial
E/S Usuario
Memoria principal
Carga/almacenamiento
vectorial
suma/resta FP
multiplicación FP
división FP
Registros
vectoriales Entero
Lógico
Registros
escalares
Figura 1.2: Estructura básica de una arquitectura vectorial con registros, DLXV.
f2 : Vj × Vk → Vi
Algunos ejemplos son: V1 = sin(V2 ) o V3 = V1 + V2 .
2. Vector-escalar: Son instrucciones en cuyos operandos interviene algún escalar y
el resultado es un vector. Si s es un escalar son las instrucciones que siguen el
siguiente tipo de función:
f3 : d × Vi → Vj
Un ejemplo es el producto escalar, que el resultado es un vector tras multiplicar el
vector origen por el escalar elemento a elemento.
3. Vector-memoria: Suponiendo que M es el comienzo de un vector en memoria,
se tienen las instrucciones de carga y almacenamiento de un vector:
f4 : M → V Carga del vector
f5 : V → M Almacenamiento del vector
4. Reducción de vectores: Son instrucciones cuyos operandos son vectores y el
resultado es un escalar, por eso se llaman de reducción. Los tipos de funciones que
describen estas instrucciones son los siguientes:
f6 : Vi → sj
f7 : Vi × Vj → sk
El máximo,Pla suma, la media, etc., son ejemplos de f6 , mientras que el producto
punto (s = ni=1 ai × bi ) es un ejemplo de f7 .
5. Reunir y Esparcir: Estas funciones sirven para almacenar/cargar vectores dis-
persos en memoria. Se necesitan dos vectores para reunir o esparcir el vector de/a
la memoria. Estas son las funciones para reunir y esparcir:
f8 : M → V1 × V0 Reunir
f9 : V 1 × V 0 → M Esparcir
Existe un bucle tı́pico para evaluar sistemas vectoriales y multiprocesadores que consiste
en realizar la operación:
Y =a·X +Y
donde X e Y son vectores que residen inicialmente en memoria, mientras que a es
un escalar. A este bucle, que es bastante conocido, se le llama SAXPY o DAXPY
dependiendo de si la operación se realiza en simple o doble precisión. A esta operación
nos referiremos a la hora de hacer cálculos de rendimiento y poner ejemplos. Estos bucles
forman el bucle interno del benchmark Linpack. (SAXPY viene de single-precision a×X
plus Y; DAXPY viene de double-precision a×X plus Y.) Linpack es un conjunto de
rutinas de álgebra lineal, y rutinas para realizar el método de eliminación de Gauss.
Para los ejemplos que siguen vamos a suponer que el número de elementos, o lon-
gitud, de un registro vectorial coincide con la longitud de la operación vectorial en la
que estamos interesados. Más adelante se estudiará el caso en que esto no sea ası́.
Resulta interesante, para las explicaciones que siguen, dar los programas en ensam-
blador para realizar el cálculo del bucle DAXPY. El siguiente programa serı́a el código
escalar utilizando el juego de instrucciones DLX:
LD F0,a
ADDI R4,Rx,#512 ; última dirección a cargar
loop:
LD F2,0(Rx) ; carga X(i) en un registro
MULTD F2,F0,F2 ; a.X(i)
LD F4,0(Ry) ; carga Y(i) en un registro
ADDD F4,F2,F4 ; a.X(i)+Y(i)
SD 0(Ry),F4 ; almacena resultado en Y(i)
ADDI Rx,Rx,#8 ; incrementa el ı́ndice de X
ADDI Ry,Ry,#8 ; incrementa el ı́ndice de Y
SUB R20,R4,Rx ; calcula lı́mite
BNZ R20,loop ; comprueba si fin.
De los códigos anteriores se desprenden dos cosas. Por un lado la máquina vectorial
reduce considerablemente el número de instrucciones a ejecutar, ya que se requieren
sólo 6 frente a casi las 600 del bucle escalar. Por otro lado, en la ejecución escalar, debe
bloquearse la suma, ya que comparte datos con la multiplicación previa; en la ejecución
vectorial, tanto la multiplicación como la suma son independientes y, por tanto, no se
bloquea el cauce durante la ejecución de cada instrucción sino entre una instrucción y la
otra, es decir, una sola vez. Estos bloqueos se pueden eliminar utilizando segmentación
software o desarrollando el bucle, sin embargo, el ancho de banda de las instrucciones
será mucho más alto sin posibilidad de reducirlo.
Figura 1.6: Tiempos de arranque y del primer y último resultados para los convoys 1-4.
Por ejemplo, si una operación necesita 10 ciclos de reloj para completarse, entonces
hace falta un cauce con una profundidad de 10 para que se pueda inicializar una ins-
trucción por cada ciclo de reloj. Las profundidades de las unidades funcionales varı́an
ampliamente (no es raro ver cauces de profundidad 20) aunque lo normal es que tengan
profundidades entre 4 y 8 ciclos.
manera equivalentes, ya que ambas determinan las tasas de inicialización de las ope-
raciones utilizando estas unidades. El procesador no puede acceder a un banco de
memoria más deprisa que en un ciclo de reloj. Para los sistemas de memoria que sopor-
tan múltiples accesos vectoriales simultáneos o que permiten accesos no secuenciales en
la carga o almacenamiento de vectores, el número de bancos de memoria deberı́a ser
más grande que el mı́nimo, de otra manera existirı́an conflictos en los bancos.
El entrelazado de orden alto (figura 1.7(b)) utiliza los a bits de orden alto como
selector de módulo, y los b bits de orden bajo como la dirección de la palabra dentro
de cada módulo. Localizaciones contiguas en la memoria están asignadas por tanto a
un mismo módulo de memoria. En un ciclo de memoria, sólo se accede a una palabra
del módulo. Por lo tanto, el entrelazado de orden alto no permite el acceso en bloque a
posiciones contiguas de memoria. Este esquema viene muy bien para tolerancia a fallos.
Por otro lado, el entrelazado de orden bajo soporta el acceso de bloque de forma
segmentada. A no ser que se diga otra cosa, se supondrá para el resto del capı́tulo que
la memoria es del tipo entrelazado de orden bajo.
Buffer de las
direcciones de
Decodificador BDM BDM BDM módulo
Direcciones
0 M0 1 M1 m-1 M m-1
Dirección a m m+1 2m-1
de memoria
Palabra Módulo
m(w-1) m(w-1)+1 mw-1
b
Buffer de
BDP dirección BDM BDM BDM Buffer de
de palabra datos de memoria
Bus de datos
Buffer de las
direcciones de
Decodificador BDM BDM BDM módulo
Direcciones
0 M0 w M1 (m-1)w M m-1
a Dirección 1 w+1 (m-1)w+1
de memoria
Módulo Palabra
w-1 2w-1 mw-1
b
Buffer de
dirección BDP BDM BDM BDM Buffer de
de palabra datos de memoria
Bus de datos
Respuesta Con seis ciclos por acceso, necesitamos al menos seis bancos de memoria,
pero como queremos que el número de bancos sea potencia de dos, elegiremos ocho
bancos. La figura 1.1 muestra las direcciones a las que se accede en cada banco en cada
periodo de tiempo.
Beginning Bank
at clock no. 0 1 2 3 4 5 6 7
0 192 136 144 152 160 168 176 184
6 256 200 208 216 224 232 240 248
14 320 264 272 280 288 296 304 312
22 384 328 336 344 352 360 368 376
Tabla 1.1: Direcciones de memoria (en bytes) y momento en el cual comienza el acceso
a cada banco.
Time
0 6 14 22 62 70
Figura 1.8: Tiempo de acceso para las primeras 64 palabras de doble precisión en una
lectura.
θ
τ= (1.2)
m
donde m es el grado de entrelazado. La temporización del acceso segmentado de 8 pala-
bras contiguas en memoria se muestra en la figura 1.9. A este tipo de acceso concurrente
a palabras contiguas se le llama acceso C a memoria. El ciclo mayor θ es el tiempo
total necesario para completar el acceso a una palabra simple de un módulo. El ciclo
W7
W6
W5 θ Ciclo mayor
W4
W3 τ=θ/m Ciclo menor
W2
W1 m Grado de entrelazado
W0
θ τ Tiempo
2θ
Módulo
m-1
(n-a) bits
de orden alto
a bits de orden bajo
Sin embargo, si la fase de acceso del último acceso, se superpone con la fase de
búsqueda del acceso actual, entonces son m palabras las que pueden ser accedidas en
un único ciclo de memoria.
P(0) MC(0)
Interconexión M(0,0) M(0,1) M(0,m-1)
P(1) MC(1)
Barra
Cruzada M(1,0) M(1,1) M(1,m-1)
P(n-1) MC(n-1)
M(n-1,0) M(n-1,1) M(n-1,m-1)
Esta raı́z cuadrada da una estimación pesimista del aumento de las prestaciones de
la memoria. Si por ejemplo se ponen 16 módulos en la memoria entrelazada, sólo se
obtiene un aumento de 4 veces el ancho de banda. Este resultado lejano a lo esperado
viene de que en la memoria principal de los multiprocesadores los accesos entrelazados
se mezclan con los accesos simples o con los accesos de bloque de longitudes dispares.
Para los procesadores vectoriales esta estimación no es realista, ya que las tran-
sacciones con la memoria suelen ser casi siempre vectoriales y, por tanto, pueden ser
fácilmente entrelazadas.
En 1992 Cragon estimó el tiempo de acceso a una memoria entrelazada vectorial de
la siguiente manera: Primero se supone que los n elementos de un vector se encuen-
tran consecutivos en una memoria de m módulos. A continuación, y con ayuda de la
figura 1.9, no es difı́cil inferir que el tiempo que tarda en accederse a un vector de n
elementos es la suma de lo que tarda el primer elemento (θ), que tendrá que recorrer
todo el cauce, y lo que tardan los (n − 1) elementos restantes (θ(n − 1)/m) que estarán
completamente encauzados. El tiempo que tarda un elemento (t1 ) se obtiene entonces
dividiendo lo que tarda el vector completo entre n:
θ(n − 1) µ ¶ µ ¶
θ+ θ θ(n − 1) θ m n−1 θ m−1
t1 = m = + = + = 1+
n n nm m n n m n
Tolerancia a fallos
La división de la memoria en bancos puede tener dos objetivos: por un lado permite un
acceso concurrente lo que disminuye el acceso a la memoria (memoria entrelazada), por
otro lado se pueden configurar los módulos de manera que el sistema de memoria pueda
seguir funcionando en el caso de que algún módulo deje de funcionar. Si los módulos
forman una memoria entrelazada el tiempo de acceso será menor pero el sistema no
será tolerante a fallos, ya que al perder un módulo se pierden palabras en posiciones
salteadas en toda la memoria, con lo que resulta difı́cil seguir trabajando. Si por el
contrario los bancos se han elegido por bloques de memoria (entrelazado de orden alto)
en vez de palabras sueltas, en el caso en que falle un bloque lo programas podrán seguir
trabajando con los bancos restantes aislándose ese bloque de memoria erróneo del resto.
En muchas ocasiones interesa tener ambas caracterı́sticas a un tiempo, es decir, por
un lado interesa tener memoria entrelazada de orden bajo para acelerar el acceso a la
memoria, pero por otro interesa también una memoria entrelazada de orden alto para
tener la memoria dividida en bloques y poder seguir trabajando en caso de fallo de un
módulo o banco. Para estos casos en que se requiere alto rendimiento y tolerancia a
fallos se puede diseñar una memoria mixta que contenga módulos de acceso entrelazado,
y bancos para tolerancia a fallos.
La figura 1.12 muestra dos alternativas que combinan el entrelazado de orden alto
con el de orden bajo. Ambas alternativas ofrecen una mejora del rendimiento de la me-
moria junto con la posibilidad de tolerancia a fallos. En el primer ejemplo (figura 1.12a)
se muestra una memoria de cuatro módulos de orden bajo y dos bancos de memoria.
En el segundo ejemplo (figura 1.12b) se cuenta con el mismo número de módulos pero
dispuestos de manera que hay un entrelazado de dos módulos y cuatro bancos de me-
moria. El primer ejemplo presenta un mayor entrelazado por lo que tendrá un mayor
rendimiento que el segundo, pero también presenta menos bancos por lo que en caso
de fallo se pierde una mayor cantidad de memoria, aparte de que el daño que se puede
causar al sistema es mayor.
do 10 i=1,n
10 Y(i)=a*X(i)+Y(i)
low=1
VL=(n mod MVL) /* Para encontrar el pedazo aparte */
do 1 j=0,(n/MVL) /* Bucle externo */
do 10 i=low,low+VL-1 /* Ejecuta VL veces */
Y(i)=a*X(i)+Y(i) /* Operación principal */
10 continue
low=low+VL /* Comienzo del vector siguiente */
VL=MVL /* Pone la longitud al máximo */
1 continue
En este bucle primero se calcula la parte que sobra del vector (que se calcula con el
modulo de n y MVL) y luego ejecuta ya las veces que sea necesario con una longitud de
vector máxima. O sea, el primer vector tiene una longitud de (n mod MVL) y el resto
tiene una longitud de MVL tal y como se muestra en la figura 1.13. Normalmente los
compiladores hacen estas cosas de forma automática.
Junto con la sobrecarga por el tiempo de arranque, hay que considerar la sobrecarga
por la introducción del bucle del seccionamiento. Esta sobrecarga por seccionamiento,
64 elementos. La dirección del byte de comienzo del segmento siguiente de cada vector
es ocho veces la longitud del vector. Como la longitud del vector es u ocho o 64, se
incrementa el registro de dirección por 8 × 8 = 64 después del primer segmento, y por
8 × 64 = 512 para el resto. El número total de bytes en el vector es 8 × 200 = 1600, y se
comprueba que ha terminado comparando la dirección del segmento vectorial siguiente
con la dirección inicial más 1600. A continuación se da el código:
Las tres instrucciones vectoriales del bucle dependen unas de otras y deben ir en
tres convoyes separados, por lo tanto Tcampanada = 3. El tiempo del bucle ya habı́amos
dicho que ronda los 15 ciclos. El valor del tiempo de arranque será la suma de tres
cosas:
• El tiempo de arranque de la instrucción de carga, que supondremos 12 ciclos.
• El tiempo de arranque de la multiplicación, 7 ciclos.
• El tiempo de arranque del almacenamiento, otros 12 ciclos.
Por lo tanto obtenemos un valor Tarranque = 12 + 7 + 12 = 31 ciclos de reloj. Con todo
esto, y aplicando la ecuación (1.3), se obtiene un tiempo total de proceso de T200 = 784
ciclos de reloj. Si dividimos por el número de elementos obtendremos el tiempo de
ejecución por elemento, es decir, 784/200 = 3.9 ciclos de reloj por elemento del vector.
Comparado con Tcampanada , que es el tiempo sin considerar las sobrecargas, vemos que
efectivamente la sobrecarga puede llegar a tener un valor significativamente alto.
Resumiendo las operaciones realizadas se tiene el siguiente proceso hasta llegar al
resultado final:
l n m
Tn = × (Tloop + Tarranque ) + n × Tcampanada
MV L
T200 = 4 × (15 + Tstart ) + 200 × 3
T200 = 60 + (4 × Tstart ) + 600 = 660 + (4 × Tstart )
Tstart=12+7+12=31
T200 = 660 + 4 × 31 = 784
6
Ciclos de reloj por elemento
0
20 40 60 80 100 120 140 160 180 200
Elementos en el vector
Figura 1.14: Tiempo de ejecución por elemento en función de la longitud del vector.
tienen una separación de 1, (1 palabra doble, 8 bytes), mientras que la matriz B tiene
una separación de 100 (100 palabras dobles, 800 bytes).
Una vez cargados estos elementos adyacentes en el registro vectorial, los elementos
son lógicamente contiguos. Por todo esto, y para aumentar el rendimiento de la carga
y almacenamiento de vectores con elementos separados, resulta interesante disponer de
instrucciones que tengan en cuenta la separación entre elementos contiguos de un vector.
La forma de introducir esto en el lenguaje ensamblador es mediante la incorporación
de dos instrucciones nuevas, una de carga y otra de almacenamiento, que tengan en
cuenta no sólo la dirección de comienzo del vector, como hasta ahora, sino también el
paso o la separación entre elementos. En DLXV, por ejemplo, existen las instrucciones
LVWS para carga con separación, y SVWS para almacenamiento con separación. Ası́, la
instrucción LVWS V1,(R1,R2) carga en V1 lo que hay a partir de R1 con un paso o
separación de elementos de R2, y SVWS (R1,R2),V1 guarda los elementos del vector V1
en la posición apuntada por R1 con paso R2.
Naturalmente, el que los elementos no estén separados de forma unitaria crea com-
plicaciones en la unidad de memoria. Se habı́a comprobado que una operación memoria-
registro vectorial podı́a proceder a velocidad completa si el número de bancos en me-
moria era al menos tan grande el tiempo de acceso a memoria en ciclos de reloj. Sin
embargo, para determinadas separaciones entre elementos, puede ocurrir que accesos
consecutivos se realicen al mismo banco, llegando incluso a darse el caso de que todos
los elementos del vector se encuentren en el mismo banco. A esta situación se le llama
conflicto del banco de memoria y hace que cada carga necesite un mayor tiempo de
acceso a memoria. El conflicto del banco de memoria se presenta cuando se le pide al
mismo banco que realice un acceso cuando el anterior aún no se habı́a completado. Por
consiguiente, la condición para que se produzca un conflicto del banco de memoria será:
esto nos lleva a un tiempo de acceso de 12 ciclos por elemento y un tiempo total de
12 × 64 = 768 ciclos de reloj.
MULTV V1,V2,V3
ADDV V4,V1,V5
Si se trata en este caso al vector V1 no como una entidad, sino como una serie de
elementos, resulta sencillo entender que la operación de suma pueda iniciarse unos ciclos
después de la de multiplicación, y no después de que termine, ya que los elementos que la
suma puede ir necesitando ya los ha generado la multiplicación. A esta idea, que permite
solapar dos instrucciones, se le llama encadenamiento. El encadenamiento permite que
una operación vectorial comience tan pronto como los elementos individuales de su
operando vectorial fuente estén disponibles, es decir, los resultados de la primera unidad
funcional de la cadena se adelantan a la segunda unidad funcional. Naturalmente deben
ser unidades funcionales diferentes, de lo contrario surge un conflicto temporal.
Si las unidades están completamente segmentadas, basta retrasar el comienzo de la
siguiente instrucción durante el tiempo de arranque de la primera unidad. El tiempo
total de ejecución para la secuencia anterior serı́a:
7 64 6 64
No encadenada Total=141
MULTV ADDV
7 64
MULTV
Encadenada
6 64
Total=77
ADDV
utilizando algunas de las capacidades que se han mostrado; esto es por ejemplo un factor
importante en la falta de vectorización de Spice. Se explican a continuación algunas
técnicas para poder ejecutar de forma vectorial algunas de estas estructuras.
Dado el siguiente bucle:
do 100 i=1,64
if (A(i) .ne. 0) then
A(i)=A(i)-B(i)
endif
100 continue
estamos dividiendo, y no queremos dividir por cero (para evitar la excepción) lo normal
es comprobar los elementos que sean cero y no dividir, pero en un procesador cuya
máscara sólo deshabilite el almacenamiento y no la operación, realizará la división por
cero generando la excepción que se pretendı́a evitar.
do 100 i=1,n
100 A(K(i))=A(K(i))+C(M(i))
Este código realiza la suma de los vectores dispersos A y C, usando como ı́ndices los
vectores K y M que designan los elementos de A y B que no son cero (ambas matrices
deben tener el mismo número de elementos no nulos). Otra forma común de representar
las matrices dispersas utiliza un vector de bits como máscara para indicar qué elementos
existen y otro vector para almacenar sus valores. A menudo ambas representaciones
coexisten en el mismo programa. Este tipo de matrices se encuentran en muchos códigos,
y hay muchas formas de tratar con ellas dependiendo de la estructura de datos utilizada
en el programa.
Un primer mecanismo consiste en las operaciones de dispersión y agrupamiento
utilizando vectores ı́ndices. El objetivo es moverse de una representación densa a la
dispersa normal y viceversa. La operación de agrupamiento coge el vector ı́ndice y
busca en memoria el vector cuyos elementos se encuentran en las direcciones dadas
por la suma de una dirección base y los desplazamientos dados por el vector ı́ndice.
El resultado es un vector no disperso (denso) en un registro vectorial. Una vez se
han realizado las operaciones sobre este vector denso, se pueden almacenar de nuevo
en memoria de forma expandida mediante la operación de dispersión que utilizará el
mismo vector de ı́ndices. El soporte hardware para estas operaciones se denomina
dispersar-agrupar (scatter-gather). En el ensamblador vienen dos instrucciones para
realizar este tipo de tareas. En el caso del DLXV se tiene LVI (cargar vector indexado),
SVI (almacenar vector indexado), y CVI (crear vector ı́ndice, por ejemplo CVI V1,R1
introduce en V1 los valores 0,R1,2*R1,3*R1,...,63*R1). Por ejemplo, suponer que Ra, Rc,
Rk y Rm contienen las direcciones de comienzo de los vectores de la secuencia anterior,
entonces el bucle interno de la secuencia se podrı́a codificar como:
LV Vk,Rk ; Carga K
LVI Va,(Ra+Vk) ; Carga A(K(i))
LV Vm,Rm ; Carga M
LVI Vc,(Rc+Vm) ; Carga C(M(i))
ADDV Va,Va,Vc ; Los suma
SVI (Ra+Vk),Va ; Almacena A(K(i))
Algo parecido se puede realizar mediante el uso de la máscara que se vio en las
sentencias condicionales. El registro de máscara se usa en este caso para indicar los
elementos no nulos y ası́ poder formar el vector denso a partir de un vector disperso.
La capacidad de dispersar/agrupar (scatter-gather ) está incluida en muchos de los
supercomputadores recientes. Estas operaciones rara vez alcanzan la velocidad de un
elemento por ciclo, pero son mucho más rápidas que la alternativa de utilizar un bucle
escalar. Si la propiedad de dispersión de un matriz cambia, es necesario calcular un
nuevo vector ı́ndice. Muchos procesadores proporcionan soporte para un cálculo rápido
de dicho vector. La instrucción CVI (Create Vector Index) del DLX crea un vector
ı́ndice dado un valor de salto (m), cuyos valores son 0, m, 2 × m, ..., 63 × m. Algunos
procesadores proporcionan una instrucción para crear un vector ı́ndice comprimido
cuyas entradas se corresponden con las posiciones a 1 en el registro máscara. En DLX,
definimos la instrucción CVI para que cree un vector ı́ndice usando el vector máscara.
Cuando el vector máscara tiene todas sus entradas a uno, se crea un vector ı́ndice
estándar.
Las cargas y almacenamientos indexados y la instrucción CVI proporcionan un
método alternativo para soportar la ejecución condicional. A continuación se mues-
tra la secuencia de instrucciones que implementa el bucle que vimos al estudiar este
problema y que corresponde con el bucle mostrado en la página 24:
5n ≥ 4n + 4 × f × n
1
lo que ocurre cuando 4
≥ f.
Es decir, el segundo método es más rápido que el primero si menos de la cuarta
parte de los elementos son no nulos. En muchos casos la frecuencia de ejecución es
mucho menor. Si el mismo vector de ı́ndices puede ser usado varias veces, o si crece el
número de sentencias vectoriales con la sentencia if, la ventaja de la aproximación de
dispersar/agrupar aumentará claramente.
1 r
P = = (1.4)
(1 − f ) + f /r (1 − f )r + f
5.5
90%
4.5
4
Rendimiento relativo (P)
80%
3.5
70%
2.5
2
50%
1.5
30%
1
1 2 3 4 5 6 7 8 9 10
relacion de velocidad escalar/vectorial (r)
Tarranque = 12 + 7 + 12 + 6 + 12 = 49
La velocidad sostenida está por encima de 4 ciclos de reloj por iteración, más que la
velocidad teórica de 3 campanadas, que ignora los costes adicionales. La mayor parte
de esta diferencia es el coste de inicio para cada bloque de 64 elementos (49 ciclos frente
a 15 de la sobrecarga del bucle).
Podemos calcular R∞ para una frecuencia de reloj de 200 MHz como
µ ¶
Operaciones por iteración × f recuencia de reloj
R∞ = limn→∞
Ciclos de reloj por iteración
El benchmark Linpack es una eliminación de Gauss sobre una matriz de 100 × 100. Ası́,
la longitud de los elementos van desde 99 hasta 1. Un vector de longitud k se usa k
veces. Ası́, la longitud media del vector viene dada por
P99 2
i
Pi=1
99 = 66.3
i=1 i
Ahora podemos determinar de forma más precisa el rendimiento del DAXPY usando
una longitud de vector de 66.
Tn = 2 × (15 + 49) + 3 × 66 = 128 + 198 = 326
2 × 66 × 200 MHz
R66 = = 81 MFLOPS
326
El rendimiento máximo, ignorando los costes de inicio, es 1.64 veces superior que
el rendimiento sostenido que hemos calculado. En realidad, el benchmark Linpack
contiene una fracción no trivial de código que no puede vectorizarse. Aunque este
código supone menos del 20% del tiempo antes de la vectorización, se ejecuta a menos
de una décima parte del rendimiento cuando se mide en FLOPs. Ası́, la ley de Amdahl
nos dice que el rendimiento total será significativamente menor que el rendimiento
estimado al analizar el bucle interno.
Dado que la longitud del vector tiene un impacto significativo en el rendimiento, las
medidas N1/2 y Nv se usan a menudo para comparar máquinas vectoriales.
Ejemplo Calcular N1/2 para el bucle interno de DAXPY para el DLXV con un reloj
de 200 MHz.
Respuesta Usando R∞ como velocidad máxima, queremos saber para qué longitud del
vector obtendremos 50 MFLOPS. Empezaremos con la fórmula para MFLOPS
suponiendo que las medidas se realizan para N1/2 elementos:
F LOP s ejecutados en N1/2 iteraciones Ciclos de reloj
M F LOP S = × × 10−6
Ciclos de reloj para N1/2 iteraciones Segundos
2 × N1/2
50 = × 200
TN1/2
Simplificando esta expresión y suponiendo que N1/2 ≤ 64, tenemos que Tn≤64 =
1 × 64 + 3 × n, lo que da lugar a
TN1/2 = 8 × N1/2
1 × 64 + 3 × N1/2 = 8 × N1/2
5 × N1/2 = 64
N1/2 = 12.8
Por lo tanto, N1/2 = 13; es decir, un vector de longitud 13 proporciona aproxima-
damente la mitad del rendimiento máximo del DLXV en el bucle DAXPY.
Ejemplo ¿Cuál es la longitud del vector, Nv , para que la operación vectorial se ejecute
más rápidamente que la escalar?
Respuesta De nuevo, sabemos que Rv < 64. El tiempo de una iteración en modo
escalar se puede estimar como 10 + 12 + 12 + 7 + 6 + 12 = 59 ciclos de reloj, donde
10 es el tiempo estimado de la sobrecarga del bucle. En el ejemplo anterior se vio
que Tn≤64 = 64 + 3 × n ciclos de reloj. Por lo tanto,
64 + 3 × Nv = 59 Nv
» ¼
64
Nv =
56
Nv = 2
2 × 200 MHz
R∞ = = 400 MFLOPS
1
Añadir unidades adicionales de acceso a memoria y una lógica de emisión más
flexible da lugar a una mejora en el rendimiento máximo de un factor de 4. Sin
embargo, T66 = 130, por lo que para vectores cortos, la mejora en el rendimiento
sostenido es de 326
100
= 2.5 veces.
10000
1000
T94
C90
DEC 8200
Ymp
IBM Power2/990
Xmp
100 MIPS R4400
Linpack MFLOPS Xmp
HP9000/735
10
Cray n=1000
MIPS M/2000
Cray n=100
MIPS M/120 Micro n=1000
Micro n=100
1 Sun 4/260
1975 1980 1985 1990 1995 2000
Figura 1.18: Comparación del rendimiento de los procesadores vectoriales y los micro-
procesadores escalares para la resolución de un sistema de ecuaciones lineales denso
(tamaño de la matriz=n × n).