Pauta de notas

Pauta de notas

Exercício de aprendizagem

Introdução

SIMD: máscaras e condições

O código escalar baseia-se em flags definidas por várias instruções para ramificar em resposta a uma determinada condição. Os valores empacotados, no entanto, representam não um, mas muitos valores em paralelo. Uma única condição pode falhar numa lane e ser verdadeira noutra.

É por isso que o código SIMD é sem ramificações por predefinição.

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

Esta máscara pode depois ser combinada com operações bit a bit para filtrar lanes específicas.

Comparações empacotadas

Um cmp escalar é genérico no sentido em que é usado para definir várias flags ao mesmo tempo. Outra instrução pode depois consumir essas flags para ramificar ou para efetuar cálculos.

No entanto, como uma comparação empacotada verifica uma condição e calcula uma máscara ao mesmo tempo, não é genérica. É preciso indicar à comparação a condição exata que está a ser testada.

Há duas formas de o fazer:

  • Nas comparações de inteiros, a condição é indicada como sufixo: eq para igualdade e gt para maior que. As outras variantes constroem-se combinando o resultado de uma destas.
  • Nas comparações de vírgula flutuante, a condição é indicada codificada num imediato. Valores diferentes deste imediato correspondem a condições diferentes que estão a ser testadas.

À exceção 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 números de vírgula flutuante, cmp + p + tamanho (s ou d). A condição é passada num imediato como operando extra.
Comparações de inteiros

Como já foi referido, existem apenas 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, usa gt com os operandos trocados: a < b == b > a.

Repara que a comparação é com sinal. Para fazer uma comparação sem sinal, inverte o bit de topo de ambos os operandos. Isto pode ser feito com um XOR com uma máscara em que só o bit de topo está a 1.

Note

Dois idiomas práticos são:

  1. Fazer XOR de um registo consigo próprio para produzir tudo a zeros.
  2. Comparar um registo consigo próprio para produzir tudo a uns.

Por exemplo:

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

Tudo a zeros e tudo a uns são máscaras comuns para codificar "falso em todo o lado" e "verdadeiro em todo o lado", respetivamente. 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 vírgula flutuante

