Mesa de mistura

Mesa de mistura

Exercício de aprendizagem

Introdução

SIMD: inteiros

Um conceito anterior apresentou valores de vírgula flutuante empacotados: vários números guardados num único registo xmm, sobre os quais se opera lane a lane e em paralelo. Os mesmos registos xmm também podem guardar inteiros empacotados.

Sintaxe

A maior parte do modelo SIMD de vírgula flutuante mantém-se inalterada para os inteiros empacotados:

  • Um registo de 128 bits é dividido em lanes
  • As instruções atuam sobre as lanes na mesma posição, em paralelo
  • Os operandos de memória seguem as mesmas regras de alinhamento de 16 bytes

No entanto, a sintaxe é ligeiramente diferente:

  1. Um prefixo p para indicar que a instrução opera sobre dados empacotados.
  2. A operação realizada, com o mesmo nome da sua equivalente não SIMD (também chamada escalar) (por exemplo, add, mul, etc.).
  3. Um sufixo para indicar o tamanho de cada lane.

Os inteiros empacotados têm quatro tamanhos de lane, e cada um tem o 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 de um tamanho e produzem lanes de outro tamanho. 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

Movimentos de memória

Duas instruções com nome de inteiros copiam 128 bits entre um registo xmm e a memória:

instrução descrição
movdqa copia inteiros empacotados de ou para uma localização alinhada
movdqu copia inteiros empacotados de ou para uma localização não alinhada

Comportam-se como movaps e movups: a movdqa dá erro num endereço desalinhado, enquanto a movdqu aceita qualquer um. Todas as quatro copiam 128 bits sem os interpretar. O par com nome de inteiros é usado com dados inteiros por convenção, não por obrigação.

Note

dq aqui significa double-qword, ou seja, 128 bits (16 bytes).

Adição e subtração

A adição e a 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 existe uma forma separada para valores com sinal e sem sinal. Em complemento para dois, a adição e a subtração produzem os mesmos bits, quer as lanes sejam lidas como valores com sinal ou como valores sem sinal, por isso uma só instrução serve para ambos. A interpretação fica ao teu critério, exatamente como acontece com add e sub escalares.

Estas instruções dão a volta em caso de overflow, tal como as suas equivalentes escalares. Uma lane de 8 bits guarda valores módulo 256, por isso um paddb de 200 + 100 produz 300 - 256 = 44, descartando os bits que não cabem.

Repara que esses bits extra não passam para a lane seguinte. Cada lane é processada separadamente das outras, mesmo que partilhem o mesmo registo.

Adição e subtração com saturação

O SIMD de inteiros acrescenta uma operação que o SIMD de vírgula 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 passa a ser o maior valor que a lane consegue guardar; abaixo do intervalo, passa a ser 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 limitação é o intervalo representável completo para um inteiro do tamanho e sinal correspondentes. Para um byte:

  • Os bytes sem sinal ficam limitados a [0, 255]: um paddusb de 200 + 100 dá 255, e um psubusb de 5 - 10 dá 0.
  • Os bytes com sinal ficam limitados a [-128, 127]: um paddsb de 100 + 50 dá 127.

A saturação é importante quando uma lane guarda uma quantidade limitada, como o canal de um pixel ou uma amostra de áudio. Dar a volta tornaria escuro um pixel com demasiado brilho, ao passo que a limitação o mantém no brilho máximo, que é o resultado que queres.

Note

A adição e a subtração com saturação só existem para lanes byte e word, não para dword ou qword.

Multiplicação

Multiplicar dois valores de N bits pode produzir um produto de 2N bits, mas a lane de destino só tem N bits de largura. As operações SIMD resolvem isto especificando que metade do produto guardar: os N bits inferiores ou os N bits superiores.

Para lanes de 16 bits, usam-se três instruções:

instrução significado
pmullw mul, lanes de 16 bits, guarda os 16 bits inferiores de cada produto
pmulhw mul, lanes de 16 bits, guarda os 16 bits superiores, operandos com sinal
pmulhuw mul, lanes de 16 bits, guarda os 16 bits superiores, operandos sem sinal

