Toda operação SIMD até agora agiu lane a lane.
O valor na lane i do resultado é calculado a partir da lane i das entradas.
No entanto, há muitas situações em que mover valores entre lanes é necessário ou desejado. Por exemplo, as lanes podem estar fora de ordem para a computação que precisamos realizar.
Há muitas instruções SIMD que movem dados entre lanes de maneiras diferentes.
Um embaralhamento reordena os bytes ou as lanes de um registrador, puxando cada lane de destino de uma lane de origem que pode estar em qualquer lugar. Eles podem selecionar a mesma lane para destinos diferentes, o que os torna capazes de também espalhar valores.
As instruções de embaralhamento se comportam de forma diferente de acordo com seu tamanho e domínio de execução.
A instrução pshufd reorganiza as quatro lanes de 32 bits de sua origem para seu destino.
A seleção é um imediato de 8 bits, lido como quatro campos de 2 bits, um por lane de destino. Cada campo seleciona qual das quatro lanes de origem copiar para aquela lane de destino:
| campo | índice da lane |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
A posição do campo no imediato, lida da direita para a esquerda, indica onde a lane selecionada é inserida. Uma lane de origem pode ser selecionada mais de uma vez, e é assim que uma única lane é espalhada por todo o registrador:
; 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 embaralhamento de ponto flutuante, seguindo a mesma sintaxe geral: shuf + p + o sufixo de tamanho (seja s ou d).
Elas também usam um imediato para selecionar as lanes.
shufps usa a mesma codificação de campo de 2 bits que os embaralhamentos inteiros anteriores.
No entanto, como há apenas 2 lanes de 64 bits em um operando de 128 bits, shufpd usa apenas uma codificação de campo de 1 bit.
Essas instruções diferem de suas equivalentes inteiras porque puxam as lanes de 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 os dois operandos alimentam o resultado, elas podem ser usadas para intercalar dois vetores em uma única etapa. Se a origem e o destino forem o mesmo registrador, todas as lanes são puxadas dele.
pshufb é o embaralhamento mais geral.
Embora pshufb e pshufd tenham nomes muito parecidos, diferindo apenas no sufixo de tamanho, eles realizam operações muito diferentes.
Primeiro, enquanto pshufd usa um imediato para selecionar posições de lane, pshufb usa um vetor de controle no operando de origem.
Esse vetor de controle é um registrador xmm ou um operando de memória de 16 bytes.
Para cada uma das 16 lanes de destino, os quatro bits baixos do vetor de controle fornecem um índice de byte de origem, de 0 a 15.
Se a lane i do vetor de controle contém o índice de byte de origem j, então a j-th lane da origem será movida para a i-th lane do destino.
Isso destaca uma segunda diferença entre as duas instruções.
Enquanto pshufd obtém as lanes de embaralhamento de um operando de origem diferente, pshufb realiza o embaralhamento 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 que isso: ele pode limpar qualquer uma das lanes.
Se o bit mais alto estiver definido em uma lane i do vetor de controle, então a lane i do destino é zerada.
Isso torna pshufb tanto uma permutação arbitrária de bytes quanto uma limpeza seletiva, em uma única instrução.
Como pshufb atua em granularidade de byte, ele também pode ser usado para embaralhar lanes de tamanhos diferentes, agrupando lanes vizinhas.
Por exemplo, ele pode ser usado para embaralhar 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 pegam lanes de dois operandos e as entrelaçam, alternando entre os dois. Há variantes inteiras e de ponto flutuante, e elas seguem em grande parte a mesma estrutura de sintaxe geral, com duas diferenças:
l ou h para indicar se ela atua na metade baixa (l) ou na metade alta (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 vão na direção oposta, combinando as lanes de dois operandos em lanes de metade da largura no destino.
Essas instruções não levam o prefixo p.
Fora isso, a sintaxe combina elementos que já vimos:
pack.s ou u para indicar se os valores de saída são com sinal ou sem sinal, respectivamente.s para saturante, como na aritmética saturante vista em um conceito anterior.unpck).Como a operação pack é de estreitamento, ela 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)
Essas instruções são saturantes, ou seja, os valores de saída são limitados ao intervalo mais estreito. Elas operam de dword para word e de word para byte. Não há variante de qword para dword.
Observe que a entrada é sempre interpretada como um valor com sinal.
O tipo, com sinal ou sem sinal, é para a saída.
Ele indica o intervalo a ser limitado.
Por exemplo, packsswb limita ao intervalo de um byte com sinal, ou seja, [-128, 127].
As lanes baixas do resultado vêm do operando de destino e as lanes altas, do operando de 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 há equivalente de ponto flutuante para essas instruções.
Até agora, sempre movemos dados entre um registrador SIMD e um registrador de uso geral usando movq/movd.
Essas instruções só podem escrever na lane baixa de um registrador SIMD, ou ler dela, e, ao escrever, limpam todas as outras lanes.
Há instruções que fazem o mesmo para qualquer lane, não apenas a primeira, deixando as outras lanes intactas:
Ambas seguem a sintaxe inteira: p + insr/extr + o sufixo de tamanho (b, w, d ou q).
Nos dois casos, o registrador de uso geral geralmente tem 32 bits de largura.
Apenas pinsrq e pextrq precisam de um operando de 64 bits.
Um operando de memória é sempre do 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
Observe que essas instruções, assim como movd e movq, apenas movem bytes brutos.
Isso significa que elas também podem ser usadas para valores de ponto flutuante armazenados na memória ou em um registrador de uso geral.
Você escreve os laços internos de um pipeline de imagens de software, o estágio que prepara texturas e compõe camadas antes que elas cheguem à tela. O pipeline trabalha com pixels um bloco de cada vez, aplicando a mesma operação ao bloco inteiro.
Um pixel é composto de quatro canais de 1 byte: vermelho, verde, azul e alfa, nessa ordem (RGBA).
Um bloco tem 4 pixels, ou seja, 16 bytes no total.
Você tem cinco tarefas.
Você recebe os operandos por meio de endereços de memória e escreve sua resposta por meio de um endereço de resultado. Todos os endereços de memória neste exercício são alinhados em 16 bytes.
As computações neste exercício devem ser realizadas usando instruções SIMD, não operações escalares.
As texturas são armazenadas como RGBA, mas o framebuffer no qual 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 cada bloco é convertido da mesma forma.
Implemente a função to_display_order, que converte uma imagem inteira de RGBA para BGRA, um bloco de cada vez.
Você deve definir a máscara de controle de reordenação de canais como uma constante empacotada na memória e reutilizá-la para cada bloco.
Esta função recebe como argumentos, nesta ordem:
result: endereço de memória de um buffer onde os blocos convertidos são escritos, 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 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 tem valor de retorno.
Para limpar uma região ou pintar um trecho uniforme, o pipeline escreve uma única cor em todos os pixels da região.
Implemente a função fill_region, que preenche uma região de block_count blocos com cópias de uma mesma cor.
Esta função recebe como argumentos, nesta ordem:
result: endereço de memória de um buffer onde os blocos preenchidos são escritos, 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 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 tem valor de retorno.
Duas camadas de canal único precisam ser mescladas em um único buffer com suas amostras entrelaçadas. O resultado alterna uma amostra da primeira camada, depois uma da segunda.
Implemente a função weave_scanlines, que entrelaça duas fileiras de 16 amostras cada em uma única fileira de 32 amostras.
Esta função recebe como argumentos, nesta ordem:
result: endereço de memória de um buffer onde as 32 amostras entrelaçadas são escritas.first: endereço de memória da primeira fileira, 16 amostras, cada uma um valor de 8 bits.second: endereço de memória da segunda fileira, 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 tem valor de retorno.
Uma etapa de brilho escala cada amostra em precisão de trabalho de 16 bits, de modo que uma amostra clara demais pode passar de 255 e uma diferença pode ficar abaixo de 0.
O estágio final estreita esses valores de trabalho de volta para amostras de 8 bits para exibição, limitando qualquer valor abaixo de 0 para 0 e qualquer valor acima de 255 para 255.
Implemente a função pack_samples, que estreita dois grupos de 8 valores de trabalho em uma fileira de 16 amostras, em ordem.
Esta função recebe como argumentos, nesta ordem:
result: endereço de memória de um buffer onde as 16 amostras limitadas são escritas, 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 tem valor de retorno.
Dados de textura e de vértices muitas vezes chegam entrelaçados, com o x e o y de cada ponto empacotados juntos.
No entanto, muitas vezes é necessário tê-los separados para um processamento eficiente: todos os valores de x em um vetor, todos os valores de y em outro.
Implemente a função split_coordinates, que separa quatro pontos (x, y) entrelaçados em um vetor de coordenadas x e um vetor de coordenadas y.
Esta função recebe como argumentos, nesta ordem:
xs: endereço de memória de um buffer onde as 4 coordenadas x são escritas, 4 números de ponto flutuante de 32 bits.ys: endereço de memória de um buffer onde as 4 coordenadas y são escritas, 4 números de ponto flutuante de 32 bits.first: endereço de memória dos dois primeiros pontos, 4 números de ponto flutuante de 32 bits, como {x0, y0, x1, y1}.second: endereço de memória dos dois pontos seguintes, 4 números de ponto 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 tem valor de retorno.
Crie sua conta no Exercism para aprender e dominar x86-64 Assembly com 22 conceitos130 exercícios e mentoria humana de verdade, tudo de graça.