Per RISC-V probabilmente stai usando GCC / clang.
Curiosità: GCC conosce alcuni di questi trucchi SWAR bithack (mostrati in altre risposte) e li può usare per te durante la compilazione di codice con vettori nativi GNU C per target senza istruzioni SIMD hardware. (Ma clang per RISC-V lo srotolerà ingenuamente in operazioni scalari, quindi devi farlo da solo se vuoi buone prestazioni tra i compilatori).
Uno dei vantaggi della sintassi vettoriale nativa è che quando si sceglie come target una macchina con hardware SIMD, la utilizzerà invece di vettorializzare automaticamente il tuo bithack o qualcosa di orribile come quello.
Semplifica le vector -= scalar
operazioni di scrittura ; la sintassi Just Works, che trasmette implicitamente aka splattando lo scalare per te.
Si noti inoltre che un uint64_t*
carico da auint8_t array[]
UB è aliasing rigoroso, quindi fai attenzione. (Vedi anche Perché lo strlen di glibc deve essere così complicato da eseguire rapidamente? Re: rendere i bithack SWAR aliasing rigoroso sicuri nella C pura). Potresti volere qualcosa del genere per dichiarare uint64_t
che puoi usare il puntatore-cast per accedere a qualsiasi altro oggetto, come il char*
funzionamento in ISO C / C ++.
usali per ottenere i dati uint8_t in uint64_t per usarli con altre risposte:
// 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)));
L'altro modo per eseguire carichi aliasing-safe è con memcpy
auint64_t
, che rimuove anche il alignof(uint64_t
) requisito di allineamento. Ma sugli ISA senza carichi non allineati efficienti, gcc / clang non memcpy
si allineano e non ottimizzano quando non possono provare che il puntatore è allineato, il che sarebbe disastroso per le prestazioni.
TL: DR: la soluzione migliore è dichiarare i tuoi datiuint64_t array[...]
o allocarli dinamicamente come uint64_t
, o preferibilmentealignas(16) uint64_t array[];
che garantisce l'allineamento ad almeno 8 byte, o 16 se specificato alignas
.
Poiché uint8_t
è quasi certamente unsigned char*
, è sicuro accedere ai byte di una uint64_t
via uint8_t*
(ma non viceversa per una matrice uint8_t). Quindi, per questo caso speciale in cui si trova il tipo di elemento strettounsigned char
, è possibile eludere il problema di alias rigoroso perché char
è speciale.
Esempio di sintassi vettoriale nativo GNU C:
I vettori nativi GNU C sono sempre autorizzati ad alias con il loro tipo sottostante (ad es. int __attribute__((vector_size(16)))
Alias sicuro int
ma no float
ouint8_t
o altro.
#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.
}
Per RISC-V senza alcun SIMD HW, è possibile utilizzare vector_size(8)
per esprimere solo la granularità che è possibile utilizzare in modo efficiente e fare il doppio dei vettori più piccoli.
Ma vector_size(8)
compila molto stupidamente per x86 sia con GCC che con clang: GCC usa i bithack SWAR nei registri di numeri interi GP, clang decomprime a elementi a 2 byte per riempire un registro XMM a 16 byte, quindi reimballa. (MMX è così obsoleto che GCC / clang non si preoccupano nemmeno di usarlo, almeno non per x86-64.)
Ma con vector_size (16)
( Godbolt ) otteniamo l'atteso movdqa
/ paddb
. (Con un vettore tutti generati da pcmpeqd same,same
). Con-march=skylake
abbiamo ancora due operazioni XMM separate invece di una YMM, quindi sfortunatamente anche i compilatori attuali non "auto-vettorizzano" le operazioni vettoriali in vettori più ampi: /
Per AArch64, non è poi così male da usare vector_size(8)
( Godbolt ); ARM / AArch64 può funzionare nativamente in blocchi di 8 o 16 byte con d
o q
registri.
Quindi probabilmente vuoi vector_size(16)
davvero compilare se vuoi prestazioni portatili su x86, RISC-V, ARM / AArch64 e POWER . Tuttavia, alcuni altri ISA fanno SIMD all'interno di registri interi a 64 bit, come penso MIPS MSA.
vector_size(8)
rende più facile guardare l'asm (solo un registro di dati): 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
Penso che sia la stessa idea di base delle altre risposte non cicliche; impedendo il trasporto quindi fissando il risultato
Queste sono 5 istruzioni ALU, peggio della risposta principale che penso. Ma sembra che la latenza del percorso critico sia di soli 3 cicli, con due catene di 2 istruzioni ciascuna che portano allo XOR. La risposta di @Reinstate Monica - ζ - viene compilata in una catena di dep a 4 cicli (per x86). Il throughput del ciclo a 5 cicli è strozzato includendo anche un ingenuosub
nel percorso critico e il ciclo esegue il collo di bottiglia alla latenza.
Tuttavia, questo è inutile con il clang. Non aggiunge nemmeno e memorizza nello stesso ordine in cui è stato caricato, quindi non sta nemmeno facendo una buona pipeline di 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