Entendendo SSE
Aprendendo sobre SIMD, SSE e registradores XMM.
Last updated
Aprendendo sobre SIMD, SSE e registradores XMM.
Last updated
Na computação existe um conceito de instrução chamado SIMD (Single Instruction, Multiple Data) que é basicamente uma instrução que processa múltiplos dados de uma única vez. Todas as instruções que vimos até agora processavam meramente um dado por vez, porém instruções SIMD podem processar diversos dados paralelamente. O principal objetivo das instruções SIMD é ganhar performance se aproveitando dos múltiplos núcleos do processador, a maioria das instruções SIMD foram implementadas com o intuito de otimizar cálculos comuns em áreas como processamento gráfico, inteligência artificial, criptografia, matemática etc.
A Intel criou a primeira versão do SSE (streaming SIMD extensions) ainda no IA-32 com o Pentium III, e de lá para cá já ganhou diversas novas versões que estendem a tecnologia adicionando novas instruções. Atualmente nos processadores mais modernos há as seguintes extensões: SSE, SSE2, SSE3, SSSE3 e SSE4.
Processadores da arquitetura x86 têm diversas tecnologias SIMD, a primeira delas foi o MMX nos processadores Intel antes mesmo do SSE. Além de haver diversas outras como AVX, AVX-512, FMA, 3DNow! (da AMD) etc.
Na arquitetura x86 existem literalmente milhares de instruções SIMD. Esteja ciente que esse tópico está longe de cobrir todo o assunto e serve meramente como conteúdo introdutório.
A tecnologia SSE adiciona novos registradores independentes de 128 bits de tamanho cada. Em todos os modos de operação são adicionados oito novos registradores XMM0 até XMM7, e em 64-bit também há mais oito registradores XMM8 até XMM15 que podem ser acessados usando o prefixo REX. Isso dá um total de 16 registradores em 64-bit e 8 registradores nos outros modos de operação.
Esses registradores podem armazenar vários dados diferentes de mesmo tipo/tamanho, conforme demonstra tabela abaixo:
Esses são os tipos empacotados (packed), onde em um único registrador há vários valores de um mesmo tipo. Existem instruções SIMD específicas que executam operações packed onde elas trabalham com os vários dados armazenados no registrador ao mesmo tempo. Em contraste existem também as operações escalares (scalar) que operam com um único dado (unpacked) no registrador, onde esse dado estaria armazenado na parte menos significativa do registrador.
Na convenção de chamada para x86-64 da linguagem C os primeiros argumentos float/double passados para uma função vão nos registradores XMM0, XMM1 etc. como valores escalares. E o retorno do tipo float/double fica no registrador XMM0 também como um valor escalar.
Na lista de instruções haverá códigos de exemplo disso.
As instruções adicionadas pela tecnologia SSE podem ser divididas em quatro grupos:
Instruções packed e scalar que lidam com números float.
Instruções SIMD com inteiros de 64 bits.
Instruções de gerenciamento de estado.
Instruções de controle de cache e prefetch.
A tabela abaixo lista a nomenclatura que irei utilizar para descrever as instruções SSE.
Para facilitar o entendimento irei usar o termo float para se referir aos números de ponto flutuante de precisão única, ou seja, 32 bits de tamanho e 23 bits de precisão. Já o termo double será utilizado para se referir aos números de ponto flutuante de dupla precisão, ou seja, de 64 bits de tamanho e 52 bits de precisão. Esses são os mesmos nomes usados como tipos na linguagem C.
Nomenclatura | Descrição |
xmm(n) | Indica qualquer um dos registradores XMM. |
float(n) | Indica N números floats em sequência na memória RAM. Exemplo: float(4) seriam 4 números float totalizando 128 bits de tamanho. |
double(n) | Indica N números double na memória RAM. Exemplo: double(2) que totaliza 128 bits. |
ubyte(n) | Indica N bytes não-sinalizados na memória RAM. Exemplo: ubyte(16) que totaliza 128 bits. |
byte(n) | Indica N bytes sinalizados na memória RAM. |
uword(n) | Indica N words (2 bytes) não-sinalizados na memória RAM. Exemplo: uword(8) que totaliza 128 bits. |
word(n) | Indica N words sinalizadas na memória RAM. |
dword(n) | Indica N double words (4 bytes) na memória RAM. |
qword(n) | Indica N quadwords (8 bytes) na memória RAM. |
reg32/64 | Registrador de propósito geral de 32 ou 64 bits. |
imm8 | Operando imediato de 8 bits de tamanho. |
As instruções SSE terminam com um sufixo de duas letras onde a penúltima indica se ela lida com dados packed ou scalar, e a última letra indica o tipo do dado sendo manipulado. Por exemplo a instrução MOVAPS onde o P indica que a instrução manipula dados packed, enquanto o S indica o tipo do dado como single-precision floating-point, ou seja, float de 32 bits de tamanho.
Já o D de MOVAPD indica que a instrução lida com valores do tipo double-precision floating-point (64 bits). Eis a lista de sufixos e seus respectivos tipos:
Sufixo | Tipo |
S | Single-precision float. Equivalente ao tipo float em C. |
D | Double-precision float. Equivalente ao tipo double em C. Ou inteiro doubleword (4 bytes) que seria um inteiro de 32 bits. |
B | Inteiro de um byte (8 bits). |
W | Inteiro word (2 bytes | 16 bits). |
Q | Inteiro quadword (8 bytes | 64 bits). |
Todas as instruções SSE que lidam com valores na memória exigem que o valor esteja em um endereço alinhado em 16 bytes, exceto as instruções que explicitamente dizem lidar com dados desalinhados (unaligned).
Caso uma instrução SSE seja executada com um dado desalinhado uma exceção #GP será disparada.