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 a uint8_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 um uint64_t
que possa ser convertido em ponteiro para acessar outros objetos, como o char*
funcionamento em ISO C / C ++.
use-os para obter dados do uint8_t em um uint64_t para uso com outras respostas:
// GNU C: gcc/clang/ICC but not MSVC
typedef uint64_t aliasing_u64 __attribute__((may_alias)); // still requires alignment
typedef uint64_t aliasing_unaligned_u64 __attribute__((may_alias, aligned(1)));
A outra maneira de realizar cargas seguras para serrilhado é com memcpy
a uint64_t
, que também remove o alignof(uint64_t
) requisito de alinhamento. Mas em ISAs sem cargas eficientes e desalinhadas, o gcc / clang não memcpy
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 como uint64_t
, ou de preferênciaalignas(16) uint64_t array[];
Isso garante alinhamento a pelo menos 8 bytes ou 16, se você especificar alignas
.
Como uint8_t
é quase certo unsigned char*
, é seguro acessar os bytes de umuint64_t
via uint8_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 porque char
é 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
ou uint8_t
ou qualquer outra coisa.
#include <stdint.h>
#include <stddef.h>
// assumes array is 16-byte aligned
void dec_mem_gnu(uint8_t *array) {
typedef uint8_t v16u8 __attribute__ ((vector_size (16), may_alias));
v16u8 *vecs = (v16u8*) array;
vecs[0] -= 1;
vecs[1] -= 1; // can be done in a loop.
}
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 esperado movdqa
/ paddb
. (Com um vetor tudo gerado por pcmpeqd 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
ou q
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 explorer
# GCC8.2 -O3 for RISC-V for vector_size(8) and only one vector
dec_mem_gnu(unsigned char*):
lui a4,%hi(.LC1) # generate address for static constants.
ld a5,0(a0) # a5 = load from function arg
ld a3,%lo(.LC1)(a4) # a3 = 0x7F7F7F7F7F7F7F7F
lui a2,%hi(.LC0)
ld a2,%lo(.LC0)(a2) # a2 = 0x8080808080808080
# above here can be hoisted out of loops
not a4,a5 # nx = ~x
and a5,a5,a3 # x &= 0x7f... clear high bit
and a4,a4,a2 # nx = (~x) & 0x80... inverse high bit isolated
add a5,a5,a3 # x += 0x7f... (128-1)
xor a5,a4,a5 # x ^= nx restore high bit or something.
sd a5,0(a0) # store the result
ret
Eu 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ênuosub
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!
# RISC-V clang (trunk) -O3
dec_mem_gnu(unsigned char*):
lb a6, 7(a0)
lb a7, 6(a0)
lb t0, 5(a0)
...
addi t1, a5, -1
addi t2, a1, -1
addi t3, a2, -1
...
sb a2, 7(a0)
sb a1, 6(a0)
sb a5, 5(a0)
...
ret