Pour RISC-V, vous utilisez probablement GCC / clang.
Fait amusant: GCC connaît certaines de ces astuces SWAR bithack (présentées dans d'autres réponses) et peut les utiliser pour vous lors de la compilation de code avec des vecteurs natifs GNU C pour des cibles sans instructions SIMD matérielles. (Mais clang pour RISC-V le déroulera naïvement en opérations scalaires, vous devez donc le faire vous-même si vous voulez de bonnes performances entre les compilateurs).
Un avantage de la syntaxe vectorielle native est que lors du ciblage d'une machine avec un SIMD matériel, il l'utilisera au lieu de vectoriser automatiquement votre bithack ou quelque chose d'horrible comme ça.
Il facilite l'écriture d' vector -= scalar
opérations; la syntaxe Just Works, diffusant implicitement aka éclaboussant le scalaire pour vous.
Notez également qu'une uint64_t*
charge provenant d'unuint8_t array[]
UB à alias strict, soyez donc prudent. (Voir aussi Pourquoi le strlen de glibc doit-il être si compliqué pour s'exécuter rapidement? Re: rendre le bithacks SWAR strict-aliasing sûr en C pur). Vous voudrez peut-être quelque chose comme ça pour déclarer un uint64_t
que vous pouvez casté par pointeur pour accéder à d'autres objets, comme la façon dont char*
fonctionne dans ISO C / C ++.
utilisez-les pour obtenir des données uint8_t dans un uint64_t à utiliser avec d'autres réponses:
// 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'autre façon de faire des charges de sécurité aliasing est avec memcpy
enuint64_t
, ce qui supprime également l' alignof(uint64_t
exigence d'alignement). Mais sur les ISA sans charges non alignées efficaces, gcc / clang ne s'alignent pas et ne s'optimisent pas memcpy
lorsqu'ils ne peuvent pas prouver que le pointeur est aligné, ce qui serait désastreux pour les performances.
TL: DR: votre meilleur pari est de déclarer vos données commeuint64_t array[...]
ou de les allouer dynamiquement commeuint64_t
, ou de préférencealignas(16) uint64_t array[];
Cela garantit l'alignement sur au moins 8 octets, ou 16 si vous spécifiez alignas
.
Puisque uint8_t
c'est presque certainement unsigned char*
, il est sûr d'accéder aux octets d'une uint64_t
viauint8_t*
(mais pas l'inverse pour un tableau uint8_t). Donc, pour ce cas spécial où le type d'élément étroit est unsigned char
, vous pouvez contourner le problème d'alias strict car il char
est spécial.
Exemple de syntaxe vectorielle native GNU C:
Les vecteurs natifs GNU C sont toujours autorisés à alias avec leur type sous-jacent (par exemple, int __attribute__((vector_size(16)))
peuvent alias en toute sécurité, int
mais pas float
ouuint8_t
ou autre chose.
#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.
}
Pour RISC-V sans HW SIMD, vous pouvez utiliser vector_size(8)
pour exprimer uniquement la granularité que vous pouvez utiliser efficacement et faire deux fois plus de vecteurs plus petits.
Mais vector_size(8)
compile très bêtement pour x86 avec GCC et clang: GCC utilise des bithacks SWAR dans les registres d'entiers GP, clang décompresse en éléments de 2 octets pour remplir un registre XMM de 16 octets puis recompresse. (MMX est tellement obsolète que GCC / clang ne prend même pas la peine de l'utiliser, du moins pas pour x86-64.)
Mais avec vector_size (16)
( Godbolt ) on obtient le movdqa
/ attendu paddb
. (Avec un vecteur tout-en-un généré par pcmpeqd same,same
). Avec-march=skylake
nous obtenons toujours deux opérations XMM distinctes au lieu d'un YMM, donc malheureusement les compilateurs actuels ne "vectorisent" pas automatiquement les opérations vectorielles en vecteurs plus larges: /
Pour AArch64, ce n'est pas si mal à utiliser vector_size(8)
( Godbolt ); ARM / AArch64 peut fonctionner de manière native en blocs de 8 ou 16 octets avec d
ou q
registres.
Donc, vous voulez probablement vector_size(16)
compiler avec si vous voulez des performances portables sur x86, RISC-V, ARM / AArch64 et POWER . Cependant, certains autres ISA font SIMD dans des registres entiers 64 bits, comme MIPS MSA je pense.
vector_size(8)
facilite la lecture de l'asm (un seul registre de données): 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
Je pense que c'est la même idée de base que les autres réponses sans boucle; empêchant le report puis fixant le résultat.
Ceci est 5 instructions ALU, pire que la réponse du haut je pense. Mais il semble que la latence du chemin critique ne soit que de 3 cycles, avec deux chaînes de 2 instructions menant chacune au XOR. La réponse de @Reinstate Monica - ζ - se compile en une chaîne dep à 4 cycles (pour x86). Le débit de boucle à 5 cycles est goulot d'étranglement en incluant également unsub
sur le chemin critique, et la boucle fait goulot d'étranglement sur la latence.
Cependant, cela est inutile avec clang. Il n'ajoute et ne stocke même pas dans le même ordre qu'il a chargé, donc il ne fait même pas de bons pipelining logiciels!
# 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