Todas as operações SIMD que vimos até agora atuaram via a via.
O valor na via i do resultado é calculado a partir da via i das entradas.
No entanto, há muitas situações em que mover valores entre vias é necessário ou desejável. Por exemplo, as vias podem estar fora de ordem para o cálculo que precisamos de efetuar.
Existem muitas instruções SIMD que movem dados entre vias de formas diferentes.
Uma permutação reordena os bytes ou as vias de um registo, indo buscar cada via de destino a uma via de origem que pode estar em qualquer posição. Podem selecionar a mesma via para destinos diferentes, o que as torna também capazes de difundir valores.
As instruções de permutação comportam-se de forma diferente consoante o seu tamanho e o domínio de execução.
A instrução pshufd rearranja as quatro vias de 32 bits da sua origem no seu destino.
A seleção é um valor imediato de 8 bits, lido como quatro campos de 2 bits, um por via de destino. Cada campo seleciona qual das quatro vias de origem copiar para essa via de destino:
| campo | índice da via |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
A posição do campo no valor imediato, lida da direita para a esquerda, indica onde é inserida a via selecionada. Uma via de origem pode ser selecionada mais do que uma vez, e é assim que uma única via é difundida por todo o registo:
; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
Há duas instruções de permutação de vírgula flutuante, que seguem a mesma sintaxe geral: shuf + p + o sufixo de tamanho (s ou d).
Também usam um valor imediato para selecionar as vias.
shufps usa a mesma codificação de campos de 2 bits que as permutações inteiras anteriores.
No entanto, como há apenas 2 vias de 64 bits num operando de 128 bits, shufpd usa apenas uma codificação de campos de 1 bit.
Estas instruções diferem das suas equivalentes inteiras por irem buscar as vias a dois operandos em vez de um:
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1 ; xmm4 = {xmm4[1], xmm5[0]}
Como ambos os operandos alimentam o resultado, podem ser usadas para entrelaçar dois vetores num único passo. Se a origem e o destino forem o mesmo registo, todas as vias são retiradas dele.
pshufb é a permutação mais geral.
Apesar de pshufb e pshufd terem nomes muito semelhantes, que só diferem no sufixo de tamanho, realizam operações muito diferentes.
Em primeiro lugar, enquanto pshufd usa um valor imediato para selecionar as posições das vias, pshufb usa um vetor de controlo no operando de origem.
Esse vetor de controlo é um registo xmm ou um operando de memória de 16 bytes.
Para cada uma das 16 vias de destino, os quatro bits inferiores do vetor de controlo dão um índice de byte de origem, de 0 a 15.
Se a via i do vetor de controlo contiver o índice de byte de origem j, então a via j-th do destino é movida para a via i-th.
Isto realça uma segunda diferença entre as duas instruções.
Enquanto pshufd vai buscar as vias a permutar a um operando de origem diferente, pshufb faz a permutação no próprio destino.
section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
section .text
fn:
pshufb xmm0, [rel reverse]
; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
; ...
; in the end, the bytes in xmm0 are reversed
pshufb é ainda mais flexível do que isso: pode limpar qualquer uma das vias.
Se o bit mais significativo estiver definido numa via i do vetor de controlo, a via i do destino fica a zero.
Isto faz de pshufb, numa única instrução, tanto uma permutação arbitrária de bytes como uma limpeza seletiva.
Como pshufb atua com uma granularidade de byte, também pode ser usada para permutar vias de tamanhos diferentes, agrupando vias vizinhas.
Por exemplo, pode ser usada para permutar dwords:
section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes
section .text
fn:
movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
pshufb xmm0, [rel reverse_dwords]
; xmm0 = {49, 37, 25, 13}
As instruções de desempacotamento retiram vias de dois operandos e entrelaçam-nas, alternando entre os dois. Há variantes inteiras e de vírgula flutuante, e seguem em grande parte a mesma estrutura de sintaxe geral, com duas diferenças:
l ou h para indicar se atua na metade inferior (l) ou na metade superior (h) de cada operando.bw ou qdq (o sufixo de tamanho para 16 bytes é dq, como em movdqu).punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}
unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}
As instruções de empacotamento seguem a direção oposta, combinando as vias de dois operandos em vias de metade da largura no destino.
Estas instruções não têm o prefixo p.
Fora isso, a sintaxe combina elementos que já vimos:
pack.s ou um u para indicar se os valores de saída são com sinal ou sem sinal, respetivamente.s para saturante, como na aritmética saturante que vimos num conceito anterior.unpck).Como a operação de empacotamento reduz a largura, não precisa de um sufixo l ou h:
packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)
Estas instruções são saturantes, ou seja, os valores de saída são limitados ao intervalo mais estreito. Operam de dword para word e de word para byte. Não existe variante de qword para dword.
Repara que a entrada é sempre interpretada como um valor com sinal.
O tipo, com ou sem sinal, refere-se à saída.
Indica o intervalo ao qual os valores são limitados.
Por exemplo, packsswb limita ao intervalo de um byte com sinal, ou seja, [-128, 127].
As vias inferiores do resultado vêm do operando de destino e as vias superiores vêm da origem:
packusdw xmm0, xmm1 ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}
Não existe equivalente de vírgula flutuante para estas instruções.
Até agora, movemos sempre dados entre um registo SIMD e um registo de uso geral com movq/movd.
Estas instruções só conseguem escrever na via inferior de um registo SIMD, ou ler a partir dela, e, ao escrever, limpam todas as outras vias.
Há instruções que fazem o mesmo para qualquer via, e não apenas para a primeira, deixando as outras vias intactas:
Ambas seguem a sintaxe dos inteiros: p + insr/extr + o sufixo de tamanho (b, w, d ou q).
Em ambos os casos, o registo de uso geral tem normalmente 32 bits.
Só pinsrq e pextrq precisam de um operando de 64 bits.
Um operando de memória tem sempre o tamanho da operação: 8 bits para pinsrb/pextrb, 16 bits para pinsrw/pextrw, e assim por diante.
pinsrb xmm0, eax, 5 ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx
Repara que estas instruções, tal como movd e movq, limitam-se a mover bytes em bruto.
Isto significa que também podem ser usadas com valores de vírgula flutuante guardados na memória ou num registo de uso geral.
Escreves os ciclos internos de um pipeline de imagens de software, a fase que prepara texturas e compõe camadas antes de chegarem ao ecrã. O pipeline trabalha sobre os pixels bloco a bloco, aplicando a mesma operação a todo o bloco.
Um pixel é composto por quatro canais de 1 byte: vermelho, verde, azul e alfa, por essa ordem (RGBA).
Um bloco tem 4 pixels, ou seja, 16 bytes no total.
Tens cinco tarefas.
Recebes os operandos através de endereços de memória e escreves a resposta através de um endereço de resultado. Todos os endereços de memória neste exercício estão alinhados a 16 bytes.
Os cálculos deste exercício devem ser feitos com instruções SIMD, e não com operações escalares.
As texturas são guardadas como RGBA, mas o framebuffer em que este pipeline desenha espera cada pixel na ordem BGRA: os canais vermelho e azul trocados, os canais verde e alfa mantidos no lugar.
Uma imagem chega como uma sequência de blocos, e todos os blocos são convertidos da mesma forma.
Implementa a função to_display_order, que converte uma imagem inteira de RGBA para BGRA, bloco a bloco.
Deves definir a máscara de controlo de reordenação de canais como uma constante empacotada em memória e reutilizá-la em todos os blocos.
Esta função recebe como argumentos, por esta ordem:
result: endereço de memória de um buffer onde são escritos os blocos convertidos, 16 bytes por bloco.pixels: endereço de memória dos blocos de origem, 4 pixels por bloco, cada pixel com 4 bytes na ordem RGBA.block_count: o número de blocos, sempre maior do que 0.pixels = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}
Esta função não devolve nenhum valor.
Para limpar uma região ou pintar uma área uniforme, o pipeline escreve uma única cor em cada pixel da região.
Implementa a função fill_region, que preenche uma região de block_count blocos com cópias de uma cor.
Esta função recebe como argumentos, por esta ordem:
result: endereço de memória de um buffer onde são escritos os blocos preenchidos, 16 bytes por bloco.color: endereço de memória de um pixel, 4 bytes na ordem RGBA.block_count: o número de blocos a preencher, sempre maior do que 0.color = {18, 52, 86, 120}
block_count = 2
result = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}
Esta função não devolve nenhum valor.
É preciso juntar duas camadas de um único canal num só buffer, com as respetivas amostras entrelaçadas. O resultado alterna entre uma amostra da primeira camada e uma da segunda.
Implementa a função weave_scanlines, que entrelaça duas linhas de 16 amostras cada numa única linha de 32 amostras.
Esta função recebe como argumentos, por esta ordem:
result: endereço de memória de um buffer onde são escritas as 32 amostras entrelaçadas.first: endereço de memória da primeira linha, 16 amostras, cada uma um valor de 8 bits.second: endereço de memória da segunda linha, 16 amostras, cada uma um valor de 8 bits.first = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}
Esta função não devolve nenhum valor.
Uma passagem de brilho escala cada amostra com precisão de trabalho de 16 bits, para que uma amostra demasiado clara possa ultrapassar 255 e uma diferença possa ficar abaixo de 0.
A fase final reduz esses valores de trabalho novamente para amostras de 8 bits para visualização, limitando tudo o que esteja abaixo de 0 a 0 e tudo o que esteja acima de 255 a 255.
Implementa a função pack_samples, que reduz, por ordem, dois grupos de 8 valores de trabalho a uma única linha de 16 amostras.
Esta função recebe como argumentos, por esta ordem:
result: endereço de memória de um buffer onde são escritas as 16 amostras limitadas, cada uma um valor de 8 bits.first: endereço de memória dos primeiros 8 valores de trabalho, cada um um inteiro com sinal de 16 bits.second: endereço de memória dos 8 valores de trabalho seguintes, cada um um inteiro com sinal de 16 bits.first = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200, 255, 255, 0, 100, 50, 255, 7, 0}
Esta função não devolve nenhum valor.
Os dados de texturas e de vértices chegam muitas vezes entrelaçados, com o x e o y de cada ponto juntos.
No entanto, é muitas vezes necessário tê-los separados para um processamento eficiente: todos os valores de x num vetor e todos os valores de y noutro.
Implementa a função split_coordinates, que separa quatro pontos (x, y) entrelaçados num vetor de coordenadas x e num vetor de coordenadas y.
Esta função recebe como argumentos, por esta ordem:
xs: endereço de memória de um buffer onde são escritas as 4 coordenadas x, 4 números de vírgula flutuante de 32 bits.ys: endereço de memória de um buffer onde são escritas as 4 coordenadas y, 4 números de vírgula flutuante de 32 bits.first: endereço de memória dos dois primeiros pontos, 4 números de vírgula flutuante de 32 bits, como {x0, y0, x1, y1}.second: endereço de memória dos dois pontos seguintes, 4 números de vírgula flutuante de 32 bits, como {x2, y2, x3, y3}.first = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}
Esta função não devolve nenhum valor.
Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.