RISC-Vの場合は、おそらくGCC / clangを使用しています。
おもしろい事実:GCCはこれらのSWARビットハックトリック(他の回答に示されている)のいくつかを知っており、ハードウェアSIMD命令のないターゲットのGNU Cネイティブベクトルでコードをコンパイルするときにそれらを使用できます。(ただし、RISC-Vのclangは単純にスカラー演算に展開するので、コンパイラー間で優れたパフォーマンスが必要な場合は、自分で行う必要があります)。
ネイティブのベクトル構文の利点の1つは、ハードウェアSIMDが搭載されたマシンを対象とする場合、ビットハックやそのような恐ろしいものを自動ベクトル化する代わりにそれを使用することです。
vector -= scalar
操作を簡単に記述できます。構文Just Worksは、暗黙的にスカラーをスプラッティングしてブロードキャストします。
また、a uint64_t*
からのロードuint8_t array[]
は厳密なエイリアスのUBであるため、そのことに注意してください。(なぜglibcのstrlenを素早く実行するためにそれほど複雑にする必要があるのですか? re:純粋なCでSWARビットハックを厳密にエイリアス化して安全にする)ISO C / C ++での動作のuint64_t
ように、ポインターキャストして他のオブジェクトにアクセスできることを宣言するには、このようなものを使用する必要がありますchar*
。
これらを使用して、他の回答で使用するためにuint8_tデータをuint64_tに取得します。
// 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)));
エイリアシングに対して安全なロードを行うもう1つの方法は、memcpy
intoを使用することです。uint64_t
これにより、alignof(uint64_t
)のアライメント要件も削除されます。しかし、効果的な非整列ロードのないISAでは、gcc / clangはインライン化せずmemcpy
、ポインターが整列していることを証明できない場合に最適化を行わないため、パフォーマンスに悪影響を及ぼします。
TL:DR:あなたの最善の策は、としてあなたのデータを宣言することであるuint64_t array[...]
かのように動的に割り当てるuint64_t
、または好ましくalignas(16) uint64_t array[];
指定した場合、少なくとも8バイト、または16のことを保証アライメントalignas
。
以来uint8_t
ほぼ確実であるunsigned char*
、それはバイトにアクセスしても安全ですuint64_t
を通じてuint8_t*
(しかし、その逆はないuint8_t配列のため)。したがって、ナローエレメントタイプがであるこの特殊なケースではunsigned char
、char
が特殊であるため、厳密なエイリアスの問題を回避できます。
GNU Cネイティブのベクトル構文の例:
GNU Cネイティブのベクトルは常に例えば(その基本となるタイプでエイリアスに許可されてint __attribute__((vector_size(16)))
安全に別名設定できるint
ではないfloat
か、uint8_t
または何か他のもの。
#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.
}
HW SIMDのないRISC-Vのvector_size(8)
場合、効率的に使用できる粒度のみを表現し、2倍の小さなベクトルを実行できます。
しかしvector_size(8)
、GCCとclangの両方でx86用に非常に愚かにコンパイルされます。GCCは、GP整数レジスターでSWARビットハックを使用し、clangは2バイト要素にアンパックして、16バイトXMMレジスターを埋めてから再パックします。(MMXは非常に古いため、GCC / clangは、少なくともx86-64の場合は、それを使用することさえありません。)
しかし、とvector_size (16)
(Godbolt)私たちは期待を取得movdqa
/をpaddb
。(で生成されたすべて1のベクトルを使用pcmpeqd same,same
)。-march=skylake
我々はまだので、残念ながら現在のコンパイラはまた、より広いベクターにはない「自動ベクトル化」ベクトルOPSを行い、代わりに1 YMM 2つの別個のXMMオプスを取得します:/
AArch64の場合、使用はそれほど悪くありませんvector_size(8)
(Godbolt); ARM / AArch64は、d
またはq
レジスタを使用して、8または16バイトのチャンクでネイティブに動作できます。
したがってvector_size(16)
、x86、RISC-V、ARM / AArch64、およびPOWER全体で移植可能なパフォーマンスが必要な場合は、実際にコンパイルする必要があります。しかし、他のいくつかのISAは、MIPS MSAのように、64ビット整数レジスタ内でSIMDを実行します。
vector_size(8)
asm(データの1つのレジスター値のみ)を簡単に見ることができます: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
それは他のループしない答えと同じ基本的な考え方だと思います。キャリーを防ぎ、結果を修正します。
これは5つのALU命令で、私が思うトップの回答よりも悪いです。しかし、クリティカルパスレイテンシは3サイクルにすぎず、2つの命令の2つのチェーンがそれぞれXORにつながるようです。@Reinstate Monica-ζ--の回答は4サイクルのdepチェーン(x86用)にコンパイルされます。5サイクルのループスループットはsub
、クリティカルパスにナイーブを含めることによってもボトルネックになり、ループはレイテンシのボトルネックになります。
ただし、これはclangでは無意味です。読み込んだ順序で追加および保存することもないため、優れたソフトウェアパイプライン処理も行われません。
# 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