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.
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:
eq para igualdade e gt para maior que.
Outras variantes são construídas compondo o resultado de uma dessas.Além do uso de um sufixo condicional específico nas comparações de inteiros, a sintaxe segue a mesma estrutura que já vimos:
p + cmp + condição + tamanho (b, w, d ou q).cmp + p + tamanho (s ou d).
A condição é passada em um imediato como um operando extra.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.
Dois truques úteis são:
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.
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 |
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.
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.
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.
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.
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:
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.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.
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.
Os cálculos deste exercício devem ser feitos com instruções SIMD.
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.
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.
Cada nota recebe uma classificação de 1 a 3:
50.0.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.
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.
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.
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.