Um conceito anterior introduziu valores de ponto flutuante empacotados: vários números mantidos em um único registrador xmm, operados lane a lane e em paralelo.
Os mesmos registradores xmm também podem conter inteiros empacotados.
A maior parte do modelo SIMD de ponto flutuante se aplica sem alterações a inteiros empacotados:
No entanto, a sintaxe é um pouco diferente:
p para indicar que a instrução opera sobre dados empacotados.add, mul, etc.).Inteiros empacotados têm quatro tamanhos de lane, e cada um tem seu próprio sufixo:
| largura da lane | bytes | lanes em 128 bits | sufixo |
|---|---|---|---|
| byte | 1 | 16 | b |
| word | 2 | 8 | w |
| dword | 4 | 4 | d |
| qword | 8 | 2 | q |
Por exemplo:
| instrução | significado |
|---|---|
paddb |
add empacotado, lanes de 8 bits (16 lanes) |
paddw |
add empacotado, lanes de 16 bits (8 lanes) |
paddd |
add empacotado, lanes de 32 bits (4 lanes) |
paddq |
add empacotado, lanes de 64 bits (2 lanes) |
Algumas instruções recebem lanes com um tamanho como entrada, mas com outro tamanho como saída. Elas seguem a mesma convenção geral, mas com dois sufixos de tamanho. O primeiro indica o tamanho da lane de entrada, o segundo indica o tamanho da lane de saída:
| instrução | significado |
|---|---|
pmovsxwd |
movsx empacotado, de lanes de 16 bits para lanes de 32 bits |
pmuldq |
mul empacotado, de lanes de 32 bits para lanes de 64 bits |
Duas instruções com nome de inteiro copiam 128 bits entre um registrador xmm e a memória:
| instrução | descrição |
|---|---|
movdqa |
copia inteiros empacotados de ou para um local alinhado |
movdqu |
copia inteiros empacotados de ou para um local desalinhado |
Elas se comportam como movaps e movups: movdqa gera falha em um endereço desalinhado, enquanto movdqu aceita qualquer um.
Todas as quatro copiam 128 bits sem interpretá-los.
O par com nome de inteiro é usado com dados inteiros por convenção, não por exigência.
dq aqui significa double-qword, ou seja, 128 bits (16 bytes).
Adição e subtração seguem a regra de nomenclatura:
paddb xmm0, xmm1 ; 16 lanes: each 8-bit, xmm0 += xmm1
paddw xmm2, xmm3 ; 8 lanes: each 16-bit, xmm2 += xmm3
psubd xmm4, xmm5 ; 4 lanes: each 32-bit, xmm4 -= xmm5
psubq xmm6, xmm7 ; 2 lanes: each 64-bit, xmm6 -= xmm7
Não há forma separada para com sinal e sem sinal.
Em complemento de dois, adição e subtração produzem os mesmos bits quer as lanes sejam lidas como com sinal ou sem sinal, então uma única instrução serve para ambas.
A interpretação é sua, exatamente como com add e sub escalares.
Essas instruções dão a volta em caso de overflow, como suas equivalentes escalares.
Uma lane de 8 bits armazena valores módulo 256, então um paddb de 200 + 100 produz 300 - 256 = 44, descartando os bits que não cabem.
Observe que esses bits extras não passam para a próxima lane. Cada lane é operada separadamente das outras, mesmo que compartilhem o mesmo registrador.
O SIMD de inteiros acrescenta uma operação que o SIMD de ponto flutuante não tem: adição e subtração com saturação, que limitam em vez de dar a volta. Um resultado acima do intervalo da lane se torna o maior valor que a lane pode armazenar; um resultado abaixo do intervalo se torna o menor.
As formas com saturação inserem s (com sinal) ou us (sem sinal) antes do sufixo de tamanho:
| instrução | significado |
|---|---|
paddsb |
add, com saturação, lanes de 8 bits com sinal |
paddusb |
add, com saturação, lanes de 8 bits sem sinal |
psubsw |
sub, com saturação, lanes de 16 bits com sinal |
psubusw |
sub, com saturação, lanes de 16 bits sem sinal |
O intervalo de saturação é o intervalo representável completo para um inteiro do tamanho e sinal correspondentes. Para um byte:
[0, 255]: um paddusb de 200 + 100 dá 255, e um psubusb de 5 - 10 dá 0.[-128, 127]: um paddsb de 100 + 50 dá 127.A saturação importa quando uma lane armazena uma quantidade limitada, como um canal de pixel ou uma amostra de áudio. O comportamento de dar a volta tornaria um pixel muito claro escuro, enquanto a saturação o mantém no brilho máximo, que é o resultado desejado.
Adição e subtração com saturação existem apenas para lanes de byte e word, não para dword ou qword.
Multiplicar dois valores de N bits pode produzir um produto de 2N bits, mas a lane de destino tem apenas N bits de largura. As operações SIMD resolvem isso especificando qual metade do produto manter, os N bits inferiores ou os N bits superiores.
Para lanes de 16 bits, três instruções são usadas:
| instrução | significado |
|---|---|
pmullw |
mul, lanes de 16 bits, mantém os 16 bits inferiores de cada produto |
pmulhw |
mul, lanes de 16 bits, mantém os 16 bits superiores, operandos com sinal |
pmulhuw |
mul, lanes de 16 bits, mantém os 16 bits superiores, operandos sem sinal |
Os 16 bits inferiores de um produto são os mesmos quer os operandos sejam lidos como com sinal ou sem sinal, então há uma única pmullw.
Os 16 bits superiores, no entanto, variam de acordo com o sinal do resultado. É por isso que a multiplicação da metade superior tem formas separadas para multiplicação com sinal e sem sinal:
pmulhw, para multiplicação com sinal.pmulhuw, com um u extra, para multiplicação sem sinal.Observe a sintaxe:
p, para inteiro empacotado.mul.h, para indicar que os bits superiores ("high") do resultado estão sendo selecionados.u opcional se o resultado deve ser interpretado como sem sinal (ou seja, não tem extensão de sinal).w, para indicar que é uma operação de word (16 bits).A multiplicação empacotada para words segue a regra acima exatamente.
A multiplicação de dword (32 bits) também segue a regra, mas há apenas a variante que seleciona a metade inferior: pmulld.
Não há variante para multiplicação de dword que selecione os bits superiores. No entanto, há variantes que ampliam a multiplicação, armazenando o produto completo de 64 bits das lanes de índice par (ou seja, as lanes nas posições 0 e 2):
| instrução | significado |
|---|---|
pmuludq |
multiplica os elementos de 32 bits de índice par, sem sinal, em 2 produtos completos de 64 bits |
pmuldq |
multiplica os elementos de 32 bits de índice par, com sinal, em 2 produtos completos de 64 bits |
Observe que a sintaxe usa dq, possivelmente com um u antes para multiplicação sem sinal.
Isso ocorre porque as instruções recebem lanes dword e produzem lanes qword.
Não há divisão inteira empacotada. Código que precise disso converte os valores para ponto flutuante, divide e converte de volta.
Existem equivalentes empacotados para movsx e movzx.
Elas seguem a mesma sintaxe que mencionamos para instruções que recebem entradas com um tamanho de lane diferente de sua saída:
pmovsxwd xmm0, xmm1 ; 4 words -> 4 dwords, sign-extended
pmovzxbw xmm0, xmm1 ; 8 bytes -> 8 words, zero-extended
Observe que o número de lanes é definido pela largura maior (de saída).
A instrução lê essa quantidade de lanes da parte inferior da origem.
Esta é a mesma semântica que já vimos para instruções cvt empacotadas entre ponto flutuante de precisão simples e dupla.
Existem instruções para converter entre inteiros de 32 bits com sinal e lanes de ponto flutuante.
Elas seguem a sintaxe usual para instruções cvt e cvtt, mas em vez de si um inteiro empacotado é representado por dq:
cvtdq2ps xmm0, xmm1 ; convert 32-bit signed integers in xmm1 to 32-bit floats in xmm0
cvtdq2pd xmm2, xmm3 ; convert 32-bit signed integers in xmm3 to 64-bit floats in xmm2
cvtps2dq xmm4, xmm5 ; convert 32-bit floats in xmm5 to 32-bit signed integers in xmm4
As instruções que usam pi para representar inteiros empacotados escrevem em registradores mmx legados e estão efetivamente obsoletas no x86-64.
Prefira as formas dq, que usam registradores xmm.
Os mesmos comentários feitos para conversão escalar entre números de ponto flutuante e inteiros são válidos aqui.
Números de ponto flutuante são arredondados de acordo com um registrador especial chamado MXCSR, cujo modo você não pode assumir na entrada da função.
Há uma variante cvtt (com um t extra) que sempre trunca o resultado.
Também é possível usar round para colocar valores de ponto flutuante empacotados em um estado conhecido antes de converter.
O valor de controle de arredondamento é o mesmo do round escalar e a sintaxe também é a mesma para valores de ponto flutuante empacotados:
roundps xmm0, xmm1, 1 ; xmm0 = floor(xmm1), packed 32-bit floats
roundpd xmm2, xmm3, 2 ; xmm2 = ceil(xmm3), packed 64-bit floats
Você está escrevendo o núcleo digital de uma pequena mesa de mixagem em um estúdio de gravação.
O som chega como um fluxo de amostras. Cada amostra é um número que descreve a posição do cone de um alto-falante em um instante. Uma mesa processa um bloco inteiro de amostras de uma vez, aplicando a mesma operação a cada uma.
Você tem cinco tarefas, cada uma operando sobre um bloco de amostras mantido na memória. Você recebe os operandos por meio de endereços de memória e escreve sua resposta por meio de um endereço de resultado.
Os cálculos deste exercício devem ser feitos com instruções SIMD, e não com operações escalares.
Às vezes, o estúdio grava instrumentos em faixas diferentes. Para reproduzi-los nos mesmos alto-falantes, você soma as duas faixas em uma só.
Implemente a função mix_tracks, que soma duas faixas, amostra por amostra.
Cada faixa é um bloco de 8 amostras de 16 bits com sinal.
Uma soma que estoura o intervalo de 16 bits com sinal deve saturar em vez de dar a volta.
Esta função recebe como argumentos, nesta ordem:
result: endereço de memória alinhado a 16 bytes para um buffer onde as 8 amostras mixadas são escritas.track_a: endereço de memória alinhado a 16 bytes da primeira faixa, com 8 amostras de 16 bits com sinal.track_b: endereço de memória alinhado a 16 bytes da segunda faixa, com 8 amostras de 16 bits com sinal.track_a = {10000, 30000, 20000, -30000, -20000, -100, 100, 0}
track_b = { 5000, 5000, 20000, -5000, -20000, 100, -100, 0}
result = {15000, 32767, 32767, -32768, -32768, 0, 0, 0}
Esta função não tem valor de retorno.
Quando dois microfones gravam ao mesmo tempo, cada um capta uma cópia fraca da fonte do outro. Essa cópia indesejada é chamada de vazamento. Depois que esse vazamento é medido, ele precisa ser subtraído de volta para limpar a faixa.
Implemente a função remove_bleed, que subtrai o bloco de vazamento do bloco da faixa, amostra por amostra.
Cada bloco tem 8 amostras de 16 bits com sinal, e a subtração deve saturar.
Esta função recebe como argumentos, nesta ordem:
result: endereço de memória onde as 8 amostras limpas são escritas (alinhamento não garantido).track: endereço de memória alinhado a 16 bytes da faixa gravada, 8 amostras de 16 bits com sinal.bleed: endereço de memória do vazamento a remover, 8 amostras de 16 bits com sinal (alinhamento não garantido).track = {10000, -30000, 5000, 32767, 0, 200, -200, 15000}
bleed = { 2000, 5000, 5000, -5, 0, 300, 300, 5000}
result = { 8000, -32768, 0, 32767, 0, -100, -500, 10000}
Esta função não tem valor de retorno.
Acima de cada canal fica um medidor de nível, uma barrinha cujo brilho mostra o quão alto está aquele canal. O painel frontal tem um medidor principal que deve brilhar tanto quanto os dois canais combinados. Você soma os dois blocos de medidores para acioná-lo.
Repare que um medidor brilhante permanece no brilho máximo em vez de voltar para o escuro, então a soma satura.
Implemente a função combine_meters, que soma dois blocos de medidores amostra por amostra.
Esta função recebe como argumentos, nesta ordem:
result: endereço de memória alinhado a 16 bytes onde os 16 valores combinados são escritos.meter_a: endereço de memória alinhado a 16 bytes do bloco de medidores do primeiro canal, com 16 valores de 8 bits sem sinal empacotados.meter_b: endereço de memória alinhado a 16 bytes do bloco de medidores do segundo canal, com 16 valores de 8 bits sem sinal empacotados.Cada bloco tem 16 valores de 8 bits sem sinal. Eles devem ser somados, saturando para o intervalo sem sinal.
meter_a = {200, 100, 50, 255, 0, 128, 10, 240, 1, 2, 3, 4, 5, 6, 7, 8}
meter_b = {100, 100, 200, 1, 0, 128, 10, 20, 1, 2, 3, 4, 5, 6, 7, 8}
result = {255, 200, 250, 255, 0, 255, 20, 255, 2, 4, 6, 8, 10, 12, 14, 16}
Esta função não tem valor de retorno.
A música não deve parar de repente, mas fazer um fade out no final. Você tem um bloco de coeficientes de ganho, um por amostra, que vão ficando cada vez mais baixos.
Cada coeficiente é uma fração expressa sobre 65536.
Por exemplo, se uma amostra tem valor 16384, ela na verdade expressa 16384 / 65536, que é 1 / 4.
O maior valor utilizável, 32767, é pouco menos que a metade.
Como 65536 é 2^16, multiplicar uma amostra pelo seu coeficiente e manter a metade alta do produto dá sample * gain / 65536.
Implemente a função apply_fade, que escala cada amostra pelo seu coeficiente.
Esta função recebe como argumentos, nesta ordem:
result: endereço de memória alinhado a 16 bytes onde todas as amostras escaladas são escritas.track: endereço de memória alinhado a 16 bytes da faixa, com n amostras de 16 bits com sinal.gains: endereço de memória alinhado a 16 bytes dos coeficientes de ganho, com n valores de 16 bits com sinal, cada um no intervalo de 0 a 32767.n: o número de amostras, que é sempre diferente de zero e múltiplo de 8, como um inteiro de 64 bits sem sinal.Repare que track é um array que contém n amostras de 16 bits com sinal, em que n nunca é zero e é sempre múltiplo de 8.
Para cada amostra, o resultado escalado são os 16 bits altos do produto com sinal: sample * gain.
track = {20000, 20000, 20000, 20000, -20000, -20000, -20000, -20000}
gains = {32767, 24576, 16384, 8192, 32767, 24576, 16384, 8192}
result = { 9999, 7500, 5000, 2500, -10000, -7500, -5000, -2500}
Manter a metade alta arredonda em direção ao infinito negativo.
É por isso que 20000 * 32767 escala para 9999, enquanto -20000 * 32767 escala para -10000 em vez de -9999.
Esta função não tem valor de retorno.
O baterista foi gravado alto demais. Para baixar a faixa inteira, você divide cada amostra por uma constante.
Implemente a função attenuate_track.
Esta função recebe como argumentos, nesta ordem:
result: endereço de memória alinhado a 16 bytes onde os resultados são escritos, como inteiros de 32 bits com sinal.samples: endereço de memória alinhado a 16 bytes das amostras, n valores de 16 bits com sinal.divisor: endereço de memória alinhado a 16 bytes de 4 cópias idênticas do divisor, como inteiros de 32 bits.n é o número de amostras, que é sempre diferente de zero e múltiplo de 4, como um inteiro de 64 bits sem sinal.Repare que samples é um array que contém n amostras de 16 bits com sinal, em que n nunca é zero e é sempre múltiplo de 4.
O divisor é fornecido como 4 cópias do mesmo inteiro de 32 bits, já replicadas para preencher as quatro vias de uma vez.
O SIMD de inteiros não tem divisão empacotada, então o trabalho é feito em ponto flutuante empacotado e depois convertido de volta para inteiros de 32 bits empacotados, para serem armazenados em result.
Converta de volta truncando em direção ao zero, então um quociente de 2.3 vira 2 e -2.3 vira -2.
samples = {300, 150, 90, -120, 100, 50, -50, 7}
divisor = {3, 3, 3, 3}
result = {100, 50, 30, -40, 33, 16, -16, 2}
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.