Quais são as melhores seqüências de instruções para gerar constantes vetoriais em tempo real?

“Melhor” significa poucas instruções (ou menos, se alguma instrução decodificar para mais de um uop). O tamanho do código de máquina em bytes é um desempatador para contagem de insensas igual.

A geração constante é, por sua própria natureza, o começo de uma nova cadeia de dependência, portanto, é incomum que a latência importe. Também é incomum gerar constantes dentro de um loop, de modo que as demandas de taxa de transferência e execução-porta também são praticamente irrelevantes.

Gerar constantes ao invés de carregá-las requer mais instruções (exceto para all-zero ou all-one), então consome um precioso espaço de cache uop. Isso pode ser um recurso ainda mais limitado que o cache de dados.

O excelente guia de assembly otimizada da Agner Fog aborda isso na Section 13.4 . A Tabela 13.10 tem sequências para gerar vetores, onde cada elemento é 0 , 1 , 2 , 3 , 4 , -1 ou -2 , com tamanhos de elemento de 8 a 64 bits. A Tabela 13.11 possui sequências para gerar alguns valores de ponto flutuante ( 0.0 , 0.5 , 1.0 , 1.5 , 2.0 , -2.0 e bitmasks para o bit de sinal.)

As sequências de Agner Fog usam apenas o SSE2, seja por design ou porque não foi atualizado por um tempo.

Quais outras constantes podem ser geradas com sequências curtas e não óbvias de instruções? (Extensões adicionais com diferentes contagens de deslocamento são óbvias e não “interessantes”.) Existem sequências melhores para gerar as constantes que Agner Fog lista?

Como mover os imediatos de 128 bits para os registradores XMM ilustra algumas maneiras de colocar uma constante arbitrária de 128b no stream de instruções, mas geralmente isso não é sensato (não economiza espaço e ocupa muito espaço de cache de uop).

Todos os zero: pxor xmm0,xmm0 (ou xorps xmm0,xmm0 , um byte de instrução mais curto).

Tudo-um: pcmpeqw xmm0,xmm0 . Este é o ponto de partida usual para gerar outras constantes, porque (como pxor ) quebra a dependência do valor anterior do registrador (exceto em CPUs antigas como K10 e pre-Core2 P6). Não há vantagem para a versão W sobre as versões de tamanho de elemento byte ou dword do pcmpeq em qualquer CPU nas tabelas de instruções do Agner Fog, mas o pcmpeqQ recebe um byte extra, é mais lento no Silvermont e requer o SSE4.1.

SO realmente não tem formatação de tabela , então vou listar adições à tabela 13.10 da Agner Fog, ao invés de uma versão melhorada. Desculpa. Talvez se essa resposta se tornar popular, usarei um gerador de tabelas ascii-art, mas esperamos que melhorias sejam implementadas em versões futuras do guia.


A principal dificuldade é de vetores de 8 bits, porque não há PSLLB

A tabela de Agner Fog gera vetores de elementos de 16 bits e usa packuswb para contornar isso. Por exemplo, pcmpeqw xmm0,xmm0 / psrlw xmm0,15 / psllw xmm0,1 / packuswb xmm0,xmm0 gera um vetor onde cada byte é 2 . (Esse padrão de mudanças, com diferentes contagens, é o principal meio de produzir a maioria das constantes para vetores mais amplos). Há um caminho melhor:

paddb xmm0,xmm0 (SSE2) funciona como um deslocamento à esquerda de um com granularidade de byte, portanto, um vetor de -2 bytes pode ser gerado com apenas duas instruções ( pcmpeqw / paddb ). paddw/d/q como left-shift-by-one para outros tamanhos de elemento salva um byte de código de máquina comparado a turnos, e geralmente pode ser executado em mais portas que um shift-imm.

