W przypadku RISC-V prawdopodobnie używasz GCC / clang.
Ciekawostka: GCC zna niektóre z tych sztuczek SWAR (pokazanych w innych odpowiedziach) i może ich użyć podczas kompilacji kodu z wektorami natywnymi GNU C dla celów bez instrukcji sprzętowych SIMD. (Ale kliknięcie na RISC-V po prostu naiwnie rozwinie go do operacji skalarnych, więc musisz to zrobić sam, jeśli chcesz dobrej wydajności pomiędzy kompilatorami).
Jedną z zalet natywnej składni wektorowej jest to, że atakując maszynę ze sprzętową kartą SIMD, użyje jej zamiast automatycznie wektoryzacji twojego bithacka lub czegoś okropnego.
Ułatwia pisanie vector -= scalaroperacji; składnia Just Works, niejawnie transmituje dla ciebie aka splatting skalar.
Zauważ też, że uint64_t*obciążenie z uint8_t array[]UB ściśle aliasinguje, więc bądź ostrożny z tym. (Zobacz także Dlaczego strli glibc musi być tak skomplikowane, aby działało szybko? Re: uczynienie binarnych SWAR rygorystycznym aliasingiem bezpiecznym w czystym C). Możesz chcieć, aby coś takiego zadeklarowało uint64_t, że możesz rzutować wskaźnik, aby uzyskać dostęp do innych obiektów, na przykład jak char*działa w ISO C / C ++.
użyj tych, aby przenieść dane uint8_t do uint64_t do użycia z innymi odpowiedziami:
// 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)));
Innym sposobem wykonania bezpiecznych dla aliasingu ładunków jest użycie znaku „ memcpya” uint64_t, który również usuwa alignof(uint64_twymóg wyrównania. Ale w ISA bez wydajnych nierównomiernych obciążeń, gcc / clang nie inline i optymalizują, memcpygdy nie mogą udowodnić, że wskaźnik jest wyrównany, co byłoby katastrofalne dla wydajności.
TL: DR: najlepiej jest zadeklarować dane jako ciuint64_t array[...] lub przeznaczyć je dynamicznie uint64_t, a najlepiejalignas(16) uint64_t array[]; , który zapewnia dostosowanie do co najmniej 8 bajtów lub 16 jeśli podasz alignas.
Ponieważ uint8_tjest prawie na pewno unsigned char*bezpieczny dostęp do bajtów uint64_tvia uint8_t*(ale nie odwrotnie w przypadku tablicy uint8_t). Dlatego w tym szczególnym przypadku, w którym występuje wąski element unsigned char, możesz ominąć problem ścisłego aliasingu, ponieważ charjest on wyjątkowy.
Przykład natywnej składni wektorowej GNU C:
GNU C rodzimych wektory są zawsze wolno alias z ich podstawowego typu (np int __attribute__((vector_size(16)))może bezpiecznie alias intale nie floatlub uint8_tczy cokolwiek innego.
#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.
}
W przypadku RISC-V bez żadnego HW SIMD, możesz użyć vector_size(8)do wyrażenia tylko szczegółowości, której możesz efektywnie użyć, i zrobić dwa razy więcej mniejszych wektorów.
Ale vector_size(8)kompiluje się bardzo głupio dla x86 zarówno z GCC, jak i clang: GCC używa bitów SWAR w rejestrach liczb całkowitych GP, clang rozpakowuje się do elementów 2-bajtowych, aby wypełnić 16-bajtowy rejestr XMM, a następnie przepakowuje. (MMX jest tak przestarzały, że GCC / clang nawet nie zawraca sobie nim głowy, przynajmniej nie dla x86-64.)
Ale z vector_size (16)( Godbolt ) otrzymujemy oczekiwany movdqa/ paddb. (Z wektorem wszystkich jedynek wygenerowanym przez pcmpeqd same,same). W -march=skylakedalszym ciągu otrzymujemy dwa oddzielne operacje XMM zamiast jednego YMM, więc niestety obecne kompilatory również nie „automatycznie wektorują” operacje wektorowe w szersze wektory: /
W przypadku AArch64 korzystanie z niego nie jest takie złe vector_size(8)( Godbolt ); ARM / AArch64 może natywnie pracować w 8 lub 16-bajtowych porcjach z rejestrami dlub q.
Prawdopodobnie chcesz vector_size(16)się faktycznie skompilować, jeśli chcesz mieć przenośną wydajność w procesorach x86, RISC-V, ARM / AArch64 i POWER . Jednak niektóre inne ISA wykonują SIMD w 64-bitowych rejestrach liczb całkowitych, jak myślę MIPS MSA.
vector_size(8)ułatwia spojrzenie na asm (dane o wartości tylko jednego rejestru): eksplorator kompilatora Godbolt
# 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
Myślę, że to ten sam podstawowy pomysł, co inne nie zapętlone odpowiedzi; zapobiegając przenoszeniu, a następnie ustalając wynik.
To jest 5 instrukcji ALU, gorsze niż najlepsza odpowiedź, jak sądzę. Wygląda jednak na to, że opóźnienie ścieżki krytycznej to tylko 3 cykle, z dwoma łańcuchami po 2 instrukcje prowadzące do XOR. @Reinstate Monica - ζ - odpowiedź kompiluje się do 4-cyklowego łańcucha dep (dla x86). Przepływ w pętli 5-cyklowej jest wąski, ponieważ obejmuje naiwność subna ścieżce krytycznej, a pętla powoduje wąskie gardło w przypadku opóźnienia.
Jest to jednak bezużyteczne w przypadku clang. Nawet nie dodaje i nie zapisuje w tej samej kolejności, w jakiej został załadowany, więc nawet nie robi dobrego potoku oprogramowania!
# 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