Adição Vetorial com Instruções SSE e AVX em Assembly NASM
Para a soma de elementos correspondentes em vetores, são empregadas as seguintes instruções:
- paddb / vpaddb: Soma de bytes em 16 pistas (SSE/AVX-128) ou 32 pistas (AVX-256).
- paddw / vpaddw: Soma de words (16 bits) em 8 pistas (SSE/AVX-128) ou 16 pistas (AVX-256).
- paddd / vpaddd: Soma de double words (32 bits) em 4 pistas (SSE/AVX-128) ou 8 pistas (AVX-256).
- paddq / vpaddq: Soma de quad words (64 bits) em 2 pistas (SSE/AVX-128) ou 4 pistas (AVX-256).
A sintaxe geral para essas instruções é a seguinte:
paddb xmmdest, xmmsrc/mem128
vpaddb xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddb ymmdest, ymmsrc1, ymmsrc2/mem256
paddw xmmdest, xmmsrc/mem128
vpaddw xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddw ymmdest, ymmsrc1, ymmsrc2/mem256
paddd xmmdest, xmmsrc/mem128
vpaddd xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddd ymmdest, ymmsrc1, ymmsrc2/mem256
paddq xmmdest, xmmsrc/mem128
vpaddq xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddq ymmdest, ymmsrc1, ymmsrc2/mem256As instruções com dois operandos somam as pistas correspondentes dos dois operandos e armazenam o resultado no primeiro. Já as instruções com três operandos somam o segundo e o terceiro operando, guardando o resultado no primeiro.
O exemplo a seguir para Linux demonstra uma soma simples.
global main
extern printf
section .data
nums0 dd 1, 2, 4, 8
nums1 dd 2, 3, 5, 9
format_str db "%d, %d, %d, %d", 10, 0
section .text
main:
sub rsp, 8
movaps xmm0, [nums0]
movaps xmm1, [nums1]
paddd xmm0, xmm1 ; XMM0 = XMM0 + XMM1
movd esi, xmm0
psrldq xmm0, 4 ; Desloca xmm0 para a direita para obter o próximo número
movd edx, xmm0
psrldq xmm0, 4
movd ecx, xmm0
psrldq xmm0, 4
movd r8d, xmm0
mov rdi, format_str
xor rax, rax
call printf
add rsp, 8
retNeste caso, o registrador XMM0 conterá um vetor cujos elementos são a soma das pistas correspondentes dos dois registradores, resultando no vetor [3, 5, 9, 17]. Após a soma, o vetor é impresso no console para verificação.
Compilação e execução do programa:
$ nasm -f elf64 hello.asm -o hello.o $ gcc -static hello.o -o hello $ ./hello 3, 5, 9, 17
A seguir, a versão análoga do programa para Windows.
global main
extern printf
section .data
nums0 dd 1, 2, 4, 8
nums1 dd 2, 3, 5, 9
format_str db "%d, %d, %d, %d", 10, 0
section .text
main:
sub rsp, 40
movaps xmm0, [rel nums0]
movaps xmm1, [rel nums1]
paddd xmm0, xmm1 ; XMM0 = XMM0 + XMM1
movd edx, xmm0
psrldq xmm0, 4
movd r8d, xmm0
psrldq xmm0, 4
movd r9d, xmm0
psrldq xmm0, 4
movd eax, xmm0
mov dword [rsp+32], eax
mov rcx, format_str
call printf
add rsp, 40
retO Problema do Overflow
Essas instruções não afetam nenhum dos flags de status e, portanto, não sinalizam a ocorrência de overflow (significa que o resultado excedeu a capacidade do tipo de dado). Se um overflow ocorre durante a adição, o valor excedente é simplesmente descartado (comportamento de wrap-around), o que pode levar a resultados incorretos.
global main
extern printf
section .data
nums0 dw -32768, 0, 0, 0, 0, 0, 0, 0
nums1 dw -10, 0, 0, 0, 0, 0, 0, 0
format_str db "%d", 10, 0
section .text
main:
sub rsp, 8
movaps xmm0, [nums0]
movaps xmm1, [nums1]
paddw xmm0, xmm1 ; XMM0 = XMM0 + XMM1
movd esi, xmm0
movsx esi, si ; Extensão de sinal do word de 16 bits para 32 bits
mov rdi, format_str
xor rax, rax
call printf
add rsp, 8
retNo código acima, o primeiro elemento do vetor nums0 (-32768) é somado com o primeiro de nums1 (-10). O resultado esperado seria -32778, mas como esse valor está fora do intervalo de um word com sinal (-32768 a 32767), ocorre um overflow, e o resultado se torna 32758.
Resultado da execução:
$ nasm -f elf64 hello.asm -o hello.o $ gcc -static hello.o -o hello $ ./hello 32758
A responsabilidade de verificar se os operandos estão dentro de um intervalo válido recai sobre o programa. No entanto, as extensões SSE/AVX fornecem uma solução para este problema.
Adição com Aritmética de Saturação
A aritmética de saturação (saturation arithmetic) é uma alternativa que evita o wrap-around e é particularmente útil no processamento de multimídia (áudio, vídeo, imagens). As instruções a seguir implementam essa lógica:
- paddsb / vpaddsb: Soma de bytes com sinal e saturação.
- paddsw / vpaddsw: Soma de words com sinal e saturação.
- paddusb / vpaddusb: Soma de bytes sem sinal e saturação.
- paddusw / vpaddusw: Soma de words sem sinal e saturação.
Sua sintaxe é similar às instruções de adição padrão.
paddsb xmmdest, xmmsrc/mem128
vpaddsb xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddsb ymmdest, ymmsrc1, ymmsrc2/mem256
paddsw xmmdest, xmmsrc/mem128
vpaddsw xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddsw ymmdest, ymmsrc1, ymmsrc2/mem256
paddusb xmmdest, xmmsrc/mem128
vpaddusb xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddusb ymmdest, ymmsrc1, ymmsrc2/mem256
paddusw xmmdest, xmmsrc/mem128
vpaddusw xmmdest, xmmsrc1, xmmsrc2/mem128
vpaddusw ymmdest, ymmsrc1, ymmsrc2/mem256Na adição sem sinal, se o resultado exceder o valor máximo para o tipo de dado, ele é "saturado" ou "fixado" nesse valor máximo. Por exemplo, se a soma de dois bytes (unsigned) ultrapassa 255 (0xFF), o resultado é fixado em 255. De forma análoga, em operações com sinal, o resultado é fixado nos limites máximo (positivo) ou mínimo (negativo) do intervalo (por exemplo, de -128 a 127 para bytes com sinal).
Modificando o exemplo anterior para usar adição com saturação:
global main
extern printf
section .data
nums0 dw -32768, 2, -4, 8
nums1 dw -10, -3, 5, -9
format_str db "%d", 10, 0
section .text
main:
sub rsp, 8
movaps xmm0, [nums0]
movaps xmm1, [nums1]
paddsw xmm0, xmm1 ; XMM0 = XMM0 + XMM1
; XMM0 = [-32768, -1, 1, -1]
movd esi, xmm0
movsx esi, si ; Extensão de sinal
mov rdi, format_str
xor rax, rax
call printf
add rsp, 8
retNeste caso, a soma dos primeiros elementos (-32768 + -10) ainda resulta em um overflow negativo. No entanto, com a aritmética de saturação, o resultado é fixado no valor mínimo possível para um word com sinal, que é -32768. Este comportamento é frequentemente mais previsível e desejável.
$ nasm -f elf64 hello.asm -o hello.o $ gcc -static hello.o -o hello $ ./hello -32768
Adição Horizontal
As extensões SSE/AVX também fornecem instruções para "adição horizontal", que somam elementos adjacentes dentro do mesmo vetor.
- (v)phaddw: Adição horizontal de números de 16 bits.
- (v)phaddd: Adição horizontal de números de 32 bits.
- (v)phaddsw: Adição horizontal de números de 16 bits com saturação.
A sintaxe é análoga às outras instruções de adição.
Essas instruções somam pares de elementos adjacentes (words ou dwords) de ambos os operandos e armazenam as somas no registrador de destino.
phaddw
A instrução phaddw opera da seguinte forma:
temp[0-15] = xmmdest[0-15] + xmmdest[16-31] temp[16-31] = xmmdest[32-47] + xmmdest[48-63] temp[32-47] = xmmdest[64-79] + xmmdest[80-95] temp[48-63] = xmmdest[96-111] + xmmdest[112-127] temp[64-79] = xmmsrc[0-15] + xmmsrc[16-31] temp[80-95] = xmmsrc[32-47] + xmmsrc[48-63] temp[96-111] = xmmsrc[64-79] + xmmsrc[80-95] temp[112-127] = xmmsrc[96-111] + xmmsrc[112-127] xmmdest = temp
As 4 words inferiores do resultado vêm da soma de pares do primeiro operando, e as 4 words superiores vêm da soma de pares do segundo operando.
vphaddw (128 bits)
A versão vphaddw de 128 bits com três operandos funciona de maneira semelhante, zerando os 128 bits superiores do registrador YMM de destino.
xmmdest[0-15] = xmmsrc1[0-15] + xmmsrc1[16-31] xmmdest[16-31] = xmmsrc1[32-47] + xmmsrc1[48-63] ... xmmdest[64-79] = xmmsrc2[0-15] + xmmsrc2[16-31] ...
phaddd
A instrução phaddd para dwords segue o mesmo padrão:
temp[0-31] = xmmdest[0-31] + xmmdest[32-63] temp[32-63] = xmmdest[64-95] + xmmdest[96-127] temp[64-95] = xmmsrc[0-31] + xmmsrc[32-63] temp[96-127] = xmmsrc[64-95] + xmmsrc[96-127] xmmdest = temp
As versões AVX (vphaddd) de 128 e 256 bits estendem essa lógica para mais elementos, operando de forma similar às suas contrapartes de 16 bits.
Se ocorrer um overflow durante a adição horizontal com (v)phaddw e (v)phaddd, ele é simplesmente ignorado (wrap-around). Com a instrução de saturação phaddsw, qualquer overflow positivo resulta em 0x7FFF, e qualquer overflow negativo resulta em 0x8000.
Resumo
- As instruções
paddrealizam a soma paralela de elementos em vetores (bytes, words, dwords, qwords). - A adição padrão não sinaliza overflow, podendo levar a resultados incorretos.
- A aritmética de saturação, com as instruções
padd*sepadd*us, evita o wrap-around fixando o resultado nos valores mínimo ou máximo do tipo de dado. - A adição horizontal, com as instruções
phadd, soma elementos adjacentes dentro do mesmo vetor, sendo útil para operações de redução como a soma de todos os elementos de um vetor.