Os 16 bits inferiores de um produto são iguais quer os operandos sejam lidos como valores com sinal ou sem sinal, por isso há um único pmullw.

Os 16 bits superiores, no entanto, variam conforme o resultado tenha ou não sinal. É por isso que a multiplicação da metade superior tem formas separadas para multiplicação com sinal e sem sinal:

  1. pmulhw, para multiplicação com sinal.
  2. pmulhuw, com um u adicional, para multiplicação sem sinal.

Repara na sintaxe:

  1. Um p, para inteiro empacotado.
  2. A operação realizada, mul.
  3. Um h, para indicar que são selecionados os bits superiores («high») do resultado.
  4. Um u opcional, se o resultado deve ser interpretado como sem sinal (ou seja, não tem extensão de sinal).
  5. Por fim, o sufixo de tamanho w, para indicar que é uma operação sobre words (16 bits).

A multiplicação empacotada para words segue exatamente a regra acima. A multiplicação para dwords (32 bits) também segue a regra, mas só existe a variante que seleciona a metade inferior: pmulld.

Não existe nenhuma variante da multiplicação para dwords que selecione os bits superiores. No entanto, existem variantes que alargam a multiplicação, guardando o produto completo de 64 bits das lanes com índice par (ou seja, as lanes nas posições 0 e 2):

instrução significado
pmuludq multiplica os elementos de 32 bits com índice par, sem sinal, em 2 produtos completos de 64 bits
pmuldq multiplica os elementos de 32 bits com índice par, com sinal, em 2 produtos completos de 64 bits

Repara que a sintaxe usa dq, possivelmente com um u antes, para multiplicação sem sinal. Isto acontece porque as instruções recebem lanes dword e produzem lanes qword.

Divisão

Não existe divisão empacotada de inteiros. O código que precisa dela converte os valores para vírgula flutuante, divide e converte de volta.

Alargamento de inteiros

Existem equivalentes empacotados para movsx e movzx. Seguem a mesma sintaxe que mencionámos para as instruções que recebem dados com um tamanho de lane diferente do da saída:

pmovsxwd xmm0, xmm1   ; 4 words -> 4 dwords, sign-extended
pmovzxbw xmm0, xmm1   ; 8 bytes -> 8 words, zero-extended

Repara que o número de lanes é determinado pela largura maior (a da saída). A instrução lê esse número de lanes a partir da parte inferior da origem. É a mesma semântica que já vimos para as instruções cvt empacotadas entre floats de precisão simples e dupla.

Conversão entre inteiros e valores de vírgula flutuante

Existem instruções para converter entre inteiros de 32 bits com sinal e lanes de vírgula flutuante. Seguem a sintaxe habitual das 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
Caution

As instruções que usam pi para representar inteiros empacotados escrevem em registos mmx legados e estão efetivamente obsoletas no x86-64. Dá preferência às formas com dq, que usam registos xmm.

Os mesmos comentários feitos para a conversão escalar entre números de vírgula flutuante e inteiros são válidos aqui. Os números de vírgula flutuante são arredondados de acordo com um registo especial chamado MXCSR, cujo modo não podes assumir à entrada de uma função. Existe uma variante cvtt (com um t adicional) que trunca sempre o resultado.

Também é possível usar round para colocar os valores de vírgula flutuante empacotados num estado conhecido antes de converter. O valor de controlo do round é o mesmo que no round escalar e a sintaxe também é a mesma para valores de vírgula flutuante empacotados:

roundps xmm0, xmm1, 1 ; xmm0 = floor(xmm1), packed 32-bit floats
roundpd xmm2, xmm3, 2 ; xmm2 = ceil(xmm3), packed 64-bit floats

Instruções

Estás a escrever o núcleo digital de uma pequena mesa de mistura de 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 altifalante num determinado instante. Uma mesa de mistura processa um bloco inteiro de amostras de uma só vez, aplicando a mesma operação a cada uma.

Tens cinco tarefas, cada uma a operar sobre um bloco de amostras guardado na memória. Recebes os operandos através de endereços de memória e escreves o resultado através de um endereço de resultado.

Note

Os cálculos deste exercício devem ser feitos com instruções SIMD, não com operações escalares.

