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.
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:
eq para igualdade e gt para maior que.
As outras variantes constroem-se combinando o resultado de uma destas.À exceção 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 num imediato como operando extra.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.
Dois idiomas práticos são:
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.
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 |
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.
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.
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.
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.
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:
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.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.
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.
Os cálculos deste exercício devem ser efetuados com instruções SIMD.
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.
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.
Cada pontuação recebe uma classificação de 1 a 3:
50.0.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.
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.
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.
Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.