En informática , un procesador vectorial o procesador de matriz es una unidad central de procesamiento (CPU) que implementa un conjunto de instrucciones donde sus instrucciones están diseñadas para operar de manera eficiente y efectiva en grandes matrices unidimensionales de datos llamadas vectores . Esto contrasta con los procesadores escalares , cuyas instrucciones operan solo en elementos de datos individuales, y en contraste con algunos de esos mismos procesadores escalares que tienen unidades aritméticas adicionales de instrucción única, datos múltiples (SIMD) o SIMD dentro de un registro (SWAR). Los procesadores vectoriales pueden mejorar en gran medida el rendimiento en ciertas cargas de trabajo, en particular la simulación numérica y tareas similares. Las técnicas de procesamiento vectorial también funcionan en el hardware de consolas de videojuegos y en aceleradores gráficos .
Las máquinas vectoriales aparecieron a principios de los años 70 y dominaron el diseño de supercomputadoras durante esa década y hasta los años 90, en particular las diversas plataformas Cray . La rápida caída de la relación precio-rendimiento de los diseños de microprocesadores convencionales condujo a un declive de las supercomputadoras vectoriales durante los años 90.
El desarrollo del procesamiento vectorial comenzó a principios de los años 1960 en Westinghouse Electric Corporation en su proyecto Solomon . El objetivo de Solomon era aumentar drásticamente el rendimiento matemático mediante el uso de una gran cantidad de coprocesadores simples bajo el control de una única unidad central de procesamiento (CPU). La CPU proporcionaba una única instrucción común a todas las unidades aritmético-lógicas (ALU), una por ciclo, pero con un punto de datos diferente para que cada una trabajara. Esto permitió que la máquina Solomon aplicara un único algoritmo a un gran conjunto de datos , que se proporcionaba en forma de matriz. [ cita requerida ]
En 1962, Westinghouse canceló el proyecto, pero la Universidad de Illinois en Urbana-Champaign retomó el proyecto con el nombre de ILLIAC IV . Su versión del diseño originalmente requería una máquina de 1 GFLOPS con 256 ALU, pero, cuando finalmente se entregó en 1972, tenía solo 64 ALU y podía alcanzar solo de 100 a 150 MFLOPS. Sin embargo, demostró que el concepto básico era sólido y, cuando se utilizó en aplicaciones de uso intensivo de datos, como la dinámica de fluidos computacional , ILLIAC fue la máquina más rápida del mundo. El enfoque de ILLIAC de usar ALU separadas para cada elemento de datos no es común en los diseños posteriores y, a menudo, se hace referencia a él en una categoría separada, la computación masivamente paralela . En esta época, Flynn categorizó este tipo de procesamiento como una forma temprana de instrucción única, múltiples subprocesos (SIMT). [ cita requerida ]
International Computers Limited intentó evitar muchas de las dificultades del concepto ILLIAC con su propio diseño de procesador de matriz distribuida (DAP), categorizando a ILLIAC y DAP como procesadores de matriz celular que potencialmente ofrecían beneficios de rendimiento sustanciales sobre los diseños de procesadores vectoriales convencionales como el CDC STAR-100 y Cray 1. [1]
Kartsev presentó y desarrolló en 1967 un ordenador para operaciones con funciones. [2]
Las primeras supercomputadoras vectoriales son la Control Data Corporation STAR-100 y la Texas Instruments Advanced Scientific Computer (ASC), que se introdujeron en 1974 y 1972, respectivamente.
La ALU básica de ASC (es decir, "one pipe") utilizaba una arquitectura de pipeline que admitía cálculos escalares y vectoriales, con un rendimiento máximo que alcanzaba aproximadamente 20 MFLOPS, que se lograba fácilmente al procesar vectores largos. Las configuraciones de ALU expandidas admitían "dos pipes" o "cuatro pipes" con una ganancia de rendimiento correspondiente de 2X o 4X. El ancho de banda de memoria era suficiente para admitir estos modos expandidos.
El STAR-100 era más lento que los superordenadores del CDC, como el CDC 7600 , pero en las tareas relacionadas con los datos podía seguirle el ritmo y además era mucho más pequeño y menos costoso. Sin embargo, la máquina también tardaba mucho tiempo en decodificar las instrucciones vectoriales y prepararse para ejecutar el proceso, por lo que necesitaba conjuntos de datos muy específicos para trabajar antes de poder acelerar algo.
La técnica vectorial fue explotada por primera vez en su totalidad en 1976 por el famoso Cray-1 . En lugar de dejar los datos en la memoria como el STAR-100 y el ASC, el diseño de Cray tenía ocho registros vectoriales , que contenían sesenta y cuatro palabras de 64 bits cada uno. Las instrucciones vectoriales se aplicaban entre registros, lo que es mucho más rápido que hablar con la memoria principal. Mientras que el STAR-100 aplicaba una sola operación a través de un vector largo en la memoria y luego pasaba a la siguiente operación, el diseño de Cray cargaba una sección más pequeña del vector en los registros y luego aplicaba tantas operaciones como pudiera a esos datos, evitando así muchas de las operaciones de acceso a la memoria mucho más lentas.
El diseño de Cray utilizó paralelismo de canalización para implementar instrucciones vectoriales en lugar de múltiples ALU. Además, el diseño tenía canalizaciones completamente separadas para diferentes instrucciones, por ejemplo, la suma/resta se implementaba en un hardware diferente al de la multiplicación. Esto permitió que un lote de instrucciones vectoriales se canalizara hacia cada una de las subunidades de la ALU, una técnica que llamaron encadenamiento vectorial . El Cray-1 normalmente tenía un rendimiento de aproximadamente 80 MFLOPS, pero con hasta tres cadenas en funcionamiento podía alcanzar un máximo de 240 MFLOPS y un promedio de alrededor de 150, mucho más rápido que cualquier máquina de la época.
Otros ejemplos siguieron. Control Data Corporation intentó volver a entrar en el mercado de alta gama con su máquina ETA-10 , pero se vendió mal y tomaron eso como una oportunidad para abandonar el campo de la supercomputación por completo. A principios y mediados de la década de 1980, las empresas japonesas ( Fujitsu , Hitachi y Nippon Electric Corporation (NEC) introdujeron máquinas vectoriales basadas en registros similares a la Cray-1, que por lo general eran un poco más rápidas y mucho más pequeñas. Floating Point Systems (FPS) con sede en Oregón construyó procesadores de matriz complementarios para minicomputadoras , y luego construyó sus propias minisupercomputadoras .
A lo largo de todo este tiempo, Cray siguió siendo el líder en rendimiento, superando continuamente a la competencia con una serie de máquinas que dieron lugar a Cray-2 , Cray X-MP y Cray Y-MP . Desde entonces, el mercado de supercomputadoras se ha centrado mucho más en el procesamiento masivo en paralelo que en mejores implementaciones de procesadores vectoriales. Sin embargo, reconociendo los beneficios del procesamiento vectorial, IBM desarrolló Virtual Vector Architecture para su uso en supercomputadoras que acoplaran varios procesadores escalares para que actuaran como un procesador vectorial.
Aunque las supercomputadoras vectoriales similares a la Cray-1 son menos populares en la actualidad, NEC ha seguido fabricando este tipo de computadoras hasta el día de hoy con su serie de computadoras SX. Más recientemente, la SX-Aurora TSUBASA coloca el procesador y 24 o 48 gigabytes de memoria en un módulo HBM 2 dentro de una tarjeta que se asemeja físicamente a un coprocesador gráfico, pero en lugar de funcionar como coprocesador, es la computadora principal y la computadora compatible con PC a la que está conectada cumple funciones de soporte.
Las unidades de procesamiento gráfico ( GPU ) modernas incluyen una serie de canales de sombreado que pueden ser controlados por núcleos de cómputo y pueden considerarse procesadores vectoriales (que utilizan una estrategia similar para ocultar las latencias de memoria). Como se muestra en el artículo de Flynn de 1972, el factor distintivo clave de las GPU basadas en SIMT es que tienen un único decodificador-transmisor de instrucciones, pero que los núcleos que reciben y ejecutan esa misma instrucción son, por lo demás, razonablemente normales: sus propias ALU, sus propios archivos de registro, sus propias unidades de carga/almacenamiento y sus propios cachés de datos L1 independientes. Por lo tanto, aunque todos los núcleos ejecutan simultáneamente exactamente la misma instrucción al unísono, lo hacen con datos completamente diferentes de ubicaciones de memoria completamente diferentes. Esto es significativamente más complejo y complejo que "Packed SIMD" , que está estrictamente limitado a la ejecución de operaciones aritméticas en canalización paralela únicamente. Aunque los detalles internos exactos de las GPU comerciales actuales son secretos privados, el equipo de MIAOW [3] pudo reunir información anecdótica suficiente para implementar un subconjunto de la arquitectura AMDGPU. [4]
Varias arquitecturas de CPU modernas se están diseñando como procesadores vectoriales. La extensión vectorial RISC-V sigue principios similares a los de los primeros procesadores vectoriales y se está implementando en productos comerciales como el AX45MPV de Andes Technology . [5] También se están desarrollando varias arquitecturas de procesadores vectoriales de código abierto , entre ellas ForwardCom y Libre-SOC .
A partir de 2016, [actualizar]la mayoría de las CPU de consumo implementan arquitecturas que cuentan con instrucciones SIMD de longitud fija. A primera vista, se las puede considerar una forma de procesamiento vectorial porque operan en conjuntos de datos múltiples (vectorizados, de longitud explícita) y toman prestadas características de los procesadores vectoriales. Sin embargo, por definición, la adición de SIMD no puede, por sí sola, calificar a un procesador como un procesador vectorial real , porque SIMD es de longitud fija y los vectores son de longitud variable . La diferencia se ilustra a continuación con ejemplos, que muestran y comparan las tres categorías: SIMD puro, SIMD predicado y procesamiento vectorial puro. [ cita requerida ]
Otros diseños de CPU incluyen algunas instrucciones múltiples para el procesamiento vectorial en conjuntos de datos múltiples (vectorizados), conocidas normalmente como MIMD (Instrucción múltiple, Datos múltiples) y realizadas con VLIW (Palabra de instrucción muy larga) y EPIC (Computación de instrucción explícitamente paralela). El procesador vectorial/VLIW Fujitsu FR-V combina ambas tecnologías.
Los conjuntos de instrucciones SIMD carecen de características cruciales en comparación con los conjuntos de instrucciones vectoriales. La más importante de ellas es que los procesadores vectoriales, por definición y diseño, siempre han tenido una longitud variable desde su creación.
Aunque a menudo se afirma erróneamente que el SIMD puro (de ancho fijo, sin predicción) es "vectorial" (porque SIMD procesa datos que resultan ser vectores), a través de un análisis minucioso y una comparación de los ISA históricos y modernos, se puede observar que los ISA vectoriales reales tienen las siguientes características que ningún ISA SIMD tiene: [ cita requerida ]
vsetvl
instrucción en RISCV RVV, [7] o la lvl
instrucción en NEC SX, [8] sin restringir la longitud a una potencia de dos o a un múltiplo de un ancho de datos fijo.SIMD predicado (parte de la taxonomía de Flynn ), que son máscaras de predicados integrales a nivel de elemento individual en cada instrucción vectorial, como las que ahora están disponibles en ARM SVE2. [9] Y AVX-512 , casi califica como un procesador vectorial. [ ¿Cómo? ] SIMD predicado utiliza ALU SIMD de ancho fijo, pero permite la activación localmente controlada (predicada) de unidades para proporcionar la apariencia de vectores de longitud variable. Los ejemplos a continuación ayudan a explicar estas distinciones categóricas.
Debido a que utiliza procesamiento por lotes de ancho fijo, SIMD no puede por diseño hacer frente a la iteración y la reducción. Esto se ilustra con más detalle a continuación con ejemplos.
Además, los procesadores vectoriales pueden ser más eficientes en el uso de recursos al utilizar hardware más lento y ahorrar energía, pero aún así lograr rendimiento y tener menos latencia que SIMD, a través del encadenamiento de vectores . [10] [11]
Consideremos un procesador SIMD y un procesador vectorial que trabajan con 4 elementos de 64 bits y realizan una secuencia de CARGA, SUMA, MULTIPLICACIÓN y ALMACENAMIENTO. Si el ancho de SIMD es 4, entonces el procesador SIMD debe CARGAR cuatro elementos por completo antes de poder pasar a las SUMA, debe completar todas las SUMA antes de poder pasar a las MULTIPLICACIONES y, asimismo, debe completar todas las MULTIPLICACIONES antes de poder comenzar las ALMACENAMIENTOS. Esto es así por definición y por diseño. [12]
El hecho de tener que realizar 4 cargas simultáneas de 64 bits y 64 bits de almacenamiento es muy costoso en términos de hardware (rutas de datos de 256 bits a la memoria). Lo mismo ocurre con 4 ALU de 64 bits, especialmente MULTIPLY. Para evitar estos altos costos, un procesador SIMD tendría que tener 1 carga de 64 bits de ancho, 1 almacenamiento de 64 bits de ancho y solo 2 ALU de 64 bits de ancho. Como se muestra en el diagrama, que asume un modelo de ejecución de múltiples emisiones , las consecuencias son que las operaciones ahora tardan más en completarse. Si no es posible realizar múltiples emisiones, las operaciones tardan aún más porque el LD puede no emitirse (iniciar) al mismo tiempo que los primeros ADD, y así sucesivamente. Si solo hay 4 ALU SIMD de 64 bits de ancho, el tiempo de finalización es aún peor: solo cuando se hayan completado las cuatro cargas pueden comenzar las operaciones SIMD, y solo cuando se hayan completado todas las operaciones ALU pueden comenzar los almacenamientos.
Por el contrario, un procesador vectorial, incluso si es de un solo problema y no utiliza ALU SIMD, y solo tiene LOAD de 64 bits de ancho 1, STORE de 64 bits de ancho 1 (y, como en el Cray-1 , la capacidad de ejecutar MULTIPLY simultáneamente con ADD), puede completar las cuatro operaciones más rápido que un procesador SIMD con LOAD de ancho 1, STORE de ancho 1 y SIMD de ancho 2. Esta utilización más eficiente de los recursos, debido al encadenamiento vectorial , es una ventaja y diferencia clave en comparación con SIMD. SIMD, por diseño y definición, no puede realizar encadenamiento excepto para todo el grupo de resultados. [13]
En términos generales, las CPU pueden manipular uno o dos datos a la vez. Por ejemplo, la mayoría de las CPU tienen una instrucción que básicamente dice "suma A a B y pon el resultado en C". Los datos de A, B y C podrían codificarse (al menos en teoría) directamente en la instrucción. Sin embargo, en una implementación eficiente, las cosas rara vez son tan simples. Los datos rara vez se envían en forma cruda, sino que se "apuntan" pasando una dirección a una ubicación de memoria que contiene los datos. Decodificar esta dirección y sacar los datos de la memoria lleva algún tiempo, durante el cual la CPU tradicionalmente permanecería inactiva esperando a que aparecieran los datos solicitados. A medida que las velocidades de la CPU han aumentado, esta latencia de memoria se ha convertido históricamente en un gran impedimento para el rendimiento; consulte Memoria de acceso aleatorio § Muro de memoria .
Para reducir la cantidad de tiempo que consumen estos pasos, la mayoría de las CPU modernas utilizan una técnica conocida como canalización de instrucciones , en la que las instrucciones pasan por varias subunidades a su vez. La primera subunidad lee la dirección y la decodifica, la siguiente "obtiene" los valores en esas direcciones y la siguiente hace los cálculos por sí misma. Con la canalización, el "truco" consiste en comenzar a decodificar la siguiente instrucción incluso antes de que la primera haya salido de la CPU, a la manera de una cadena de montaje , de modo que el decodificador de direcciones esté en uso constantemente. Cualquier instrucción en particular tarda la misma cantidad de tiempo en completarse, un tiempo conocido como latencia , pero la CPU puede procesar un lote completo de operaciones, de manera superpuesta, mucho más rápido y de manera más eficiente que si lo hiciera una a la vez.
Los procesadores vectoriales llevan este concepto un paso más allá. En lugar de canalizar sólo las instrucciones, también canalizan los datos en sí. El procesador recibe instrucciones que no sólo indican que hay que sumar A a B, sino que hay que sumar todos los números "de aquí a aquí" a todos los números "de allí a allí". En lugar de tener que decodificar constantemente las instrucciones y luego buscar los datos necesarios para completarlas, el procesador lee una única instrucción de la memoria y simplemente está implícito en la definición de la propia instrucción que la instrucción volverá a funcionar en otro elemento de datos, en una dirección un incremento mayor que la última. Esto permite un ahorro significativo en el tiempo de decodificación.
Para ilustrar la diferencia que esto puede suponer, considere la sencilla tarea de sumar dos grupos de 10 números. En un lenguaje de programación normal, se escribiría un "bucle" que recogiera cada uno de los pares de números por turno y luego los sumara. Para la CPU, esto se vería así:
; Máquina RISC hipotética ; suponga que a, b y c son ubicaciones de memoria en sus respectivos registros ; sume 10 números en a a 10 números en b, almacene los resultados en c mueva $10 , cuente ; cuente: = 10 bucle: cargue r1 , a cargue r2 , b sume r3 , r1 , r2 ; r3: = r1 + r2 almacene r3 , c sume a , a , $ 4 ; continúe sume b , b , $4 sume c , c , $4 dec cuenta ; decremente cuenta , bucle ; vuelva a bucle si cuenta aún no es 0 ret
Pero para un procesador vectorial, esta tarea parece considerablemente diferente:
; supongamos que tenemos registros vectoriales v1-v3 ; con tamaño igual o mayor que 10 mover $10 , contar ; contar = 10 vload v1 , a , contar vload v2 , b , contar vadd v3 , v1 , v2 vstore v3 , c , contar ret
Tenga en cuenta la falta total de bucles en las instrucciones, porque es el hardware el que ha realizado 10 operaciones secuenciales: efectivamente, el recuento de bucles se realiza explícitamente por instrucción .
Las ISA vectoriales de estilo Cray llevan esto un paso más allá y proporcionan un registro de "conteo" global, llamado longitud vectorial (VL):
; nuevamente supongamos que tenemos registros vectoriales v1-v3 ; con tamaño mayor o igual a 10 setvli $10 # Establecer longitud del vector VL=10 vload v1 , a # 10 carga desde a vload v2 , b # 10 carga desde b vadd v3 , v1 , v2 # 10 agrega vstore v3 , c # 10 almacena en c ret
Este enfoque conlleva varios ahorros inherentes. [14]
Además, en los procesadores vectoriales ISA más modernos, se ha introducido "Fail on First" o "Fault First" (ver más abajo), lo que aporta aún más ventajas.
Pero más que eso, un procesador vectorial de alto rendimiento puede tener múltiples unidades funcionales que sumen esos números en paralelo. No es necesario verificar las dependencias entre esos números, ya que una instrucción vectorial especifica múltiples operaciones independientes. Esto simplifica la lógica de control requerida y puede mejorar aún más el rendimiento al evitar bloqueos. De este modo, las operaciones matemáticas se completan mucho más rápido en general; el factor limitante es el tiempo necesario para recuperar los datos de la memoria.
No todos los problemas pueden ser atacados con este tipo de solución. Incluir este tipo de instrucciones necesariamente agrega complejidad al núcleo de la CPU. Esa complejidad generalmente hace que otras instrucciones se ejecuten más lentamente, es decir, siempre que no estén sumando muchos números seguidos. Las instrucciones más complejas también aumentan la complejidad de los decodificadores, lo que puede ralentizar la decodificación de las instrucciones más comunes, como la suma normal. ( Esto se puede mitigar en cierta medida manteniendo todos los principios de ISA a RISC : RVV solo agrega alrededor de 190 instrucciones vectoriales incluso con las características avanzadas. [15] )
Los procesadores vectoriales se diseñaron tradicionalmente para funcionar mejor solo cuando hay grandes cantidades de datos con los que trabajar. Por este motivo, este tipo de CPU se encontraban principalmente en supercomputadoras , ya que las supercomputadoras en sí mismas se encontraban, en general, en lugares como centros de predicción meteorológica y laboratorios de física, donde se "procesan" enormes cantidades de datos. Sin embargo, como se muestra arriba y lo demuestra RISC-V RVV, la eficiencia de las ISA vectoriales trae otros beneficios que son atractivos incluso para los casos de uso integrados.
El ejemplo de pseudocódigo vectorial anterior parte de la premisa de que el ordenador vectorial puede procesar más de diez números en un lote. Para una mayor cantidad de números en el registro vectorial, resulta inviable que el ordenador tenga un registro tan grande. Como resultado, el procesador vectorial obtiene la capacidad de realizar bucles por sí mismo o expone algún tipo de registro de control vectorial (estado) al programador, generalmente conocido como longitud vectorial.
Las instrucciones autorrepetitivas se encuentran en los primeros ordenadores vectoriales como el STAR-100, donde la acción anterior se describía en una única instrucción (algo así como vadd c, a, b, $10
). También se encuentran en la arquitectura x86REP
como prefijo. Sin embargo, de esta manera solo se pueden realizar cálculos muy simples de manera efectiva en hardware sin un aumento de costo muy grande. Dado que todos los operandos tienen que estar en la memoria para la arquitectura STAR-100, la latencia causada por el acceso también se volvió enorme.
Broadcom incluyó espacio en todas las operaciones vectoriales del ISA Videocore IV para un REP
campo, pero a diferencia del STAR-100 que utiliza memoria para sus repeticiones, las repeticiones del Videocore IV están en todas las operaciones, incluidas las operaciones vectoriales aritméticas. La longitud de la repetición puede ser un rango pequeño de potencia de dos o provenir de uno de los registros escalares. [16]
El Cray-1 introdujo la idea de utilizar registros de procesador para almacenar datos vectoriales en lotes. Las longitudes de los lotes (longitud del vector, VL) se podían establecer dinámicamente con una instrucción especial; la importancia en comparación con Videocore IV (y, fundamentalmente como se mostrará más adelante, también con SIMD) es que la longitud de repetición no tiene que ser parte de la codificación de la instrucción. De esta manera, se puede hacer mucho más trabajo en cada lote; la codificación de la instrucción también es mucho más elegante y compacta. El único inconveniente es que para aprovechar al máximo esta capacidad adicional de procesamiento por lotes, la carga de memoria y la velocidad de almacenamiento también tuvieron que aumentar correspondientemente. A veces se afirma [¿ por quién? ] que esto es una desventaja de los procesadores vectoriales de estilo Cray: en realidad es parte de lograr un alto rendimiento, como se ve en las GPU , que enfrentan exactamente el mismo problema.
Los ordenadores SIMD modernos afirman mejorar los primeros Cray al utilizar directamente múltiples ALU, para un mayor grado de paralelismo en comparación con el uso exclusivo de la canalización escalar normal. Los procesadores vectoriales modernos (como el SX-Aurora TSUBASA ) combinan ambos, al emitir múltiples datos a múltiples ALU SIMD canalizadas internamente, siendo el número emitido elegido dinámicamente por el programa vectorial en tiempo de ejecución. Se pueden utilizar máscaras para cargar y almacenar datos de forma selectiva en ubicaciones de memoria, y utilizar esas mismas máscaras para deshabilitar selectivamente el elemento de procesamiento de las ALU SIMD. Algunos procesadores con SIMD ( AVX-512 , ARM SVE2 ) son capaces de este tipo de procesamiento selectivo por elemento ( "predicado" ), y son estos los que de alguna manera merecen la nomenclatura de "procesador vectorial" o al menos merecen la afirmación de ser capaces de "procesamiento vectorial". Los procesadores SIMD sin predicación por elemento ( MMX , SSE , AltiVec ) categóricamente no lo hacen.
Las GPU modernas, que tienen muchas unidades de cómputo pequeñas, cada una con sus propias ALU SIMD independientes, utilizan SIMT ( Single Instruction Multiple Threads ). Las unidades SIMT se ejecutan desde una única unidad de instrucción sincronizada de transmisión compartida. Los "registros vectoriales" son muy amplios y las tuberías tienden a ser largas. La parte de "subprocesamiento" de SIMT implica la forma en que se manejan los datos de forma independiente en cada una de las unidades de cómputo.
Además, las GPU como Broadcom Videocore IV y otros procesadores vectoriales externos como NEC SX-Aurora TSUBASA pueden utilizar menos unidades vectoriales de las que implica el ancho: en lugar de tener 64 unidades para un registro de 64 números de ancho, el hardware podría hacer un bucle segmentado sobre 16 unidades para un enfoque híbrido. Broadcom Videocore IV también es capaz de este enfoque híbrido: nominalmente afirma que su motor SIMD QPU admite operaciones de matriz de 16 FP de longitud en sus instrucciones, pero en realidad las realiza de 4 en 4, como (otra) forma de "hilos". [17]
Este ejemplo comienza con un algoritmo ("IAXPY"), primero lo muestra en instrucciones escalares, luego SIMD, luego SIMD predicado y finalmente instrucciones vectoriales. Esto ayuda a ilustrar de manera incremental la diferencia entre un procesador vectorial tradicional y uno SIMD moderno. El ejemplo comienza con una variante entera de 32 bits de la función "DAXPY", en C :
void iaxpy ( tamaño_t n , int a , const int x [], int y []) { para ( tamaño_t i = 0 ; i < n ; i ++ ) y [ i ] = a * x [ i ] + y [ i ]; }
En cada iteración, a cada elemento de y se le suma un elemento de x multiplicado por a. El programa se expresa en forma lineal escalar para facilitar su lectura.
La versión escalar de esto cargaría uno de cada uno de x e y, procesaría un cálculo, almacenaría un resultado y realizaría un bucle:
loop: load32 r1 , x ; carga un dato de 32 bits load32 r2 , y mul32 r1 , a , r1 ; r1 := r1 * a add32 r3 , r1 , r2 ; r3 := r1 + r2 store32 r3 , y addl x , x , $4 ; x := x + 4 addl y , y , $4 subl n , n , $1 ; n := n - 1 jgz n , loop ; vuelve al bucle si n > 0 out: ret
El código similar al de STAR sigue siendo conciso, pero como la vectorización del STAR-100 se basó por diseño en los accesos a la memoria, ahora se necesita una ranura de memoria adicional para procesar la información. También se necesita el doble de latencia debido al requisito adicional de acceso a la memoria.
; Suponga que tmp está preasignado vmul tmp , a , x , n ; tmp[i] = a * x[i] vadd y , y , tmp , n ; y[i] = y[i] + tmp[i] ret
Una arquitectura SIMD empaquetada moderna, conocida por muchos nombres (enumerados en la taxonomía de Flynn ), puede realizar la mayoría de las operaciones en lotes. El código es en su mayor parte similar a la versión escalar. Se supone que tanto x como y están correctamente alineados aquí (solo comienzan en un múltiplo de 16) y que n es un múltiplo de 4, ya que de lo contrario se necesitaría algún código de configuración para calcular una máscara o ejecutar una versión escalar. También se puede suponer, para simplificar, que las instrucciones SIMD tienen una opción para repetir automáticamente los operandos escalares, como puede hacerlo ARM NEON. [18] Si no es así, se debe utilizar un "splat" (transmisión) para copiar el argumento escalar a través de un registro SIMD:
splatx4 v4 , a ; v4 = a,a,a,a
El tiempo empleado sería básicamente el mismo que el de una implementación vectorial y = mx + c
descrita anteriormente.
vloop: load32x4 v1 , x load32x4 v2 , y mul32x4 v1 , a , v1 ; v1 := v1 * a add32x4 v3 , v1 , v2 ; v3 := v1 + v2 store32x4 v3 , y addl x , x , $16 ; x := x + 16 addl y , y , $16 subl n , n , $4 ; n := n - 4 jgz n , vloop ; retroceder si n > 0 salida: ret
Tenga en cuenta que tanto los punteros x como y se incrementan en 16, porque esa es la longitud (en bytes) de cuatro números enteros de 32 bits. Se tomó la decisión de que el algoritmo solo se ocuparía de SIMD de 4 anchos, por lo tanto, la constante está codificada en el programa.
Desafortunadamente para SIMD, la clave estaba en la suposición anterior, "que n es un múltiplo de 4", así como en el "acceso alineado", que, claramente, es un caso de uso especializado limitado.
De manera realista, para bucles de propósito general, como los de las bibliotecas portátiles, donde n no se puede limitar de esta manera, la sobrecarga de configuración y limpieza de SIMD para poder hacer frente a números que no sean múltiplos del ancho de SIMD puede superar con creces el recuento de instrucciones dentro del propio bucle. Suponiendo que, en el peor de los casos, el hardware no pueda realizar accesos a la memoria SIMD desalineados, un algoritmo del mundo real:
El SIMD de ocho anchos requiere repetir el algoritmo de bucle interno primero con elementos SIMD de cuatro anchos, luego con SIMD de dos anchos, luego con uno (escalar), con una prueba y una rama entre cada uno, para cubrir el primer y el último elemento SIMD restante (0 <= n <= 7).
Esto triplica el tamaño del código y, de hecho, en casos extremos, da como resultado un aumento de un orden de magnitud en el recuento de instrucciones. Esto se puede demostrar fácilmente compilando el ejemplo iaxpy para AVX-512 , utilizando las opciones "-O3 -march=knl"
de gcc .
Con el tiempo, a medida que ISA evoluciona para seguir aumentando el rendimiento, los arquitectos de ISA agregan SIMD de 2 anchos, luego SIMD de 4 anchos, luego SIMD de 8 anchos y más. Por lo tanto, se puede entender por qué existe AVX-512 en x86.
Sin predicción, cuanto mayor sea el ancho de SIMD, peores serán los problemas, lo que lleva a una proliferación masiva de códigos de operación, rendimiento degradado, consumo adicional de energía y complejidad de software innecesaria. [19]
Por otro lado, los procesadores vectoriales están diseñados para realizar cálculos de longitud variable para un conteo arbitrario, n, y por lo tanto requieren muy poca configuración y ninguna limpieza. Incluso en comparación con las ISA SIMD que tienen máscaras (pero ninguna setvl
instrucción), los procesadores vectoriales producen un código mucho más compacto porque no necesitan realizar un cálculo de máscara explícito para cubrir los últimos elementos (ilustrado a continuación).
Suponiendo una ISA SIMD hipotéticamente predicha (con capacidad de máscara), y suponiendo nuevamente que las instrucciones SIMD pueden manejar datos desalineados, el bucle de instrucciones se vería así:
vloop: # preparar máscara. algunas ISA tienen mínimo aunque min t0 , n , $4 ; t0 = min(n, 4) shift m , $1 , t0 ; m = 1<<t0 sub m , m , $1 ; m = (1<<t0)-1 # ahora realiza la operación, enmascarada por m bits load32x4 v1 , x , m load32x4 v2 , y , m mul32x4 v1 , a , v1 , m ; v1:= v1 * a add32x4 v3 , v1 , v2 , m ; v3:= v1 + v2 store32x4 v3 , y , m # actualiza x, y y n para el siguiente bucle addl x , t0 * 4 ; x := x + t0*4 addl y , t0 * 4 subl n , n , t0 ; n := n - t0 # bucle? jgz n , vloop ; volver atrás si n > 0 salida: ret
Aquí se puede ver que el código es mucho más limpio pero un poco complejo: al menos, sin embargo, no hay configuración ni limpieza: en la última iteración del bucle, la máscara de predicado se establecerá en 0b0000, 0b0001, 0b0011, 0b0111 o 0b1111, lo que dará como resultado que se realicen entre 0 y 4 operaciones de elementos SIMD, respectivamente. Una posible complicación adicional: algunas ISA RISC no tienen una instrucción "min", y necesitan en su lugar usar una comparación predicada de bifurcación o escalar.
Está claro que el SIMD predicado merece al menos el término "capaz de trabajar con vectores", porque puede trabajar con vectores de longitud variable mediante el uso de máscaras de predicado. Sin embargo, el paso final hacia una ISA vectorial "verdadera" es no tener ninguna evidencia en la ISA de un ancho de SIMD, y dejar eso completamente en manos del hardware.
Para las ISA vectoriales de estilo Cray, como RVV, se utiliza una instrucción llamada "setvl" (establecer longitud del vector). El hardware primero define cuántos valores de datos puede procesar en un "vector": pueden ser registros reales o un bucle interno (el enfoque híbrido, mencionado anteriormente). Esta cantidad máxima (la cantidad de "carriles" de hardware) se denomina "MVL" (Longitud máxima del vector). Tenga en cuenta que, como se vio en SX-Aurora y Videocore IV, MVL puede ser una cantidad de carriles de hardware real o virtual . (Nota: como se mencionó en el Tutorial de ARM SVE2, los programadores no deben cometer el error de asumir un ancho de vector fijo: en consecuencia, MVL no es una cantidad que el programador necesite saber. Esto puede ser un poco desconcertante después de años de mentalidad SIMD). [ tono ]
Al llamar a setvl con la cantidad de elementos de datos pendientes de procesar, "setvl" tiene permitido (esencialmente es necesario) limitarlo a la Longitud Máxima de Vector (MVL) y, por lo tanto, devuelve la cantidad real que puede procesar el hardware en las instrucciones vectoriales posteriores y establece el registro especial interno, "VL", en esa misma cantidad. ARM se refiere a esta técnica como programación "agnóstica a la longitud del vector" en sus tutoriales sobre SVE2. [20]
A continuación se muestra el ensamblador vectorial de estilo Cray para el mismo bucle de estilo SIMD, arriba. Tenga en cuenta que se utiliza t0 (que, al contener una copia conveniente de VL, puede variar) en lugar de constantes codificadas:
vloop: setvl t0 , n # VL=t0=min(MVL, n) vld32 v0 , x # cargar vector x vld32 v1 , y # cargar vector y vmadd32 v1 , v0 , a # v1 += v0 * a vst32 v1 , y # almacenar Y sumar y , t0 * 4 # avanzar y por VL*4 sumar x , t0 * 4 # avanzar x por VL*4 sub n , t0 # n -= VL (t0) bnez n , vloop # repetir si n != 0
Esto esencialmente no es muy diferente de la versión SIMD (procesa 4 elementos de datos por bucle) o de la versión inicial Scalar (procesa solo uno). n todavía contiene el número de elementos de datos que quedan por procesar, pero t0 contiene la copia de VL: el número que se procesará en cada iteración. t0 se resta de n después de cada iteración, y si n es cero, entonces se han procesado todos los elementos.
Una serie de cosas a tener en cuenta al comparar con la variante de ensamblaje SIMD predicho:
setvl
instrucción tiene incorporada una min
instrucciónDe esta forma se puede ver, muy claramente, cómo las ISA vectoriales reducen el número de instrucciones.
También tenga en cuenta que, al igual que la variante SIMD predicada, los punteros a x e y se avanzan por t0 multiplicado por cuatro porque ambos apuntan a datos de 32 bits, pero n se decrementa por t0 directo. En comparación con el ensamblador SIMD de tamaño fijo, hay muy poca diferencia aparente: x e y avanzan por una constante codificada de forma rígida 16, n se decrementa por un 4 codificado de forma rígida, por lo que inicialmente es difícil apreciar la importancia. La diferencia viene en la comprensión de que el hardware vectorial podría ser capaz de realizar 4 operaciones simultáneas, o 64, o 10.000, sería exactamente el mismo ensamblador vectorial para todas ellas y todavía no habría código de limpieza SIMD . Incluso comparado con el SIMD con capacidad de predicado, sigue siendo más compacto, más claro, más elegante y utiliza menos recursos.
No solo es un programa mucho más compacto (ahorra el tamaño de caché L1), sino que, como se mencionó anteriormente, la versión vectorial puede emitir mucho más procesamiento de datos a las ALU, ahorrando nuevamente energía porque la decodificación y emisión de instrucciones pueden permanecer inactivas.
Además, la cantidad de elementos que entran en la función puede comenzar en cero. Esto establece la longitud del vector en cero, lo que deshabilita efectivamente todas las instrucciones del vector, convirtiéndolas en operaciones sin efecto en tiempo de ejecución. Por lo tanto, a diferencia del SIMD sin predicado, incluso cuando no hay elementos para procesar, no hay código de limpieza desperdiciado.
Este ejemplo comienza con un algoritmo que implica reducción. Al igual que en el ejemplo anterior, se mostrará primero en instrucciones escalares, luego en SIMD y, por último, en instrucciones vectoriales, comenzando en c :
void ( tamaño_t n , int a , const int x []) { int y = 0 ; para ( tamaño_t i = 0 ; i < n ; i ++ ) y += x [ i ]; devolver y ; }
Aquí, se utiliza un acumulador (y) para sumar todos los valores de la matriz, x.
La versión escalar de esto cargaría cada uno de x, lo agregaría a y y haría un bucle:
establecer y , 0 ; y inicializado a cero loop: load32 r1 , x ; cargar un dato de 32 bits add32 y , y , r1 ; y:= y + r1 addl x , x , $4 ; x:= x + 4 subl n , n , $1 ; n:= n - 1 jgz n , loop ; bucle inverso si n > 0 out: ret y ; devuelve resultado, y
Esto es muy sencillo. "y" comienza en cero, los números enteros de 32 bits se cargan uno a la vez en r1, se suman a y, y la dirección de la matriz "x" se mueve al siguiente elemento de la matriz.
Aquí es donde empiezan los problemas. SIMD, por diseño, es incapaz de realizar operaciones aritméticas "entre elementos". El elemento 0 de un registro SIMD puede añadirse al elemento 0 de otro registro, pero el elemento 0 no puede añadirse a nada que no sea otro elemento 0. Esto impone algunas limitaciones severas a las posibles implementaciones. Para simplificar, se puede suponer que n es exactamente 8:
addl r3 , x , $16 ; para los 2dos 4 de x load32x4 v1 , x ; primeros 4 de x load32x4 v2 , r3 ; 2dos 4 de x add32x4 v1 , v2 , v1 ; sumar 2 grupos
En este punto se han realizado cuatro adiciones:
x[0]+x[4]
- Primer SIMD ADD: elemento 0 del primer grupo añadido al elemento 0 del segundo grupox[1]+x[5]
- Segundo SIMD ADD: elemento 1 del primer grupo añadido al elemento 1 del segundo grupox[2]+x[6]
- Tercera SIMD ADD: elemento 2 del primer grupo añadido al elemento 2 del segundo grupox[3]+x[7]
- Cuarto SIMD ADD: elemento 3 del primer grupo añadido al elemento 2 del segundo grupopero como el SIMD de 4 anchos no puede por diseño sumar x[0]+x[1]
, por ejemplo, las cosas se van rápidamente cuesta abajo, tal como sucedía con el caso general de usar SIMD para bucles IAXPY de propósito general. Para sumar los cuatro resultados parciales, se puede usar SIMD de dos anchos, seguido de una única suma escalar, para finalmente producir la respuesta, pero, con frecuencia, los datos deben transferirse fuera de los registros SIMD dedicados antes de que se pueda realizar el último cálculo escalar.
Incluso con un bucle general (n no fijo), la única forma de utilizar SIMD de 4 anchos es suponer cuatro "flujos" separados, cada uno de ellos desplazado por cuatro elementos. Finalmente, los cuatro resultados parciales deben sumarse. Otras técnicas implican la mezcla: se pueden encontrar ejemplos en línea para AVX-512 de cómo hacer una "suma horizontal" [21] [22]
Además del tamaño del programa y la complejidad, surge un problema potencial adicional si se trata de cálculos de punto flotante: el hecho de que los valores no se sumen en orden estricto (cuatro resultados parciales) podría generar errores de redondeo.
Los conjuntos de instrucciones vectoriales tienen operaciones de reducción aritmética incorporadas en el ISA. Si se supone que n es menor o igual a la longitud máxima del vector, solo se requieren tres instrucciones:
setvl t0 , n # VL=t0=min(MVL, n) vld32 v0 , x # cargar vector x vredadd32 y , v0 # reducir-agregar en y
El código cuando n es mayor que la longitud máxima del vector no es mucho más complejo y es un patrón similar al primer ejemplo ("IAXPY").
establecer y , 0 vloop: setvl t0 , n # VL=t0=min(MVL, n) vld32 v0 , x # cargar vector x vredadd32 y , y , v0 # sumar todo x en y add x , t0 * 4 # avanzar x por VL*4 sub n , t0 # n -= VL (t0) bnez n , vloop # repetir si n != 0 ret y
La simplicidad del algoritmo es abismal en comparación con SIMD. Nuevamente, al igual que en el ejemplo de IAXPY, el algoritmo es independiente de la longitud (incluso en implementaciones integradas donde la longitud máxima del vector podría ser solo uno).
Las implementaciones en hardware pueden, si están seguras de que se producirá la respuesta correcta, realizar la reducción en paralelo. Algunas ISA vectoriales ofrecen un modo de reducción en paralelo como una opción explícita, para cuando el programador sabe que los posibles errores de redondeo no importan y la baja latencia es crítica. [23]
Este ejemplo resalta nuevamente una diferencia fundamental y crítica entre los verdaderos procesadores vectoriales y aquellos procesadores SIMD, incluidas la mayoría de las GPU comerciales, que están inspirados en características de los procesadores vectoriales.
En comparación con cualquier procesador SIMD que se autodenomine procesador vectorial, la reducción de orden de magnitud en el tamaño del programa es casi impactante. Sin embargo, este nivel de elegancia a nivel ISA tiene un precio bastante alto a nivel de hardware:
En general, entonces, existe la opción de tener
Estas marcadas diferencias son las que distinguen a un procesador vectorial de uno que tiene SIMD.
Si bien muchas ISA SIMD toman prestada o se inspiran en la lista siguiente, las características típicas que tendrá un procesador vectorial son: [24] [25] [26]
x[i] = y[i] + x[i-1]
donde la reducción tiene la formax = y[0] + y[1]… + y[n-1]
Dado que muchas aplicaciones de sombreado 3D necesitan operaciones trigonométricas , así como vectores cortos para operaciones comunes (RGB, ARGB, XYZ, XYZW), las GPU modernas suelen tener soporte para lo siguiente, además de lo que se encuentra en los procesadores vectoriales:
En ARM SVE2 y RISC-V se introdujo el concepto de cargas vectoriales secuenciales especulativas. ARM SVE2 tiene un registro especial llamado "First Fault Register", [35] donde RVV modifica (trunca) la longitud del vector (VL). [36]
El principio básico de ffirst es intentar una carga vectorial secuencial grande, pero permitir que el hardware trunque arbitrariamente la cantidad real cargada a la cantidad que se ejecutaría sin generar un error de memoria o simplemente a una cantidad (mayor que cero) que sea más conveniente. El factor importante es que las instrucciones posteriores reciben una notificación o pueden determinar exactamente cuántas cargas se ejecutaron correctamente, utilizando esa cantidad para realizar únicamente el trabajo sobre los datos que realmente se han cargado.
Comparemos esta situación con SIMD, que tiene un ancho de carga fijo (inflexible) y un ancho de procesamiento de datos fijo, incapaz de lidiar con cargas que cruzan los límites de página, e incluso si lo hiciera, no podría adaptarse a lo que realmente tuvo éxito; sin embargo, paradójicamente, si el programa SIMD intentara siquiera averiguar de antemano (en cada bucle interno, cada vez) qué podría tener éxito de manera óptima, esas instrucciones solo servirían para obstaculizar el rendimiento porque, por necesidad, serían parte del bucle interno crítico.
Esto comienza a dar una pista de la razón por la que ffirst es tan innovador, y se ilustra mejor con memcpy o strcpy cuando se implementan con SIMD no ffirst no predicado de 128 bits estándar. Para IBM POWER9, la cantidad de instrucciones optimizadas manualmente para implementar strncpy supera las 240. [37] En cambio, la misma rutina strncpy en ensamblador RVV optimizado manualmente tiene apenas 22 instrucciones. [38]
El ejemplo SIMD anterior podría potencialmente fallar y fallar al final de la memoria, debido a los intentos de leer demasiados valores: también podría causar un número significativo de fallas de página o desalineadas al cruzar límites de manera similar. En contraste, al permitir que la arquitectura vectorial tenga la libertad de decidir cuántos elementos cargar, la primera parte de un strncpy, si comienza inicialmente en un límite de memoria subóptimo, puede devolver solo las cargas suficientes para que en iteraciones posteriores del bucle los lotes de lecturas de memoria vectorizadas estén alineados de manera óptima con los cachés subyacentes y las disposiciones de memoria virtual. Además, el hardware puede optar por usar la oportunidad para finalizar las lecturas de memoria de cualquier iteración de bucle dada exactamente en un límite de página (evitando una costosa segunda búsqueda de TLB), con la ejecución especulativa preparando la siguiente página de memoria virtual mientras los datos aún se procesan en el bucle actual. Todo esto lo determina el hardware, no el programa en sí. [39]
Sea r la razón de velocidad del vector y f la razón de vectorización. Si el tiempo que tarda la unidad vectorial en sumar una matriz de 64 números es 10 veces más rápido que su equivalente escalar, r = 10. Además, si el número total de operaciones en un programa es 100, de las cuales solo 10 son escalares (después de la vectorización), entonces f = 0,9, es decir, el 90% del trabajo lo realiza la unidad vectorial. De ello se deduce que la aceleración alcanzable es:
Por lo tanto, incluso si el rendimiento de la unidad vectorial es muy alto ( ), hay una aceleración menor que , lo que sugiere que la relación f es crucial para el rendimiento. Esta relación depende de la eficiencia de la compilación, como la adyacencia de los elementos en la memoria.