pabsb xmm0,xmm0 (SSSE3) transforma um vetor de todas as pabsb xmm0,xmm0 ( -1 ) em um vetor de 1 byte , de modo que somente duas instruções são necessárias. Nós podemos gerar 2 bytes com pcmpeqw / paddb / pabsb . (Ordem de add vs abs não importa). pabs não precisa de um imm8, mas apenas salva bytes de código para outras larguras de elementos vs. deslocamento à direita quando ambos exigem um prefixo VEX de 3 bytes. Isso só acontece quando o registrador de origem é xmm8-15. ( vpabsb/w/d sempre requer um prefixo VEX de 3 bytes para VEX.128.66.0F38.WIG , mas o vpsrlw dest,src,imm pode usar um prefixo VEX de 2 bytes para seu VEX.NDD.128.66.0F.WIG ).

Nós podemos realmente salvar instruções na geração de 4 bytes , também: pcmpeqw / pabsb / psllw xmm0, 2 . Todos os bits que são deslocados pelos limites de byte pelo deslocamento da palavra são zero, graças ao pabsb . Obviamente, outras contagens de deslocamento podem colocar o único bit de conjunto em outros locais, incluindo o bit de sinal para gerar um vetor de -128 (0x80) bytes . Observe que o pabsb não é destrutivo (o operando de destino é somente gravação e não precisa ser o mesmo que a origem para obter o comportamento desejado). Você pode manter os todos ao redor como uma constante, ou como o início da geração de outra constante, ou como um operando de origem para o psubb (para incrementar por um).

Um vetor de 0x80 bytes também pode ser gerado (veja o parágrafo anterior) a partir de qualquer coisa que sature a -128, usando packsswb . por exemplo, se você já tem um vetor de 0xFF00 para outra coisa, apenas copie-o e use packsswb . Constantes carregadas da memory que saturam corretamente são alvos em potencial para isso.

Um vetor de 0x7f bytes pode ser gerado com pcmpeqw / psrlw xmm0, 9 / packuswb xmm0,xmm0 . Eu estou contando isso como “não óbvio”, porque a maior parte da natureza não me fez pensar em apenas gerá-lo como um valor em cada palavra e fazer o usual packuswb .

pavgb (SSE2) contra um registrador zerado pode deslocar para a direita um, mas somente se o valor for par. (Ele faz unsigned dst = (dst+src+1)>>1 para arredondamento, com precisão interna de 9 bits para o temporário.) Isso não parece ser útil para geração constante, embora, porque 0xff é ímpar: pxor xmm1,xmm1 / pcmpeqw xmm0,xmm0 / paddb xmm0,xmm0 / pavgb xmm0, xmm1 produz 0x7f bytes com mais um insn que shift / pack. Se um registro zerado já é necessário para outra coisa, o paddb / pavgb economiza um byte de instrução.


Eu testei essas seqüências. A maneira mais fácil é lançá-los em um .asm , montar / link e executar o gdb nele. layout asm , display /x $xmm0.v16_int8 para despejar depois de todas as instruções de etapa única e etapa única ( ni ou si ). No modo layout reg , você pode fazer tui reg vec para alternar para uma exibição de regs vetoriais, mas é quase inútil porque você não pode selecionar qual interpretação exibir (você sempre obtém todas elas, e não pode hscroll, e a colunas não se alinham entre registros). É excelente para regs / flags inteiros, no entanto.


Note que o uso destes com intrínsecos pode ser complicado. Compiladores não gostam de operar em variables ​​não inicializadas, então você deve usar _mm_undefined_si128() para dizer ao compilador o que você quis dizer. Ou talvez usando _mm_set1_epi32(-1) irá fazer com que seu compilador emita um pcmpeqd same,same . Sem isso, alguns compiladores terão xor zero de variables ​​vetoriais não inicializadas antes do uso ou até mesmo (MSVC) carregarão memory não inicializada da pilha.


Muitas constantes podem ser armazenadas mais compactamente na memory aproveitando-se do pmovzx ou pmovsx do SSE4.1 para zero ou extensão de sinal em tempo real. Por exemplo, um vetor 128b de {1, 2, 3, 4} como elementos de 32 bits poderia ser gerado com uma carga pmovzx de um local de memory de 32 bits. Os operandos de memory podem se micro-fundir com pmovzx , portanto não são necessários uops extra de domínio fundido. Previne o uso da constante diretamente como um operando de memory, no entanto.