As lanes de vírgula flutuante usam uma forma 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 memorizar. Em todos os exemplos seguintes, o x em px pode ser s (números de vírgula flutuante de 32 bits) ou d (números de vírgula flutuante 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

Selecionar com uma máscara

Uma máscara codifica o resultado de uma condição. Pode depois ser usada para escolher, lane a lane, entre dois conjuntos de valores de acordo com essa condição. Retiramos a lane de um valor onde a máscara é verdadeira e de outro onde é 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

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

Este padrão é a forma empacotada da seleção sem ramificações. Todas as lanes são calculadas e é só a máscara que decide qual valor sobrevive, sem qualquer jcc.

Blends dedicados

Existem instruções que efetuam essa mesma seleção diretamente, lendo um bit por elemento de um registo de máscara. Chamam-se 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

Repara que a primeira instrução segue a sintaxe de inteiros, ao passo que as outras duas seguem a sintaxe de vírgula flutuante. No entanto, como estas instruções selecionam simplesmente bytes em bruto, qualquer uma delas pode ser usada tanto com inteiros como com números de vírgula flutuante.

Para cada elemento, o blend mantém o destino quando o bit de topo do elemento correspondente da máscara está a 0, e usa a origem quando está a 1. Só esse bit de topo é consultado, o que uma máscara de comparação satisfaz, dado que as suas lanes são tudo a uns ou tudo a zeros. O registo da 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 uma lane verdadeira são uns, pblendvb seleciona-os todos.

Note

Estas instruções acrescentam todas um v depois da operação que realizam (blend). Este v vem de variable, porque a seleção não é estática: depende de um registo.

Há também variantes sem v, que selecionam de acordo com um imediato. Seguem o mesmo padrão: selecionam a lane i se o bit i do imediato estiver a 1.

De uma máscara de volta ao escalar

Apesar de ser poderoso, o código SIMD não tem grande parte da flexibilidade do código escalar. Em muitas situações, é necessário passar de um registo empacotado de volta ao mundo das instruções escalares.

A família de instruções movmsk serve de ponte entre os dois mundos. Estas instruções extraem o bit de topo de cada lane para um registo de uso geral:

instrução recolhe largura do resultado
pmovmskb bit de topo de cada um dos 16 bytes 16 bits
movmskps bit de topo de cada um dos 4 dwords 4 bits
movmskpd bit de topo de cada um dos 2 qwords 2 bits

Se usadas depois de uma comparação, cada bit a 1 representa uma lane "verdadeira" e cada bit a 0, uma lane "falsa". Este resultado pode depois ser manipulado como habitualmente com instruções escalares. Por exemplo, um popcnt conta o número de correspondências e um tzcnt encontra a primeira.

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

Testar um vetor inteiro

Há também uma variante empacotada da instrução escalar test: ptest.

É semelhante ao seu equivalente escalar, na medida em que efetua uma operação AND entre dois operandos, sem os modificar. Ao contrário de test, ptest também efetua uma operação ANDN, negando o primeiro operando.

Assim, ptest pode ser visto como uma versão não destrutiva de pand e pandn que define flags de acordo com o resultado. Tal como estas duas instruções, ptest trata todo o registo SIMD como uma única lane e, por isso, não leva prefixo de tamanho.

Se o resultado de uma operação AND for 0, o ZF fica definido, e se a operação ANDN der 0, é o CF que fica definido. Isto significa que ptest pode ser usado para verificar tanto uma máscara de tudo a uns como uma de tudo a zeros:

  1. Usar ptest num registo consigo próprio define o ZF apenas se o registo for tudo a zeros. Isto imita o idioma escalar comum de usar test num registo consigo próprio para verificar se é 0.
  2. Usar ptest num registo com uma máscara de tudo a uns define o CF apenas se o registo for tudo a uns. Além disso, define o ZF apenas se o registo for tudo a zeros, o que permite verificar as duas máscaras ao mesmo tempo.
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 um ptest pode ser usado para ramificar ou em instruções sem ramificações como setcc ou cmovcc, como habitualmente.

Instruções

Trabalhas no posto de avaliação de uma escola, onde atribuis pontuações aos resultados da turma bloco a bloco.

Cada bloco contém 4 resultados e o posto aplica a mesma operação a todos os resultados do bloco. Uma pontuação é um número de vírgula flutuante de 32 bits. Vários passos trabalham com uma máscara: um bloco de 4 vias em que cada via é só de uns (um sim para esse resultado) ou só de zeros (um não).

Tens cinco tarefas. Recebes os operandos através de endereços de memória. Algumas tarefas escrevem a resposta num endereço de resultado, enquanto outras devolvem-na diretamente.

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

Note

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

1. Marca as pontuações acima do limiar

O primeiro passo avalia cada resultado em relação a um limiar. Um resultado passa a fasquia quando a sua pontuação é estritamente maior que o limiar. Qualquer pontuação inferior ou igual ao limiar não passa.

Implementa a função flag_above_threshold, que constrói uma máscara com uma via só de uns para cada pontuação acima do respetivo limiar e uma via só de zeros caso contrário.

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

  • result: endereço de memória de um buffer onde são escritas as 4 vias da máscara.
  • scores: endereço de memória das pontuações, com 4 números de vírgula flutuante normais de 32 bits (nunca NaN).
  • thresholds: endereço de memória do limiar de cada via, com 4 números de vírgula 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 devolvido.

2. Marca as pontuações perfeitas

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

Implementa a função flag_perfect, que constrói uma máscara com uma via só de uns para cada pontuação igual ao respetivo máximo e uma via só de zeros caso contrário.

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

  • result: endereço de memória de um buffer onde são escritas as 4 vias da máscara.
  • scores: endereço de memória das pontuações, com 4 números de vírgula flutuante normais de 32 bits (nunca NaN).
  • maxima: endereço de memória da nota máxima de cada via, com 4 números de vírgula 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 devolvido.

3. Atribui uma classificação

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

  • Classificação 1 para uma pontuação igual ou inferior ao limiar de aprovação de 50.0.
  • Classificação 2 para uma pontuação acima desse limiar, mas abaixo do máximo.
  • Classificação 3 para uma pontuação perfeita, ou seja, igual ao máximo.

Implementa a função assign_ranks, que escreve a classificação de cada pontuação.

Deves definir o limiar de aprovação e os valores das classificações como constantes empacotadas na memória. Podes reutilizar as funções das duas tarefas anteriores: uma pontuação é pelo menos classificação 2 quando está acima do limiar, e classificação 3 quando é igual ao máximo.

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

  • result: endereço de memória de um buffer onde são escritas as 4 classificações, cada uma um inteiro sem sinal de 32 bits.
  • scores: endereço de memória das pontuações, com 4 números de vírgula flutuante normais de 32 bits (nunca NaN).
  • maxima: endereço de memória da nota máxima de cada via, com 4 números de vírgula 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 devolvido.

4. Conta as reprovações

Ao longo do ano, cada aluno vai acumulando uma classificação total. O posto conta quantas classificações, em toda a turma, ficam abaixo de um limiar de aprovação, para planear quantas aulas extra realizar.

Implementa a função count_failures, que devolve quantas classificações, em todos os blocos, são estritamente inferiores ao limiar de aprovação. O limiar é dado como um bloco de 4 vias idênticas, por isso podes carregá-lo uma vez e reutilizá-lo em todos os blocos.

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

  • ranks: endereço de memória das classificações, um número inteiro de blocos de 4 vias, sendo cada classificação um inteiro sem sinal de 32 bits.
  • block_count: o número de blocos de 4 vias, sempre maior que 0.
  • pass_threshold: endereço de memória do limiar 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 devolve a contagem como um inteiro com sinal de 32 bits.

5. Passaram todos?

Antes de os registos serem arquivados, o posto verifica se a turma está limpa: passa quando nem um único resultado falhou em qualquer bloco.

Implementa a função all_passed, que devolve 1 se todos os alunos passaram e 0 caso contrário. Um aluno passa quando a via correspondente no array failing é só de zeros.

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

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

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

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

Estás pronto para começar Pauta de notas?

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