Se eu tiver um número inteiro de 64 bits, estou interpretando como uma matriz de números inteiros de 8 bits compactados com 8 elementos. Preciso subtrair a constante 1
de cada número inteiro compactado enquanto lida com o estouro sem que o resultado de um elemento afete o resultado de outro elemento.
Eu tenho esse código no momento e funciona, mas preciso de uma solução que faça a subtração de cada número inteiro de 8 bits em paralelo e não faça acessos à memória. No x86, eu poderia usar instruções SIMD como psubb
essa subtrai números inteiros de 8 bits em paralelo, mas a plataforma pela qual estou codificando não suporta instruções SIMD. (RISC-V neste caso).
Então, eu estou tentando fazer o SWAR (SIMD dentro de um registro) para cancelar manualmente a propagação entre bytes de a uint64_t
, fazendo algo equivalente a isso:
uint64_t sub(uint64_t arg) {
uint8_t* packed = (uint8_t*) &arg;
for (size_t i = 0; i < sizeof(uint64_t); ++i) {
packed[i] -= 1;
}
return arg;
}
Eu acho que você poderia fazer isso com operadores bit a bit, mas não tenho certeza. Estou procurando uma solução que não use instruções SIMD. Estou procurando uma solução em C ou C ++ que seja bastante portátil ou apenas a teoria por trás dela para que eu possa implementar minha própria solução.
Respostas:
Se você possui uma CPU com instruções SIMD eficientes, o SSE / MMX
paddb
(_mm_add_epi8
) também é viável. A resposta de Peter Cordes também descreve a sintaxe do vetor GNU C (gcc / clang) e a segurança para UB com alias estrito. Eu recomendo fortemente a revisão dessa resposta também.Fazer você mesmo
uint64_t
é totalmente portátil, mas ainda requer cuidados para evitar problemas de alinhamento e UB com alias estrito ao acessar umauint8_t
matriz com auint64_t*
. Você deixou essa parte fora de questão, começando com seus dados em umuint64_t
já, mas para o GNU C ummay_alias
typedef resolve o problema (consulte a resposta de Peter para isso oumemcpy
).Caso contrário, você poderá alocar / declarar seus dados
uint64_t
e acessá-losuint8_t*
quando quiser bytes individuais.unsigned char*
é permitido alias qualquer coisa para evitar o problema no caso específico de elementos de 8 bits. (Seuint8_t
existe, provavelmente é seguro assumir que é umunsigned char
.)Observe que isso é uma alteração de um algoritmo incorreto anterior (consulte o histórico de revisões).
Isso é possível sem loop para subtração arbitrária e fica mais eficiente para uma constante conhecida como
1
em cada byte. O principal truque é impedir a execução de cada byte, definindo o bit alto e, em seguida, corrija o resultado da subtração.Vamos otimizar um pouco a técnica de subtração fornecida aqui . Eles definem:
com
H
definido como0x8080808080808080U
(ou seja, os MSBs de cada número inteiro compactado). Para um decremento,y
é0x0101010101010101U
.Sabemos que
y
todos os seus MSBs estão limpos, para que possamos pular uma das etapas da máscara (ou seja,y & ~H
é a mesmay
do nosso caso). O cálculo prossegue da seguinte forma:x
como 1, para que um empréstimo não possa se propagar além do MSB para o próximo componente. Chame isso de entrada ajustada.0x01010101010101
da entrada corrigida. Isso não causa empréstimos entre componentes, graças à etapa 1. Chame isso de saída ajustada.A operação pode ser escrita como:
De preferência, isso é incorporado pelo compilador (use as diretivas do compilador para forçar isso) ou a expressão é escrita embutida como parte de outra função.
Casos de teste:
Detalhes de desempenho
Aqui está o assembly x86_64 para uma única chamada da função. Para um melhor desempenho, ele deve ser alinhado com a esperança de que as constantes possam viver em um registro o maior tempo possível. Em um loop restrito em que as constantes vivem em um registro, o decremento real leva cinco instruções: ou + não + e + adiciona + xor após a otimização. Não vejo alternativas que superariam a otimização do compilador.
Com alguns testes da IACA do seguinte trecho:
podemos mostrar que em uma máquina Skylake, a execução do decremento, xor e compare + jump pode ser realizada em pouco menos de 5 ciclos por iteração:
(Obviamente, no x86-64 você apenas carregaria ou
movq
em um registro XMMpaddb
, portanto, pode ser mais interessante ver como ele é compilado para um ISA como o RISC-V.)fonte
uint8_t
é permitido aliasuint8_t
dados. Os chamadores de sua função (que precisam incluiruint8_t
dados em auint64_t
) são os que precisam se preocupar com o aliasing estrito! Portanto, provavelmente o OP deve declarar / alocar matrizes apenasuint64_t
porquechar*
é permitido alias qualquer coisa no ISO C ++, mas não vice-versa.Para o RISC-V, você provavelmente está usando o GCC / clang.
Curiosidade: O GCC conhece alguns desses truques de bithack do SWAR (mostrados em outras respostas) e pode usá-los para você ao compilar código com vetores nativos do GNU C para destinos sem instruções SIMD de hardware. (Mas o clang para o RISC-V apenas o desenrola ingenuamente para operações escalares, então você precisa fazer isso sozinho se quiser um bom desempenho entre os compiladores).
Uma vantagem da sintaxe do vetor nativo é que, ao direcionar uma máquina com o hardware SIMD, ela será usada em vez de vetorizar automaticamente seu bithack ou algo horrível assim.
Isso facilita a gravação de
vector -= scalar
operações; a sintaxe Just Works, transmitindo implicitamente, ou seja, dividindo o escalar para você.Observe também que uma
uint64_t*
carga de auint8_t array[]
é UB com alias estrito; portanto, tenha cuidado com isso. (Veja também Por que o strlen da glibc precisa ser tão complicado para ser executado rapidamente? Re: tornando os bithacks do SWAR com alias estrito seguro em C puro). Você pode querer que algo assim declare umuint64_t
que possa ser convertido em ponteiro para acessar outros objetos, como ochar*
funcionamento em ISO C / C ++.use-os para obter dados do uint8_t em um uint64_t para uso com outras respostas:
A outra maneira de realizar cargas seguras para serrilhado é com
memcpy
auint64_t
, que também remove oalignof(uint64_t
) requisito de alinhamento. Mas em ISAs sem cargas eficientes e desalinhadas, o gcc / clang nãomemcpy
se alinha e otimiza quando não pode provar que o ponteiro está alinhado, o que seria desastroso para o desempenho.TL: DR: sua melhor aposta é declarar seus dados como
uint64_t array[...]
ou alocá-los dinamicamente comouint64_t
, ou de preferênciaalignas(16) uint64_t array[];
Isso garante alinhamento a pelo menos 8 bytes ou 16, se você especificaralignas
.Como
uint8_t
é quase certounsigned char*
, é seguro acessar os bytes de umuint64_t
viauint8_t*
(mas não vice-versa para uma matriz uint8_t). Portanto, neste caso especial em que o tipo de elemento estreito éunsigned char
, você pode contornar o problema de alias estrito porquechar
é especial.Exemplo de sintaxe de vetor nativo GNU C:
Os vetores nativos do GNU C sempre têm permissão para usar o alias com seu tipo subjacente (por exemplo,
int __attribute__((vector_size(16)))
podem com segurança alias,int
mas nãofloat
ouuint8_t
ou qualquer outra coisa.Para o RISC-V sem nenhum HW SIMD, você pode
vector_size(8)
expressar apenas a granularidade que pode usar com eficiência e fazer o dobro de vetores menores.Mas
vector_size(8)
compila de maneira estúpida para o x86 com o GCC e o clang: o GCC usa bithacks SWAR em registros de número inteiro GP, clang descompacta elementos de 2 bytes para preencher um registro XMM de 16 bytes e depois repete. (A MMX é tão obsoleta que o GCC / clang nem se importa em usá-lo, pelo menos não para x86-64.)Mas com
vector_size (16)
( Godbolt ) obtemos o esperadomovdqa
/paddb
. (Com um vetor tudo gerado porpcmpeqd same,same
). Como-march=skylake
ainda temos duas operações XMM separadas em vez de uma YMM, infelizmente os compiladores atuais também não "auto-vectorizam" as operações vetoriais em vetores mais amplos: /Para o AArch64, não é tão ruim de usar
vector_size(8)
( Godbolt ); O ARM / AArch64 pode trabalhar nativamente em blocos de 8 ou 16 bytes comd
ouq
registradores.Portanto, você provavelmente deseja
vector_size(16)
compilar se deseja desempenho portátil em x86, RISC-V, ARM / AArch64 e POWER . No entanto, alguns outros ISAs fazem SIMD em registros inteiros de 64 bits, como MIPS MSA, eu acho.vector_size(8)
facilita a análise do asm (apenas um registro de dados): Godbolt compiler explorerEu acho que é a mesma idéia básica que as outras respostas sem loop; impedindo o transporte e fixando o resultado.
Estas são 5 instruções da ULA, pior que a resposta principal, eu acho. Mas parece que a latência do caminho crítico é de apenas 3 ciclos, com duas cadeias de 2 instruções, cada uma levando ao XOR. A resposta de @Reinstate Monica - ζ - é compilada em uma cadeia dep de 4 ciclos (para x86). A taxa de transferência de loop de 5 ciclos é um gargalo, incluindo também um ingênuo
sub
no caminho crítico, e o loop afunila na latência.No entanto, isso é inútil com o clang. Ele nem adiciona e armazena na mesma ordem em que foi carregado, por isso não está fazendo um bom pipelining de software!
fonte
Eu apontaria que o código que você escreveu realmente vetoriza quando você começa a lidar com mais de um único uint64_t.
https://godbolt.org/z/J9DRzd
fonte
__vector_loop(index, start, past, pad)
construção que uma implementação poderia tratar comofor(index=start; index<past; index++)
[o que significa que qualquer implementação poderia processar código usando-a, apenas definindo uma macro], mas que teria uma semântica mais vaga para convidar um compilador para processar as coisas. qualquer tamanho de bloco de potência de dois atépad
, estendendo o início para baixo e terminando para cima se ainda não forem múltiplos do tamanho do bloco. Os efeitos colaterais dentro de cada pedaço seria unsequenced, e se umbreak
ocorre dentro do loop, outros representantes ...restrict
seja útil (e seria mais útil se a Norma reconhecesse um conceito de "pelo menos potencialmente baseado em" e depois definido "baseado em" e "pelo menos potencialmente baseado em" diretamente sem casos de canto patetas e impraticáveis) minha proposta também permitiria que um compilador executasse mais execuções do loop do que o solicitado - algo que simplificaria bastante a vetorização, mas para o qual o Padrão não faz nenhuma provisão.Você pode garantir que a subtração não transborde e conserte o bit alto:
fonte
splat(0x01)
esplat(0x80)
, em vez de obter uma da outra com um turno. Mesmo escrever dessa maneira na fonte godbolt.org/z/6y9v-u não impede o compilador de criar código melhor; apenas faz propagação constante.Não tenho certeza se é isso que você deseja, mas ele faz as 8 subtrações em paralelo entre si:
Explicação: A máscara de bit começa com 1 em cada um dos números de 8 bits. Nós concordamos com nosso argumento. Se tivéssemos um 1 nesse local, subtraímos 1 e temos que parar. Isso é feito configurando o bit correspondente como 0 em new_mask. Se tivéssemos um 0, definimos como 1 e temos que realizar o transporte, para que o bit permaneça 1 e deslocamos a máscara para a esquerda. É melhor você verificar se a geração da nova máscara funciona como pretendido, acho que sim, mas uma segunda opinião não seria ruim.
PS: Na verdade, não tenho certeza se a verificação de
mask_cp
não ser nulo no loop pode atrasar o programa. Sem ele, o código ainda estaria correto (uma vez que a máscara 0 simplesmente não faz nada) e seria muito mais fácil para o compilador desenrolar o loop.fonte
for
não vai funcionar em paralelo, você está confuso comfor_each
?Você pode fazer isso com operações bit a bit usando o descrito acima, e basta dividir seu número inteiro em partes de 8 bits para enviar 8 vezes para esta função. A parte a seguir foi retirada de Como dividir um número de 64 bits em oito valores de 8 bits? comigo adicionando na função acima
É válido C ou C ++, independentemente de como alguém se deparar com isso
fonte
for_each(std::execution::par_unseq,...
vez deNão tentando criar o código, mas para um decréscimo de 1, você pode diminuir pelo grupo de 8 1s e depois verificar se os LSBs dos resultados foram "invertidos". Qualquer LSB que não tenha sido alternado indica que ocorreu uma transferência dos 8 bits adjacentes. Deve ser possível elaborar uma sequência de ANDs / ORs / XORs para lidar com isso, sem ramificações.
fonte
Concentre o trabalho em cada byte completamente sozinho e coloque-o de volta onde estava.
fonte