C / C ++ intrinsics suporte para usar pmovz/sx como uma carga é terrível : há _mm_cvtepu8_epi32 (__m128i a) , mas nenhuma versão que leva um operando uint32_t * ponteiro. Você pode contorná-lo, mas é feio e a falha de otimização do compilador é um problema. Veja a questão relacionada para detalhes e links para os relatórios de bugs do gcc.

Com 256b e (não) em breve constantes 512b, a economia de memory é maior. Isso só importa muito se várias constantes úteis puderem compartilhar uma linha de cache.

O equivalente FP disso é VCVTPH2PS xmm1, xmm2/m64 , exigindo o sinalizador de recurso F16C (meia precisão). (Há também uma instrução de armazenamento que empacota a metade ou metade, mas nenhuma computação com a metade da precisão. É apenas uma otimização de largura de banda de memory / cache.)


Obviamente, quando todos os elementos são iguais (mas não adequados para gerar pshufd ), pshufd ou AVX vbroadcastps / AVX2 vpbroadcastb/w/d/q/i128 são úteis. pshufd pode pegar um operando fonte de memory, mas tem que ser 128b. movddup (SSE3) faz um carregamento de 64 bits, transmitido para preencher um registro 128b. Na Intel, ele não precisa de uma unidade de execução da ALU, apenas carrega a porta. (Da mesma forma, cargas de tamanho dword e maiores do AVX v[p]broadcast são manipuladas na unidade de carregamento, sem ALU).

Broadcasts ou pmovz/sx são excelentes para salvar o tamanho do executável quando você vai carregar uma máscara em um registrador para uso repetido em um loop. Gerar várias máscaras similares de um ponto de partida também pode economizar espaço, se levar apenas uma instrução.

Veja também For para um vetor SSE que possui todos os mesmos componentes, gerar na hora ou pré-compilar? que está perguntando mais sobre o uso do set1 intrínseco, e não está claro se ele está perguntando sobre constantes ou transmissões de variables.

Eu também experimentei alguns com saída do compilador para transmissões .


Se as falhas de cache forem um problema , dê uma olhada no seu código e veja se o compilador possui constantes _mm_set duplicadas quando a mesma function é embutida em diferentes chamadores. Também atente para as constantes que são usadas juntas (por exemplo, em funções chamadas uma após a outra) sendo espalhadas em diferentes linhas de cache. Muitas cargas dispersas para constantes são muito piores do que carregar muitas constantes de perto umas das outras.

pmovzx e / ou cargas de broadcast permitem que você pmovzx mais constantes em uma linha de cache, com pouca sobrecarga para carregá-las em um registrador. A carga não estará no caminho crítico, por isso, mesmo que seja necessário um extra extra, ela pode ter uma unidade de execução livre em qualquer ciclo em uma janela longa.

clang na verdade faz um bom trabalho com isso : constantes set1 separadas em funções diferentes são reconhecidas como idênticas, da mesma forma que literais de string idênticos podem ser mesclados. Observe que a saída de origem do clang asm parece mostrar cada function com sua própria cópia da constante, mas a desassembly binária mostra que todos esses endereços efetivos relativos ao RIP estão referenciando o mesmo local. Para as versões 256b das funções repetidas, o clang também usa vbroadcastsd para requerer apenas uma carga de 8B, às custas de uma instrução extra em cada function. (Isso é em -O3 , então, claramente, os desenvolvedores clang perceberam que o tamanho é importante para o desempenho, não apenas para -Os ). IDK porque não vai para baixo a uma constante 4B com vbroadcastss , porque isso deve ser igualmente rápido. Infelizmente, o vbroadcast não vem simplesmente de parte da constante 16B das outras funções usadas. Isso talvez faça sentido: uma versão AVX de algo provavelmente só poderia mesclar algumas de suas constantes com uma versão SSE. É melhor deixar as páginas de memory com constantes SSE completamente frias e ter a versão AVX mantendo todas as constantes juntas. Além disso, é um problema de correspondência de padrão mais difícil de ser tratado na assembly ou no tempo de link (no entanto, é feito. Eu não li todas as diretivas para descobrir qual delas permite a mesclagem).

O gcc 5.3 também mescla constantes, mas não usa cargas de transmissão para comprimir 32B constantes. Novamente, a constante 16B não se sobrepõe à constante 32B.