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 -= scalar
operacji; 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 „ memcpy
a” uint64_t
, który również usuwa alignof(uint64_t
wymóg wyrównania. Ale w ISA bez wydajnych nierównomiernych obciążeń, gcc / clang nie inline i optymalizują, memcpy
gdy 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_t
jest prawie na pewno unsigned char*
bezpieczny dostęp do bajtów uint64_t
via 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ż char
jest 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 int
ale nie float
lub uint8_t
czy 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=skylake
dalszym 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 d
lub 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ść sub
na ś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