RVV(RISC-V Vector Extension)は、RISC-Vにベクトル演算を加える標準拡張です。1.0版は2021年11月に批准され、RISC-V Internationalが同年12月2日に公表しました。x86のAVXやArmのNEONのように命令ごとにビット幅が決まる固定長SIMDと違い、レジスタの長さ(VLEN)をCPUごとに変えられ、プログラムは実行時に「今回いくつ処理できるか」を問い合わせて動きます。
この記事では、VLEN・SEW・LMULとvsetvliの関係を仕様の原文に沿って整理し、C intrinsicsのコードと、clang 22.1.8・21.1.0でコンパイルした実際のアセンブリを示します。ストライド・セグメント・fault-only-firstの各ロードとリダクションのコードは、QEMU 11.1.2でVLENを128・256・1,024ビットに切り替えて実行し、結果を確かめました。実機を選ぶときに詰まる0.7.1版との非互換や、RVA23プロファイルでの必須化も扱います。
まとめ:RVV 1.0で押さえる仕組みと使い始める前の判断
- RVVは可変長ベクトル方式。
vsetvliが残り要素数から今回の処理数vlを返すため、端数処理のループを書かずに済み、同じバイナリがVLEN 128ビットのCPUでも256ビットのCPUでも動きます。 - 1命令で扱う要素数の上限はVLMAX=LMUL×VLEN÷SEW。VLEN 128ビットで32ビット要素・LMUL=8なら32要素です。
- C intrinsicsは
__riscv_で始まる関数群で、仕様v1.0は2025年4月25日に批准。Clang 19とGCC 14以降が対応します。 - 普通のCで書いたsaxpyは、clang 22.1.8の
-O2でvsetvliのループ1本に自動ベクトル化されました。clang 21.1.0ではスカラーの端数ループが残るので、生成コードはコンパイラの版で変わります。 - メモリアクセスは、連続・ストライド・インデックスの3方式と、セグメント・fault-only-firstの派生形を取り上げます。終端が分からない
strlenはfault-only-first、RGBのような交互配置はセグメントロードで書きます。 - 「1周目は必ずVLMAX個」と決め打ちしたコードは、QEMUでVLENを変えても通り、
rvv_vl_half_avl=onを付けたときだけ失敗しました。 - T-HeadのC906などが載せる0.7.1版(XTheadVector)は1.0と互換がありません。V拡張はRVA22ではオプション、RVA23で必須になりました。
実務での順序は、まずコンパイラの自動ベクトル化を試し、効かないホットループだけintrinsicsで書く、です。以下、仕組み、書き方、ロード・ストア、互換性、検証手順の順に説明します。
RVVの可変長ベクトル方式:固定長SIMDとの違いとVLAの仕組み
同じバイナリがVLEN 128ビットでも256ビットでも動く理由
AVX2のコードは「256ビット=float 8個」をコードに埋め込みます。AVX-512で倍の幅を使うには、別の命令で書き直して再コンパイルが必要です。RVVのコードには幅が書かれていません。ループの先頭でvsetvliが「要素型は32ビット、残りはn個」と問い合わせ、ハードウェアがそのCPUで処理できる数を返します。
このため、VLEN 128ビットのKendryte K230でも256ビットのSpacemiT K1でも、同じ機械語をVLENの違いを気にせず使えます(OS・ABI・Cライブラリの条件がそろっていることが前提です)。この書き方をVLA(Vector Length Agnostic、ベクトル長非依存)と呼びます。
vlの決まり方と、ストリップマイニングで端数ループが消える理由
配列を「1周でvl個ずつ」切り出して処理するループをストリップマイニングと呼びます。RISC-V ISAマニュアルのベクトル拡張の章(Constraints on Setting vl)は、vsetvliが返すvlを次のように定めています。
| 残り要素数(AVL) | 設定されるvl |
|---|---|
| AVL ≦ VLMAX | AVLそのもの |
| VLMAX < AVL < 2×VLMAX | ceil(AVL÷2)以上、VLMAX以下 |
| AVL ≧ 2×VLMAX | VLMAX |
最後の周回では残り数がそのままvlになるので、端数を別に扱うコードを書かずに済みます。固定長SIMDでは、AVXのvmaskmovpsのようなマスク付きロード・ストアを使うか、8個未満の残りをスカラーで処理する後始末ループを書くのが一般的です。注意したいのは表の2行目です。最後の2周に作業を均等に振り分けるため、実装はvlをVLMAXより小さく返してよいと仕様に明記されています。「1周目は必ずVLMAX個」と決め打ちしたインデックス計算は、移植先で壊れます。ループは必ず返ってきたvlで進めてください。
Arm SVE・AVX-512・NEONとの比較
| 方式 | ベクトル長 | 端数処理 | 要素数の決め方 |
|---|---|---|---|
| RISC-V RVV 1.0 | 実装ごと(V拡張は128ビット以上) | vlで吸収 | vsetvliが実行時に返す |
| Arm SVE/SVE2 | 実装ごと(128〜2,048ビット) | 述語レジスタで吸収 | whilelt等で述語を作る |
| x86 AVX-512 | 命令ごとに固定(AVX512VLで128/256も) | マスクで処理 | 命令に固定 |
| Arm NEON/x86 AVX2 | 128/256ビット固定 | 後始末ループかマスク付きロード | 命令に固定 |
固定長SIMDの一般論はSIMDの仕組みと自動ベクトル化・組み込み関数の書き方で、RISC-VとArm・x86の命令セット全体の比較はRISC-VとArm・x86の違いの比較記事で扱っています。
RVVのレジスタ構成:VLEN・SEW・LMULとVLMAXの計算
32本のベクトルレジスタとVLENの下限128ビット
RVVはv0〜v31の32本のベクトルレジスタを追加します。1本の長さがVLENで、仕様上は2の累乗かつ216(65,536)ビット以下です。アプリケーションプロセッサ向けの「V」拡張はZvl128bとZve64dへの依存が定められており、VLENは128ビット以上、要素は64ビットの整数と倍精度浮動小数点まで扱えます。RISC-V Internationalが最新安定版として案内するISA仕様(版識別子20260120)でも、V拡張の章はVersion 1.0のままです。
マイコン向けにはZve32x・Zve32f・Zve64x・Zve64f・Zve64dという縮小版があります。Zve32xは浮動小数点を持たないため、後述のfloat版intrinsicsはコンパイルできません。clang 22.1.8でZve32xだけを指定すると、__riscv_vは定義されず、__riscv_vectorが1、__riscv_v_min_vlenと__riscv_v_elenがどちらも32でした。縮小版も含めて「何らかのベクトル命令があるか」は__riscv_vector、フルのV拡張かどうかは__riscv_vで見分けます。
SEW・LMULとVLMAXの早見表(VLEN 128/256ビット)
SEWは1要素のビット幅で8・16・32・64から選びます。LMULは何本のレジスタを束ねて1つのベクトルとして扱うかの倍率で、1/8・1/4・1/2・1・2・4・8を符号化できます。ただしSEWとLMULは自由に組み合わせられるわけではなく、分数LMULではSEWに上限があります。たとえばELEN=64の実装でも、SEW=64とLMUL=1/2の組み合わせは実装が対応を求められていません。対応外の設定ではvtypeのvillビットが立ち、vtypeに依存するベクトル命令が不正命令例外になります(vsetvliによる再設定やレジスタ丸ごとのロード・ストアは実行できます)。1命令で処理できる要素数の上限は、仕様の式VLMAX=LMUL×VLEN÷SEWで決まります。
| SEW | VLEN128・LMUL1 | VLEN128・LMUL8 | VLEN256・LMUL1 | VLEN256・LMUL8 |
|---|---|---|---|---|
| 8ビット | 16 | 128 | 32 | 256 |
| 16ビット | 8 | 64 | 16 | 128 |
| 32ビット | 4 | 32 | 8 | 64 |
| 64ビット | 2 | 16 | 4 | 32 |
LMUL=8は1命令の幅が最大になる一方、レジスタグループの名前がv0・v8・v16・v24の4つに減ります。マスク付き命令ではv0をマスクに使うため、値を置けるグループはさらに少なくなります。同時に生きている値がこの数を超えるとスタックへの退避(スピル)が起きやすくなり、出力アセンブリにvs8r.vなどのレジスタ丸ごとのストアが現れます。LMULはm1・m2・m4・m8で同じループをコンパイルし、スピルの有無と実機での所要時間を比べて決めてください。分数LMULは、16ビットの入力から32ビットの結果を作るような幅の混在する処理で、狭い側にレジスタ1本未満を割り当てるためにあります。
vsetvliの書式とta・maポリシー
vsetvli t0, a0, e32, m8, ta, ma
この1行は「残り要素数a0、要素32ビット、LMUL=8で設定し、今回の処理数をt0に返す」という意味です。ta(tail agnostic)はvlを超えた末尾要素の値を、ma(mask agnostic)はマスクで無効にした要素の値を、それぞれ保証しなくてよいという指定です。前の値を残す必要があるときだけtu・muを選びます。
マスクレジスタv0による条件付き演算
比較命令の結果は要素ごとのビット列(マスク)として任意のベクトルレジスタに書けます。条件付き実行で参照できるマスクはv0だけで、後続の命令にv0.tを付けると、v0のビットが立った要素だけが演算されます。intrinsicsでは_m付きの関数(例:__riscv_vadd_vv_i32m1_m)がこれに当たり、第1引数にvbool32_tなどのマスク型を受け取ります。マスク型の数字はSEW÷LMULの比で、32ビット要素・LMUL=8ならvbool4_tです。
C intrinsicsでRVVを書く:命名規則とsaxpy・ReLUの実装例
__riscv_接頭辞と型名・サフィックスの読み方
RISC-V Vector C Intrinsic Specificationはv1.0が2025年4月25日に批准されました。関数名は__riscv_+命令名+オペランド形式+型で組み立てられます。__riscv_vfmacc_vf_f32m8なら、vfmacc命令の、ベクトルとスカラー(vf)を取る、32ビット浮動小数点・LMUL=8の版です。型も同じ規則で、vfloat32m8_t・vint32m1_t・vint16mf2_t(LMUL=1/2)のように幅とLMULが名前に入ります。
古い資料のvle32_v_f32m8のような接頭辞なしの名前は、Clang 16で全intrinsicsに__riscv_が付く前の表記です。現行のコンパイラではそのまま通らないので、ネットのサンプルを写すときは名前を読み替えてください。
ポリシー接尾辞_tu・_tum・_mu・_tumuと既定のta・ma
intrinsics仕様v1.0では、接尾辞の無い関数はtail agnostic・mask agnostic(vta=1・vma=1)が既定です。末尾やマスク外の要素に前の値を残したいときは、関数名の末尾にポリシーを付けます。
| 接尾辞 | マスク | 末尾(vl以降) | マスク外の要素 |
|---|---|---|---|
| なし | なし | 保証なし | - |
| _tu | なし | 前の値を残す | - |
| _m | あり | 保証なし | 保証なし |
| _tum | あり | 前の値を残す | 保証なし |
| _mu | あり | 保証なし | 前の値を残す |
| _tumu | あり | 前の値を残す | 前の値を残す |
前の値を残すベクトル(passthrough)は、_tuでは第1引数、マスク付きの_tum・_mu・_tumuではマスクに続く第2引数に渡します。次のコードはvl=2で足し算し、4要素を書き出します。
int32_t a[4] = {1, 1, 1, 1}, b[4] = {10, 10, 10, 10}, out[4];
vint32m1_t dst = __riscv_vle32_v_i32m1(b, 4);
vint32m1_t va = __riscv_vle32_v_i32m1(a, 4);
vint32m1_t r = __riscv_vadd_vv_i32m1_tu(dst, va, va, 2);
__riscv_vse32_v_i32m1(out, r, 4); // out = {2, 2, 10, 10}
QEMUでVLEN 128・256・1,024ビットのどれで実行しても、outは2 2 10 10でした。要素2・3にはdstの10が残っています。接尾辞なしの__riscv_vadd_vv_i32m1では要素2・3の値が保証されないので、ループの外で末尾を読むコードには_tuを使います。
saxpyをvsetvlのループで書くコードとclang 22の出力
y=a×x+yを計算するsaxpyをintrinsicsで書くと次のとおりです。xとyが部分的に重なる配列だと、1要素ずつ処理するスカラー版と結果が変わりうるため、重ならないことを前提にしています。vfmaccは積と和を1回の丸めで計算する融合積和なので、検算の参照値もC標準のfmafで作ります。後述のテストのようにNaNや負のゼロを含まない有限値の入力なら、==で一致を確かめられます。
#include <riscv_vector.h>
#include <stddef.h>
void saxpy(size_t n, float a, const float *x, float *y) {
for (size_t vl; n > 0; n -= vl, x += vl, y += vl) {
vl = __riscv_vsetvl_e32m8(n);
vfloat32m8_t vx = __riscv_vle32_v_f32m8(x, vl);
vfloat32m8_t vy = __riscv_vle32_v_f32m8(y, vl);
vy = __riscv_vfmacc_vf_f32m8(vy, a, vx, vl);
__riscv_vse32_v_f32m8(y, vy, vl);
}
}
zig 0.17.1に同梱のclang 22.1.8で-target riscv64-linux-musl -mcpu=generic_rv64+m+a+f+d+c+v -O2を指定してコンパイルすると、ループ本体は次の命令列になりました(スタックフレーム操作を除く)。
.LBB0_2:
vsetvli a3, a0, e32, m8, ta, ma
vle32.v v8, (a1)
vle32.v v16, (a2)
sub a0, a0, a3
vfmacc.vf v16, fa0, v8
vse32.v v16, (a2)
slli a3, a3, 2
add a1, a1, a3
add a2, a2, a3
bnez a0, .LBB0_2
ループは10命令で、端数用の分岐はありません。VLENに当たる定数もコードに出てこないので、このオブジェクトはVLEN 128でも1,024でも同じように動きます。LMULをm1に替えても、使うレジスタがv8・v9に変わるだけで、ループは同じ10命令でした。clang 21.1.0でも、同じ条件で同じ命令列になりました。
ReLUをマスク付きで書くコード
負の値を0にするReLUは、比較でマスクを作り、マスクが立った要素を0で置き換えます。単独で試すときはsaxpyと同じくriscv_vector.hとstddef.hをインクルードします。vmflt(未満の比較)はNaNと-0.0fに対して偽を返すので、この2つは置き換えられずにそのまま残ります。
void relu(size_t n, float *x) {
for (size_t vl; n > 0; n -= vl, x += vl) {
vl = __riscv_vsetvl_e32m8(n);
vfloat32m8_t v = __riscv_vle32_v_f32m8(x, vl);
vbool4_t neg = __riscv_vmflt_vf_f32m8_b4(v, 0.0f, vl);
v = __riscv_vfmerge_vfm_f32m8(v, 0.0f, neg, vl);
__riscv_vse32_v_f32m8(x, v, vl);
}
}
同じ条件でコンパイルすると、比較結果はvmflt.vf v0, v8, fa5でv0に書かれ、置き換えはvmerge.vim v8, v8, 0, v0になりました。0.0fのビット列は整数の0と同じなので、コンパイラが整数の即値マージに置き換えています。分岐なしで条件処理が書けます。
V拡張を指定しないとコンパイルが通らない:-marchと__riscv_v_intrinsic
上のsaxpyを、V拡張を外したgeneric_rv64+m+a+f+d+cでコンパイルすると、RISC-V type 'vfloat32m8_t' (aka '__rvv_float32m8_t') requires the 'zve32f' extensionというエラーで止まります。GCCやclangを直接使う場合は-march=rv64gcvのようにVを含めて指定します。
ソースを両対応にするときは、マクロでコンパイル対象を判定します。clang 22.1.8でV有効時の定義済みマクロを確認すると、__riscv_vと__riscv_v_intrinsicがどちらも1000000、__riscv_v_min_vlenが128でした。注意点は、__riscv_v_intrinsicがコンパイラのintrinsics対応版を示すマクロで、V拡張を外した設定でも1000000と定義されることです。V拡張の有無は__riscv_vで判定します。
#if defined(__riscv_v) && defined(__riscv_v_intrinsic) && __riscv_v_intrinsic >= 1000000
#include <riscv_vector.h>
#define HAVE_RVV 1
#endif
これはビルド時の判定で、実行するCPUがV拡張を持つかどうかは分かりません。V無しのCPUでも動く配布バイナリを作るなら、Linuxのgetauxval(AT_HWCAP)などで実行時に確かめて、ベクトル版とスカラー版を切り替えます。
ロード・ストアとリダクション:アクセス方式の使い分けと配列の合計
連続・ストライド・インデックス・セグメント・fault-only-firstの使い分け
RVVのロード・ストアには連続・ストライド・インデックスの3つのアドレッシング方式があり、セグメント転送やfault-only-firstロードも用意されています。V拡張の仕様では、ストライドはバイト単位で、負の値や0も指定できます。インデックス方式には要素の順にアクセスする版(vloxei)と順序を保証しない版(vluxei)があり、メモリマップドI/Oのように順序が意味を持つ領域でだけ順序付きを使います。
| 方式 | 命令の例 | intrinsicsの例 | 向いているデータ |
|---|---|---|---|
| 連続(ユニットストライド) | vle32.v | __riscv_vle32_v_f32m8 | 普通の配列 |
| ストライド | vlse32.v | __riscv_vlse32_v_f32m1 | 行列の列、構造体の1メンバー |
| インデックス | vluxei32.v | __riscv_vluxei32_v_f32m1 | 表引き(ギャザー) |
| セグメント | vlseg3e8.v | __riscv_vlseg3e8_v_u8m2x3 | RGBなど交互に並んだ値 |
| fault-only-first(ロードのみ) | vle8ff.v | __riscv_vle8ff_v_u8m8 | 終端が分からない文字列 |
fault-only-firstでstrlenを書く:ページ境界の先を読んでも落ちない理由
文字列の長さは読んでみるまで分からないので、VLMAX個まとめて読むと、終端の先にある読めないページに触れることがあります。fault-only-firstロードは、仕様上「要素0で例外が起きたときだけトラップし、要素1以降で起きた場合はトラップせずにvlを縮める」命令です。縮んだvlは第2引数で受け取ります。
size_t rvv_strlen(const char *s) {
const char *p = s;
for (;;) {
size_t vl;
vuint8m8_t v = __riscv_vle8ff_v_u8m8((const uint8_t *)p, &vl, __riscv_vsetvlmax_e8m8());
vbool1_t z = __riscv_vmseq_vx_u8m8_b1(v, 0, vl);
long i = __riscv_vfirst_m_b1(z, vl);
if (i >= 0) return (size_t)(p - s) + (size_t)i;
p += vl;
}
}
検査ではmmapで2ページ確保して2ページ目をPROT_NONEにし、1ページ目の末尾ぎりぎりに長さ0〜299の文字列を置きました。QEMUでVLEN 128・256・1,024ビットのどれでも、300通り全ての長さがstrlenと一致しています。clang 22.1.8の出力では、vle8ff.vの直後にcsrr a2, vlで縮んだvlを読み直していました。
セグメントロードでRGBを3本に分ける:vlseg3e8とタプル型
RGBRGB…と交互に並んだ画素は、セグメントロード1命令でR・G・Bの3本のベクトルに分けられます。intrinsics v1.0では結果がタプル型(vuint8m2x3_t)で返り、__riscv_vgetで1本ずつ取り出します。仕様の制約はEMUL×フィールド数≦8で、3フィールドならLMUL=2までです。次のコードは輝度を(77R+150G+29B)÷256で求めます。
void rgb2gray(size_t n, const uint8_t *rgb, uint8_t *gray) {
for (size_t vl; n > 0; n -= vl, rgb += vl * 3, gray += vl) {
vl = __riscv_vsetvl_e8m2(n);
vuint8m2x3_t t = __riscv_vlseg3e8_v_u8m2x3(rgb, vl);
vuint8m2_t r = __riscv_vget_v_u8m2x3_u8m2(t, 0);
vuint8m2_t g = __riscv_vget_v_u8m2x3_u8m2(t, 1);
vuint8m2_t b = __riscv_vget_v_u8m2x3_u8m2(t, 2);
vuint16m4_t acc = __riscv_vwmulu_vx_u16m4(r, 77, vl);
acc = __riscv_vwmaccu_vx_u16m4(acc, 150, g, vl);
acc = __riscv_vwmaccu_vx_u16m4(acc, 29, b, vl);
__riscv_vse8_v_u8m2(gray, __riscv_vnsrl_wx_u8m2(acc, 8, vl), vl);
}
}
8ビットの積は8ビットに収まらないことがあるため、vwmulu・vwmaccuで16ビットに広げて計算し、vnsrlで8ビットに戻します。広げた側はLMUL=4になるので、LMUL=8で読むと拡幅の先が16本分になって組めません。1,000画素をスカラー計算と比べ、QEMUの3通りのVLENで全画素が一致しました。
リダクションで配列を合計する:vwredsumで64ビットへ拡幅
配列の合計や内積は、ベクトルの全要素を1つの値にまとめるリダクション命令で書きます。結果はベクトルレジスタの要素0に入るので、それを次の周回の初期値に渡して積み上げます。32ビット整数の合計は桁あふれしやすいので、要素を2倍の幅に符号拡張してから足すvwredsumを使います。
int64_t sum_i32(size_t n, const int32_t *x) {
vint64m1_t acc = __riscv_vmv_s_x_i64m1(0, 1);
for (size_t vl; n > 0; n -= vl, x += vl) {
vl = __riscv_vsetvl_e32m4(n);
vint32m4_t v = __riscv_vle32_v_i32m4(x, vl);
acc = __riscv_vwredsum_vs_i32m4_i64m1(v, acc, vl);
}
return __riscv_vmv_x_s_i64m1_i64(acc);
}
20億前後の値を1,001個並べた配列で、結果は2,001,999,499,500となり、64ビットのスカラー合計と一致しました。浮動小数点の合計には、要素の順に足すvfredosumと、順序を決めないvfredusumがあります。仕様はvfredusumに二分木状の任意の順序を認めており、丸め誤差の出方が実装で変わりえます。QEMU 11.1.2では1÷1から1÷1,000までの合計が両方とも逐次和の7.4854784と一致しましたが、実機で同じ結果になる保証はありません。結果を逐次和とビット単位で一致させる必要がある処理はvfredosumで書きます。
自動ベクトル化の実測:clang 22と21で変わる端数処理と対応コンパイラの版
clang 22.1.8と21.1.0のsaxpy出力比較:スカラーの端数ループの有無
intrinsicsを使わず、普通のCで書いたsaxpyを2つの版のclangでコンパイルしました。
#include <stddef.h>
void saxpy_auto(size_t n, float a, const float *restrict x, float *restrict y) {
for (size_t i = 0; i < n; i++) y[i] = a * x[i] + y[i];
}
どちらの版も-Rpass=loop-vectorizeでvectorized loop (vectorization width: vscale x 4, interleaved count: 1)と表示され、ベクトル化は効いています。違いは端数の扱いです。clang 22.1.8の-O2では、ループはvsetvli a4, a0, e32, m2, ta, maで始まる11命令の1本だけで、スカラーの後始末がありません。
.LBB0_2:
vsetvli a4, a0, e32, m2, ta, ma
slli a5, a3, 2
add a6, a1, a5
add a5, a5, a2
vle32.v v8, (a6)
vle32.v v10, (a5)
sub a0, a0, a4
vfmacc.vf v10, fa0, v8
vse32.v v10, (a5)
add a3, a3, a4
bnez a0, .LBB0_2
| コンパイラ(-O2) | 本体ループ | スカラーの端数ループ |
|---|---|---|
| clang 22.1.8 | vsetvliで残り数をvlに設定 | なし |
| clang 21.1.0 | vlenbを読みvl2re32.vで割り切れる分 | あり(flw・fmadd.s・fsw) |
| clang 21.1.0+EVL指定 | vsetvliで残り数をvlに設定 | なし |
clang 21.1.0は固定長SIMDと同じ「本体+後始末」の形です。-mllvm -prefer-predicate-over-epilogue=predicate-else-scalar-epilogue -mllvm -force-tail-folding-style=data-with-evlの2つを付けると、22と同じvsetvliのループ1本になりました。-mllvmはLLVM内部のオプションで、版によって名前や既定値が変わるため、製品ビルドに固定で入れるよりclangの版を上げるほうが確実です。V拡張を外したrv64gc向けでは、clang 22.1.8でもfmadd.sの1要素ずつのループのままでした。
ベクトル化されないループの見分け方(-Rpass)
a[i] = a[i-1] * 0.5f + a[i]のように前の要素の結果を次で使うループは、-Rpass-analysis=loop-vectorizeでloop not vectorized: unsafe dependent memory operations in loopと報告されます(clang 22.1.8で確認)。要素間に依存があるループはRVVでも並列化できません。intrinsicsに書き換える前に、このリマークで「書き換えれば並列になる処理か」を確かめてください。GCCでは-fopt-info-vec-missedが同じ役割です。
GCC・Clangの対応版
| intrinsics仕様の版 | Clang | GCC |
|---|---|---|
| v0.11(タプル型なし) | 16 | 13 |
| v0.12 | 17 | - |
| v1.0 | 19以降 | 14以降 |
表はintrinsics仕様のリポジトリの記載です。GCC自身のマニュアルが「批准版1.0に対応」と書くのはGCC 16.1(2026年4月30日)からで、14.1〜15.2のマニュアルは0.11と記載したままです。v0.11にはセグメントロード・ストア用のタプル型や丸めモード引数付きの関数が無いので、前述のrgb2grayのようなコードを書くなら、GCCは手元の版で実際にコンパイルが通るかを先に確かめてください。
自動ベクトル化はGCC 14でV拡張のループ・SLPベクトル化が有効になり、ClangはLLVM 22.1.0(2026年2月24日)で「RISC-Vではループベクトル化が既定でtail foldingを行う」ようになりました。前節のclang 21と22の違いはこの変更です。2026年8月25日公開のLLVM 23.1.0では、SpacemiT X100向けの-mcpu=spacemit-x100が追加されています。これから書くなら、Clang 22以降かGCC 14以降を前提にします。
RVV 1.0と0.7.1・RVA22とRVA23:実機選定で詰まる互換性
0.7.1(XTheadVector)とRVV 1.0のバイナリ非互換
T-Head(XuanTie)のC906はVector 0.7系を実装し、C910の公式Buildrootの実行例もベクトル版を0.7.1と表示します。T-Headはこの旧版をXTheadVectorという非標準拡張として仕様化しており、その仕様書が1.0との差分を列挙しています。vsetvlの符号化とロード・ストア命令が異なり、vsetivliと分数LMULが無く、ta・maは指定できずTAMU固定です。1.0向けにビルドしたバイナリは0.7.1のCPUで動かず、仕様書は「XTheadVectorと標準のV拡張は競合する」と明記しているため、両方を同時に有効にしたビルドもできません。
C906を載せた安価なボードで「RVV対応」とあっても、1.0のコードを試す目的なら選ばないでください。そのボードでも動かすならビルドを分けます。GCC 14はベンダー拡張としてXTheadVectorに対応しています。
RVA22ではオプション、RVA23で必須になったV拡張
プロファイルは、OSやアプリの配布側が「このCPUなら何が使えるか」を前提にするための拡張の組み合わせです。RVA22U64ではV拡張はZfh・Zkn・Zksと並ぶ4つのオプションの1つで、無くてもRVA22準拠でした。RVA23U64(v1.0は2024年10月17日付、RISC-V Internationalの公表は同年10月21日)ではVが必須になり、あわせてZvbb(ベクトルのビット操作)・Zvkt(データ非依存の実行時間)・Zvfhmin(半精度の最小変換)も必須に入りました。
RVA22のCPUでは、V拡張の有無を実行時に確認して分岐するコードが要ります。RVA23を前提にできる環境なら、Vの有無を確かめずにベクトル版を使えます。RVA23準拠を公称するSoCは、2026年にはSpacemiT K3搭載のボードとして一般に購入できるようになりました。
RVV 1.0搭載の実機とVLEN
| SoC・コア | ベクトル仕様 | VLEN | 備考 |
|---|---|---|---|
| SpacemiT K3(X100) | RVV 1.0 | 256ビット | RVA23準拠を公称 |
| SpacemiT K3(A100 AIコア) | RVV 1.0 | 1,024ビット | 同じK3内のAI向けコア |
| SpacemiT K1(X60×8) | RVV 1.0 | 256ビット | RVA22対応を公称 |
| Kendryte K230(C908) | RVV 1.0 | 128ビット | 大コア側がRVV対応 |
| XuanTie C906 | Vector 0.7系 | - | 1.0とは非互換 |
K3の数値はSpacemiTのデータシートによります。搭載ボードは、Banana Piが2026年5月4日にBPI-SM10(K3-CoM260)とK3 Pico-ITXの一般販売を告知しています。1つのSoCの中でもコアによってVLENが256と1,024に分かれるので、VLENを決め打ちしたコードはK3の上でも移植性を失います。
VLENが2倍でも所要時間が半分になるとは限りません。パイプライン構成やメモリ帯域が異なるため、性能比較は同じ入力サイズで測り、SoC名・VLEN・クロック・コンパイラの版を一緒に記録します。
Linuxカーネル6.5以降のユーザー空間ベクトル対応
ベクトルレジスタは大きいため、OSがコンテキストスイッチで保存・復元しないとユーザープログラムは使えません。Linuxでは6.5でユーザー空間のベクトル状態の保存・復元、ELFのHWCAPによる検出、prctlによる有効化の制御が入りました。ボード付属のBSPカーネルはベンダーが独自に機能を取り込んでいる場合があり、版番号だけでは判断できません。BSPのカーネル設定でCONFIG_RISCV_ISA_Vが有効か、/proc/cpuinfoのisa行にvが含まれるかを確認してください。
実機なしで検証する:QEMUでVLENを切り替えるテスト手順
QEMUのRISC-V CPUはv・vlen・elenのプロパティでベクトル構成を変えられます。QEMU 11.1.2でvlen=2048を指定すると、Vector extension implementation only supports VLEN in the range [128, 1024]と表示されて起動しませんでした。ユーザーモードのqemu-riscv64はLinuxホスト用です。macOSやWindowsでは、DockerなどのLinuxコンテナに入れて使います。Alpine Linuxならapk add qemu-riscv64の1パッケージで済みます。
検査用のプログラムは、要素ごとに値が異なる入力でsaxpyの結果をfmafで作った参照値と比べ、配列長を0・1・端数・1,000と変えて確かめる形にします。不一致なら終了コード1で止まります。
#include <riscv_vector.h>
#include <stddef.h>
#include <stdio.h>
#include <math.h>
static void saxpy(size_t n, float a, const float *x, float *y) {
for (size_t vl; n > 0; n -= vl, x += vl, y += vl) {
vl = __riscv_vsetvl_e32m8(n);
vfloat32m8_t vx = __riscv_vle32_v_f32m8(x, vl);
vfloat32m8_t vy = __riscv_vle32_v_f32m8(y, vl);
vy = __riscv_vfmacc_vf_f32m8(vy, a, vx, vl);
__riscv_vse32_v_f32m8(y, vy, vl);
}
}
int main(void) {
static float x[1000], y[1000], ref[1000];
const size_t lens[] = {0, 1, 3, 31, 33, 64, 65, 127, 1000};
for (size_t t = 0; t < sizeof lens / sizeof lens[0]; t++) {
size_t n = lens[t];
for (size_t i = 0; i < n; i++) {
x[i] = (float)i * 0.5f;
y[i] = (float)i * 0.25f + 1.0f;
ref[i] = fmaf(2.0f, x[i], y[i]);
}
saxpy(n, 2.0f, x, y);
for (size_t i = 0; i < n; i++) {
if (y[i] != ref[i]) {
printf("NG n=%zu i=%zu\n", n, i);
return 1;
}
}
}
puts("OK");
return 0;
}
Python仮想環境でpython3 -m pip install ziglang==0.17.1を実行し、コードをsaxpy_test.cとして保存して静的リンクでビルドします。clangではRISC-V向けのsysroot(Debian系ならgcc-riscv64-linux-gnuパッケージ)が要りますが、zig ccはmuslを同梱しているのでそのままリンクできます。
python3 -m ziglang cc -target riscv64-linux-musl -mcpu=generic_rv64+m+a+f+d+c+v -O2 -static saxpy_test.c -o saxpy_test
qemu-riscv64 -cpu rv64,v=true,vlen=128 ./saxpy_test
qemu-riscv64 -cpu rv64,v=true,vlen=256 ./saxpy_test
qemu-riscv64 -cpu rv64,v=true,vlen=1024 ./saxpy_test
qemu-riscv64 -cpu rv64,v=true,vlen=128,rvv_vl_half_avl=on ./saxpy_test
VLENを変えるだけでは、vlをVLMAXより小さく返す実装は再現できません。最後の行のrvv_vl_half_avl=onは、残りがVLMAXの2倍未満のときにvlをceil(AVL÷2)にするQEMUのプロパティ(既定はoff)です。確かめるために、ループの進め方だけを「1周目は必ずVLMAX個」と決め打ちした版を作りました。
const size_t vlmax = __riscv_vsetvlmax_e32m8();
for (size_t vl; n > 0; n -= (n < vlmax ? n : vlmax), x += vlmax, y += vlmax) {
vl = __riscv_vsetvl_e32m8(n);
/* 以下は正しい版と同じ */
zig 0.17.1でビルドし、Alpine Linux上のQEMU 11.1.2で実行した結果です。
| -cpuの指定 | 正しい版 | 決め打ち版 |
|---|---|---|
| rv64,v=true,vlen=128 | OK | OK |
| rv64,v=true,vlen=256 | OK | OK |
| rv64,v=true,vlen=1024 | OK | OK |
| vlen=128,rvv_vl_half_avl=on | OK | NG n=33 i=17 |
| vlen=1024,rvv_vl_half_avl=on | OK | NG n=1000 i=756 |
| rv64,v=false | Illegal instruction(signal 4) | 未実行 |
決め打ち版はVLENをどう変えても通り、rvv_vl_half_avl=onのときだけ落ちました。VLEN 128ビット・LMUL=8ではVLMAXが32なので、n=33は「VLMAXより多く2倍未満」に当たり、vlは17になります。決め打ち版はポインタを32進めるため、要素17〜31を飛ばしたところで不一致になりました。テストはvlenの3通りにrvv_vl_half_avl=onを加えた4通りで回してください。最終行のように、V拡張の無いCPUではベクトル命令に当たった時点で不正命令で止まります。V無しのCPUにも配るバイナリでは、前述の実行時判定が要ります。QEMUの実行時間は実機の性能を表さないので、QEMUで確かめるのは正しさまでにします。
RVVを使う場面・見送る場面:採否の判断基準
効果が出る処理:要素が独立した長い配列と暗号処理
RVVが効くのは、各要素の計算が互いに依存せず、多数の要素をまとめて処理するループです。行列積、畳み込み、画像フィルタ、音声のFIRフィルタ、量子化済みモデルの推論ループがこれに当たります。8ビット整数ならVLEN 256ビット・LMUL=8で1命令256要素を扱えます。
暗号処理には、2023年10月9日に批准版1.0.0が公開されたベクトル暗号拡張があり、Zvkned(AES)、Zvknha(SHA-256)、Zvknhb(SHA-256とSHA-512)などの専用命令を使えます。RVA23ではZvkng・Zvksgがオプション扱いなので、使う前にCPUの対応を確認します。推論の負荷が大きい場合はNPUとCPU・GPUの役割分担も検討してください。センサーの信号処理や推論をエッジ機器に載せる開発は、一相のAI/IoTソリューションでも支援しています。
見送る条件:0.7.1のCPU・依存のあるループ・メモリ帯域律速
次のどれかに当てはまるなら、intrinsicsで書く工数はかけません。
- 対象CPUがC906などVector 0.7系しか持たない。1.0向けのバイナリは動かず、0.7.1向けに別のビルドと検証が要ります。
- ループに要素間の依存がある(前の結果を次で使う、ポインタを辿る)。
-Rpassで依存が報告されたループは、アルゴリズムを変えない限り並列になりません。 - 1要素あたりの計算が足し算1回程度で、データ量がキャッシュに収まらず、スカラー版の時点で実測のメモリ転送量が帯域の上限近くに達している。この状態では演算をベクトル化しても所要時間はほとんど変わりません。
- 配列が短く、対象の実機で測るとベクトル版がスカラー版より速くならない。
vsetvliなどの準備命令の固定費に対して処理量が足りない場合に起きます。
判断の手順は、プロファイラで処理時間の大半を占めるループを特定し、そのループで自動ベクトル化が効いているかを-Rpassで確かめ、効いていない理由が「依存」でなければintrinsicsで書く、です。
RVVについてよくある質問
RVVとは何の略ですか?
RISC-V Vector Extensionの略で、拡張名は「V」です。1.0版が2021年11月に批准されました。ISA文字列ではrv64gcvのようにVを含めて表し、RVA23プロファイルでは必須拡張です。
VLENとは何ですか?VLMAXとの違いは?
VLENはベクトルレジスタ1本のビット数で、CPUの実装ごとに決まる定数です。V拡張では128ビット以上の2の累乗で、Kendryte K230の大コアは128ビット、SpacemiT K1は256ビット、K3はX100が256ビット、A100が1,024ビットです。VLMAXは1命令の最大要素数で、VLMAX=LMUL×VLEN÷SEWで計算します。VLENは固定、VLMAXはvsetvliの設定で変わります。
可変長ベクトル方式と固定長SIMDの違いは何ですか?
固定長SIMD(SSE・AVX2・NEON)は命令ごとに幅が決まります。例えばAVX2のコードはAVX-512対応CPUでも動きますが、512ビット幅を使うにはAVX-512命令を使います。可変長ベクトル方式(RVV・SVE)はコードに幅を書かず、実行時にCPUが処理数を返すので、同じバイナリが幅の違うCPUで動きます。
RVVとArm SVEはどちらも可変長ですが、何が違いますか?
どちらも長さ非依存のコードを書ける点は同じです。SVEは処理する要素を述語レジスタ(要素ごとの有効ビット)で表し、ベクトル長は128〜2,048ビットです。RVVは処理数そのものをvlに持ち、LMULで最大8本のレジスタを束ねて1命令の幅を広げられます。RVVのマスクはSVEの述語に近い役割ですが、マスク付き実行で指定できるレジスタはv0だけです。
RVV 0.7.1と1.0に互換性はありますか?
ありません。vsetvlの符号化やロード・ストア命令が異なり、1.0向けのバイナリは0.7.1のCPU(XuanTie C906など)では動きません。新規開発は1.0を前提にし、0.7.1のボードは1.0の検証に使わないでください。