Planilha de notas

Planilha de notas

Exercício de aprendizagem

Introdução

SIMD: máscaras e condições

O código escalar depende de flags definidas por várias instruções para desviar em resposta a uma certa condição. Valores empacotados, porém, representam não um, mas muitos valores em paralelo. Uma mesma condição pode falhar para um lane e valer para outro.

É por isso que o código SIMD é sem desvios por padrão.

Em vez de depender de flags, as comparações empacotadas normalmente produzem uma máscara no operando de destino. Para cada lane, a comparação preenche o lane inteiro com uns quando ela é verdadeira e com zeros quando é falsa. Lido como um inteiro com sinal, um lane verdadeiro é -1 e um lane falso é 0.

Essa máscara pode então ser combinada com operações bit a bit para filtrar lanes específicos.

Comparações empacotadas

Um cmp escalar é genérico no sentido de que é usado para definir várias flags ao mesmo tempo. Outra instrução pode então consumir essas flags para desviar ou realizar cálculos.

Porém, como uma comparação empacotada tanto verifica uma condição quanto calcula uma máscara, ela não é genérica. É preciso informar à comparação exatamente qual condição está sendo testada.

Há duas formas de fazer isso:

  • Nas comparações de inteiros, a condição é dada como um sufixo: eq para igualdade e gt para maior que. Outras variantes são construídas compondo o resultado de uma dessas.
  • Nas comparações de ponto flutuante, a condição é codificada em um imediato. Valores diferentes desse imediato correspondem a condições diferentes sendo testadas.

Além do uso de um sufixo condicional específico nas comparações de inteiros, a sintaxe segue a mesma estrutura que já vimos:

  • Para inteiros, p + cmp + condição + tamanho (b, w, d ou q).
  • Para floats, cmp + p + tamanho (s ou d). A condição é passada em um imediato como um operando extra.
Comparações de inteiros

Como mencionado, só existem comparações de inteiros para igualdade e maior que:

instrução descrição
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq igualdade por lane
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq maior que com sinal por lane
movdqa  xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0

Para criar uma comparação de menor que, use gt com os operandos trocados: a < b == b > a.

Observe que a comparação é com sinal. Para fazer uma comparação sem sinal, inverta o bit mais alto dos dois operandos. Isso pode ser feito com um XOR com uma máscara em que apenas o bit mais alto está definido.

Note

Dois truques úteis são:

  1. Fazer XOR de um registrador com ele mesmo para produzir todos zeros.
  2. Comparar um registrador com ele mesmo para produzir todos uns.

Por exemplo:

pxor xmm4, xmm4    ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones

Todos zeros e todos uns são máscaras comuns para codificar "falso em todo lugar" e "verdadeiro em todo lugar", respectivamente. Elas também podem ser usadas para representar 0 empacotado ou -1 empacotado, que são valores sentinela comuns. Por exemplo, o NUL que marca o fim de uma string é um 0.

Comparações de ponto flutuante

Os lanes de ponto flutuante usam um formato diferente: uma única instrução, cmpps (e cmppd para lanes de 64 bits), com a condição como imediato:

movaps xmm0, [rel readings]
cmpps  xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]

O NASM também tem pseudo-ops que correspondem ao imediato correto e são mais fáceis de lembrar. Em tudo o que segue, o x em px pode ser s (floats de 32 bits) ou d (floats de 64 bits):

pseudo-op imediato comparação
cmpeqpx 0 a == b
cmpltpx 1 a < b
cmplepx 2 a <= b
cmpunordpx 3 a é NaN ou b é NaN
cmpneqpx 4 a != b
cmpnltpx 5 a >= b
cmpnlepx 6 a > b
cmpordpx 7 nem a nem b é NaN

Selecionando com uma máscara

Uma máscara codifica o resultado de uma condição. Ela pode então ser usada para escolher, lane a lane, entre dois conjuntos de valores de acordo com essa condição. Pegamos o lane de um valor onde a máscara é verdadeira e de outro onde ela é falsa:

; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0  ; xmm0 holds the mask, keep a copy
pand   xmm2, xmm3  ; xmm2 = a AND mask: lanes of a where mask is true
pandn  xmm0, xmm4  ; xmm0 = NOT mask AND b: lanes of b where mask is false
por    xmm2, xmm0  ; combine the two halves

Observe que a assimetria do pandn compensa aqui: a máscara fica no destino, é negada e seleciona a partir de b em uma única instrução.

Esse padrão é a forma empacotada da seleção sem desvios. Todo lane é calculado, e só a máscara decide qual valor sobrevive, sem nenhum jcc em lugar algum.

Blends dedicadas

Há instruções que realizam essa mesma seleção diretamente, lendo um bit por elemento de um registrador de máscara. Elas são chamadas de instruções blend:

instrução elemento origem da máscara
pblendvb byte xmm0 implícito
blendvps lane de 32 bits xmm0 implícito
blendvpd lane de 64 bits xmm0 implícito

Observe que a primeira instrução segue a sintaxe de inteiros, enquanto as outras duas seguem a sintaxe de floats. No entanto, como essas instruções simplesmente selecionam bytes brutos, qualquer uma delas pode ser usada tanto com inteiros quanto com floats.

Para cada elemento, o blend mantém o destino quando o bit mais alto do elemento de máscara correspondente está limpo, e pega a origem quando ele está definido. Apenas esse bit mais alto é consultado, o que uma máscara de comparação satisfaz, já que seus lanes são todos uns ou todos zeros. O registrador de máscara é sempre xmm0, que é implícito:

movaps   xmm0, [rel mask]  ; the selecting mask must be in xmm0
movaps   xmm1, [rel b]     ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a]     ; source: taken where the mask bit is set

Também é possível usar pblendvb para selecionar lanes a partir de uma máscara de comparação, para qualquer outro tamanho. Como todos os bytes de um lane verdadeiro são todos uns, pblendvb seleciona todos eles.

Note

Essas instruções acrescentam um v depois da operação que está sendo realizada (blend). Esse v significa variável, porque a seleção não é estática: ela depende de um registrador.

Também existem variantes sem o v, que selecionam de acordo com um imediato. Elas seguem o mesmo padrão, selecionando um lane i se o bit i do imediato estiver definido.

De uma máscara de volta ao escalar

Embora seja poderoso, o código SIMD não tem boa parte da flexibilidade do código escalar. Em muitas situações, é preciso sair de um registrador empacotado e voltar ao mundo das instruções escalares.

A família de instruções movmsk serve de ponte entre os dois mundos. Essas instruções extraem o bit mais alto de cada lane para um registrador de uso geral:

instrução reúne largura do resultado
pmovmskb o bit mais alto de cada um dos 16 bytes 16 bits
movmskps o bit mais alto de cada um dos 4 dwords 4 bits
movmskpd o bit mais alto de cada um dos 2 qwords 2 bits

Se usadas após uma comparação, cada bit definido representa um lane "verdadeiro" e cada bit limpo, um lane "falso". Esse resultado pode então ser manipulado como de costume com instruções escalares. Por exemplo, um popcnt conta o número de correspondências, e um tzcnt encontra a primeira.

O registrador de uso geral pode ter 32 ou 64 bits de largura.

Testando um vetor inteiro

Também existe uma variante empacotada da instrução escalar test: a ptest.

Ela é parecida com sua equivalente escalar no sentido de que realiza uma operação AND entre dois operandos, sem modificá-los. Diferente do test, a ptest também realiza uma operação ANDN, negando o primeiro operando.

Assim, a ptest pode ser vista como uma versão não destrutiva do pand e do pandn que define flags de acordo com o resultado. Assim como essas duas instruções, a ptest trata todo o registrador SIMD como um único lane e, por isso, não usa um prefixo de tamanho.

Se o resultado de uma operação AND for 0, a flag ZF é definida; e se a operação ANDN resultar em 0, é a CF que é definida. Isso significa que a ptest pode ser usada para verificar tanto uma máscara de todos uns quanto uma de todos zeros:

  1. Usar a ptest em um registrador com ele mesmo define ZF apenas se o registrador for todos zeros. Isso imita o truque escalar comum de usar test em um registrador com ele mesmo para verificar se é 0.
  2. Usar a ptest em um registrador com uma máscara de todos uns define CF apenas se o registrador for todos uns. Além disso, define ZF apenas se o registrador for todos zeros, o que permite verificar as duas máscaras de uma só vez.
pxor    xmm0, xmm0   ; all zeros
pcmpeqb xmm1, xmm1   ; all ones
pcmpeqb xmm2, xmm2

ptest xmm0, xmm0     ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1     ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1     ; CF is set only if xmm2 is all ones

O resultado de uma ptest pode ser usado para desviar ou em instruções sem desvios como setcc ou cmovcc, como de costume.

Instruções

Você comanda a estação de correção de uma escola, atribuindo notas aos resultados da turma bloco a bloco.

Cada bloco contém 4 resultados, e a estação aplica a mesma operação a todos os resultados do bloco. Uma nota é um número de ponto flutuante de 32 bits. Várias etapas trabalham com uma máscara: um bloco de 4 lanes, em que cada lane é ou todos uns (um sim para aquele resultado) ou todos zeros (um não).

Você tem cinco tarefas. Você recebe os operandos por endereços de memória. Algumas tarefas escrevem a resposta em um endereço de resultado, enquanto outras a retornam diretamente.

Todos os endereços de memória deste exercício estão alinhados em 16 bytes.

