Für RISC-V verwenden Sie wahrscheinlich GCC / clang.
Unterhaltsame Tatsache: GCC kennt einige dieser SWAR-Bithack-Tricks (in anderen Antworten gezeigt) und kann sie für Sie verwenden, wenn Sie Code mit nativen GNU C-Vektoren für Ziele ohne Hardware-SIMD-Anweisungen kompilieren . (Aber wenn Sie für RISC-V klirren, wird es nur naiv für skalare Operationen abgewickelt, sodass Sie es selbst tun müssen, wenn Sie eine gute Leistung über Compiler hinweg wünschen.)
Ein Vorteil der nativen Vektorsyntax besteht darin, dass beim Targeting einer Maschine mit Hardware-SIMD diese verwendet wird, anstatt Ihren Bithack oder etwas Schreckliches automatisch zu vektorisieren.
Es macht es einfach, vector -= scalar
Operationen zu schreiben ; Die Syntax Just Works überträgt implizit den Skalar für Sie.
Beachten Sie auch, dass eine uint64_t*
Last von a ein uint8_t array[]
striktes Aliasing für UB ist. Seien Sie also vorsichtig damit. (Siehe auch Warum muss glibc's strlen so kompliziert sein, um schnell zu laufen? Betreff: SWAR-Bithacks in reinem C sicher strikt aliasing machen). Möglicherweise möchten Sie, dass so etwas deklariert, uint64_t
dass Sie mit dem Zeiger auf andere Objekte zugreifen können, z. B. wie dies char*
in ISO C / C ++ funktioniert.
Verwenden Sie diese, um uint8_t-Daten in ein uint64_t zu übertragen und mit anderen Antworten zu verwenden:
// 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)));
Die andere Möglichkeit, aliasing-sichere Lasten auszuführen, ist memcpy
in a uint64_t
, wodurch auch die alignof(uint64_t
Ausrichtungsanforderung entfällt . Bei ISAs ohne effiziente nicht ausgerichtete Lasten wird gcc / clang jedoch nicht inline und optimiert, memcpy
wenn nicht nachgewiesen werden kann, dass der Zeiger ausgerichtet ist, was für die Leistung katastrophal wäre.
TL: DR: Am besten deklarieren Sie Ihre Daten alsuint64_t array[...]
oder ordnen sie dynamisch zu uint64_t
, oder vorzugsweise.alignas(16) uint64_t array[];
Dies stellt die Ausrichtung auf mindestens 8 Bytes oder 16 Bytes sicher, wenn Sie dies angeben alignas
.
Da dies uint8_t
mit ziemlicher Sicherheit der Fall ist unsigned char*
, ist es sicher, auf die Bytes eines uint64_t
Via zuzugreifen uint8_t*
(bei einem uint8_t-Array jedoch nicht umgekehrt). In diesem speziellen Fall, in dem es sich um einen schmalen Elementtyp handelt unsigned char
, können Sie das Problem des strengen Aliasing umgehen, da char
es speziell ist.
Beispiel für die native Vektorsyntax von GNU C:
GNU C-native Vektoren dürfen immer einen Alias mit ihrem zugrunde liegenden Typ haben (z. B. int __attribute__((vector_size(16)))
können sicher Alias sein, int
aber nicht float
oder uint8_t
oder irgendetwas anderes.
#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.
}
Bei RISC-V ohne HW-SIMD können Sie vector_size(8)
nur die Granularität ausdrücken, die Sie effizient verwenden können, und doppelt so viele kleinere Vektoren erstellen.
Aber vector_size(8)
kompiliert sehr dumm für x86 mit GCC und clang: GCC verwendet SWAR-Bithacks in GP-Integer-Registern, Clang entpackt in 2-Byte-Elemente, um ein 16-Byte-XMM-Register zu füllen, und packt dann neu. (MMX ist so veraltet, dass GCC / Clang sich nicht einmal die Mühe macht, es zu verwenden, zumindest nicht für x86-64.)
Aber mit vector_size (16)
( Godbolt ) bekommen wir das erwartete movdqa
/ paddb
. (Mit einem All-One-Vektor generiert von pcmpeqd same,same
). Da -march=skylake
wir immer noch zwei separate XMM-Operationen anstelle einer YMM erhalten, "vektorisieren" aktuelle Compiler leider auch keine Vektoroperationen automatisch in breitere Vektoren: /
Für AArch64 ist es nicht so schlecht zu verwenden vector_size(8)
( Godbolt ); ARM / AArch64 kann nativ in 8- oder 16-Byte-Blöcken mit d
oder q
Registern arbeiten.
Sie möchten also wahrscheinlich vector_size(16)
tatsächlich kompilieren, wenn Sie eine tragbare Leistung für x86, RISC-V, ARM / AArch64 und POWER wünschen . Einige andere ISAs machen jedoch SIMD innerhalb von 64-Bit-Integer-Registern, wie MIPS MSA, denke ich.
vector_size(8)
erleichtert das Betrachten des asm (nur ein Register mit Daten): 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
Ich denke, es ist die gleiche Grundidee wie bei den anderen Antworten ohne Schleifen. Verhindern Sie das Tragen und korrigieren Sie das Ergebnis.
Dies sind 5 ALU-Anweisungen, schlimmer als die beste Antwort, denke ich. Es sieht jedoch so aus, als ob die kritische Pfadlatenz nur 3 Zyklen beträgt, wobei zwei Ketten mit jeweils 2 Befehlen zum XOR führen. Die Antwort von @Reinstate Monica - ζ - wird zu einer 4-Zyklus-Dep-Kette (für x86) kompiliert. Der 5-Zyklus-Schleifendurchsatz wird durch die Einbeziehung eines Naiven sub
in den kritischen Pfad eingeschränkt, und die Schleife führt zu einem Engpass bei der Latenz.
Dies ist jedoch bei Klirren nutzlos. Es wird nicht einmal in der Reihenfolge hinzugefügt und gespeichert, in der es geladen wurde, sodass es nicht einmal ein gutes Software-Pipelining durchführt!
# 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