1. Misturar duas faixas

Por vezes, o estúdio grava instrumentos em faixas diferentes. Para os reproduzir nos mesmos altifalantes, somas as duas faixas numa só.

Implementa a função mix_tracks, que soma duas faixas, amostra a amostra. Cada faixa é um bloco de 8 amostras de 16 bits com sinal. Uma soma que ultrapasse o intervalo de 16 bits com sinal tem de saturar em vez de dar a volta.

Esta função recebe como argumentos, por esta ordem:

  • result: endereço de memória alinhado a 16 bytes de um buffer onde são escritas as 8 amostras misturadas.
  • 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 devolve nada.

2. Remover o bleed

Quando dois microfones gravam ao mesmo tempo, cada um capta uma cópia ténue da fonte do outro. A esta cópia indesejada chama-se bleed. Depois de medido, esse bleed tem de ser subtraído para limpar a faixa.

Implementa a função remove_bleed, que subtrai o bloco de bleed ao bloco da faixa, amostra a amostra. Cada bloco tem 8 amostras de 16 bits com sinal, e a subtração tem de saturar.

Esta função recebe como argumentos, por esta ordem:

  • result: endereço de memória onde são escritas as 8 amostras limpas (alinhamento não garantido).
  • track: endereço de memória alinhado a 16 bytes da faixa gravada, com 8 amostras de 16 bits com sinal.
  • bleed: endereço de memória do bleed a remover, com 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 devolve nada.

3. Combinar medidores

Por cima de cada canal está um medidor de nível, uma pequena barra cujo brilho mostra o volume desse canal. O painel frontal tem um medidor principal que deve brilhar tanto como os dois canais juntos. Somas os dois blocos de medidores para o alimentar.

Repara que um medidor aceso fica no brilho máximo em vez de voltar ao escuro, por isso a soma satura.

Implementa a função combine_meters, que soma dois blocos de medidores, amostra a amostra. Esta função recebe como argumentos, por esta ordem:

  • result: endereço de memória alinhado a 16 bytes onde são escritos os 16 valores combinados.
  • 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. Devem ser somados, saturando ao intervalo dos valores 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 devolve nada.

4. Aplicar um desvanecimento

A canção não deve parar de repente: deve ir-se desvanecendo no fim. Tens um bloco de coeficientes de ganho, um por amostra, que vão diminuindo cada vez mais.

Cada coeficiente é uma fração com denominador 65536. Por exemplo, se uma amostra tem o valor 16384, na verdade representa 16384 / 65536, que é 1 / 4. O maior valor utilizável, 32767, fica um pouco abaixo de metade.

Como 65536 é 2^16, multiplicar uma amostra pelo seu coeficiente e ficar com a metade superior do produto dá sample * gain / 65536.

Implementa a função apply_fade, que escala cada amostra pelo seu coeficiente. Esta função recebe como argumentos, por esta ordem:

  • result: endereço de memória alinhado a 16 bytes onde são escritas todas as amostras escaladas.
  • 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.

Repara que a faixa é 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 superiores 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}
Note

Ficar com a metade superior arredonda em direção a menos infinito. É por isso que 20000 * 32767 dá 9999, enquanto -20000 * 32767 dá -10000 e não -9999.

Esta função não devolve nada.

5. Atenuar uma faixa

O baterista foi gravado com um volume demasiado alto. Para baixar a faixa inteira, divides cada amostra por uma constante.

Implementa a função attenuate_track. Esta função recebe como argumentos, por esta ordem:

  • result: endereço de memória alinhado a 16 bytes onde são escritos os resultados, 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 iguais 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.

Repara 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á replicado para poder preencher as quatro vias de uma só vez.

O SIMD inteiro não tem divisão empacotada, por isso o trabalho é feito em vírgula flutuante empacotada e depois convertido de volta em inteiros de 32 bits empacotados, para serem guardados em result. Converte de volta truncando para zero, de modo que um quociente de 2.3 passa a 2 e -2.3 passa a -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 devolve nada.

Editar via GitHub A ligação abre numa nova janela ou separador
x86-64 Assembly Exercism

Estás pronto para começar Mesa de mistura?

Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.