Note

Os cálculos deste exercício devem ser feitos com instruções SIMD.

1. Sinalizar as notas acima do limite

A primeira etapa avalia cada resultado em relação a um limite. Um resultado passa quando sua nota é estritamente maior que o limite. Qualquer nota menor que o limite, ou igual a ele, não passa.

Implemente a função flag_above_threshold, que monta uma máscara com uma lane de todos uns para cada nota acima do seu limite e uma lane de todos zeros caso contrário.

Esta função recebe como argumentos, nesta ordem:

  • result: endereço de memória de um buffer onde as 4 lanes da máscara são escritas.
  • scores: endereço de memória das notas, com 4 números de ponto flutuante normais de 32 bits (nunca NaN).
  • thresholds: endereço de memória do limite de cada lane, com 4 números de ponto flutuante normais de 32 bits (nunca NaN).
scores     = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result     = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

Esta função não tem valor de retorno.

2. Sinalizar as notas perfeitas

Um relatório separado destaca os resultados perfeitos, aqueles que alcançaram a nota máxima possível.

Implemente a função flag_perfect, que monta uma máscara com uma lane de todos uns para cada nota igual ao seu máximo e uma lane de todos zeros caso contrário.

Esta função recebe como argumentos, nesta ordem:

  • result: endereço de memória de um buffer onde as 4 lanes da máscara são escritas.
  • scores: endereço de memória das notas, com 4 números de ponto flutuante normais de 32 bits (nunca NaN).
  • maxima: endereço de memória da nota máxima de cada lane, com 4 números de ponto flutuante normais de 32 bits (nunca NaN).
scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}

Esta função não tem valor de retorno.

3. Atribuir uma classificação

Cada nota recebe uma classificação de 1 a 3:

  • Classificação 1 para uma nota igual ou abaixo do limite de aprovação de 50.0.
  • Classificação 2 para uma nota acima desse limite, mas abaixo do máximo.
  • Classificação 3 para uma nota perfeita, aquela que é igual ao máximo.

Implemente a função assign_ranks, que escreve a classificação de cada nota.

Você deve definir o limite de aprovação e os valores das classificações como constantes empacotadas na memória. As funções das duas tarefas anteriores podem ser reutilizadas: uma nota tem no mínimo classificação 2 quando está acima do limite, e classificação 3 quando é igual ao máximo.

Esta função recebe como argumentos, nesta ordem:

  • result: endereço de memória de um buffer onde as 4 classificações são escritas, cada uma um inteiro sem sinal de 32 bits.
  • scores: endereço de memória das notas, com 4 números de ponto flutuante normais de 32 bits (nunca NaN).
  • maxima: endereço de memória da nota máxima de cada lane, com 4 números de ponto flutuante normais de 32 bits (nunca NaN).
scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}

Esta função não tem valor de retorno.

4. Contar as reprovações

Ao longo do ano, cada estudante acumula um total de classificações. A estação conta quantas classificações, em toda a turma, ficam abaixo de um limite de aprovação, para planejar quantas aulas extras oferecer.

Implemente a função count_failures, que retorna quantas classificações, em todos os blocos, estão estritamente abaixo do limite de aprovação. O limite é dado como um bloco de 4 lanes idênticas, então você pode carregá-lo uma vez e reutilizá-lo em todos os blocos.

Esta função recebe como argumentos, nesta ordem:

  • ranks: endereço de memória das classificações, um número inteiro de blocos de 4 lanes, cada classificação um inteiro sem sinal de 32 bits.
  • block_count: o número de blocos de 4 lanes, sempre maior que 0.
  • pass_threshold: endereço de memória do limite de aprovação, com 4 inteiros idênticos de 32 bits.
ranks          = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count    = 2
pass_threshold = {2, 2, 2, 2}
// => 3

Esta função retorna a contagem como um inteiro com sinal de 32 bits.

5. Todos passaram?

Antes de arquivar os registros, a estação verifica se a turma está limpa: ela passa quando nenhum resultado foi reprovado em nenhum bloco.

Implemente a função all_passed, que retorna 1 se todos os estudantes passaram, e 0 caso contrário. Um estudante passa quando a lane correspondente no array failing é toda de zeros.

Esta função recebe como argumentos, nesta ordem:

  • failing: endereço de memória das máscaras de reprovação, um número inteiro de blocos de 4 lanes, cada lane toda de uns ou toda de zeros.
  • block_count: o número de blocos de 4 lanes, sempre maior que 0.
failing     = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
               0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1

Esta função retorna a resposta como um inteiro com sinal de 32 bits, 1 ou 0.

Editar via GitHub O link abre em uma nova janela ou aba
x86-64 Assembly Exercism

Tudo pronto para começar Planilha de notas?

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.