RVV Intrinsicsの一部紹介 その1

RISC-V "V" Vector Extension Intrinsicsについて

risc-vにはベクトル拡張というものがあり、現在公式に批准されるために評価されている段階で、仕様も凍結されている。そのため、ベクトル拡張が正式にrisc-vの拡張命令として認められる日もそう遠くはないだろう。そんなベクトル命令をアセンブリとして書くことができるのはもちろん、高級言語からその命令を使うように指示することができる。つまり、高級言語コンパイル結果としてベクトル命令を吐いてくれるように高級言語から指定できるのだ。これによりコンパイラが最適化を頑張らなくても高級言語プログラマが頑張ることで、より実行性能が高いアセンブリを出力できるようになる。これがIntrinsicsである。これから例を交えながら紹介していく。

rvv_strlen.c

文字列の長さを測るstrlen.cをrvv(risc-v vector exetension) intrinsicを使って書いたコードがgithubに例として上がっているので、それを以下に示す。

#include "common.h"
#include <riscv_vector.h>
#include <string.h>

// reference https://github.com/riscv/riscv-v-spec/blob/master/example/strlen.s
size_t strlen_vec(char *src) {
    // このアーキテクチャが扱える8bitデータの最大の要素数を取得
    // m8としているのは8個のレジスタをグルーピングして一度に使おうとしている
    size_t vlmax = vsetvlmax_e8m8();
    // 文字列があるアドレスに対するポインタ
    char *copy_src = src;
    long first_set_bit = -1;
    
    // 実際に一つのループで扱う要素数
    size_t vl;
    while (first_set_bit < 0) {
        // Unit-stride Fault-Only-First Loads Functions
        /* vlmaxだけ読み込もうとして、アクセス禁止領域へのアクセスのような例外が発生する
       ようなインデックスに来たら、実際には例外は発生させずに読み込みを終了してvlに例外を
       発生させたインデックス(読み込めた要素数)を書き込む。*/
        // 最初の要素が例外を発生させるような場所だったら実際に例外を発生させる。
        // これが最後のループなら"hogefuga($0)"のように文字列の終端記号もvlは含めている
        /* メモリにはデータが連続して格納されている可能性があるので、
       vec_src = "hoge($0)fuga"のようになっている可能性もある。*/
        // 読み取った文字(8bit)がvec_srcに代入されていく。
        vint8m8_t vec_src = vle8ff_v_i8m8(copy_src, &vl, vlmax);
        
        // ベクトル整数比較命令
        // seq = set if equal
        // 文字列の終端記号の検出。
        // vx とあるので、ベクトルとスカラレジスタの値を比較して等しいと1をセット
        // vec_srcの中をvlの数だけ調べる
        // vbool1_tとあるのは、n(1) = SEW(i8) / LMUL(m8)ゆえ
        vbool1_t string_terminate = vmseq_vx_i8m8_b1(vec_src, 0, vl);
        
        // このループで扱ったデータ数だけポインタを進める
        copy_src += vl;
        
        /* string_terminateから1を持つ要素(終端記号がある位置)を探してその中で
       最も低い番号を出力 */
        // 最後のループの場合は、結果的にこのループ内で処理した対象の文字列の文字数を出力
        // 1がない場合は-1を出力
        first_set_bit = vfirst_m_b1(string_terminate, vl);
    }
    copy_src -= vl - first_set_bit;
    return (size_t)(copy_src - src);
}

int main() {
    const uint32_t seed = 0xdeadbeef;
    srand(seed);
    
    int N = rand() % 2000;
    
    // data gen
    char s0[N];
    gen_string(s0, N);
    
    // compute
    size_t golden, actual;
    golden = strlen(s0);
    actual = strlen_vec(s0);
    
    // compare
    pust(golden == actual ? "pass" : "fail");
}

わかったこと

  • vlの更新は基本的に、vsetとfault-only-firstでのみ行われる。大体ループの最初でvsetによってvlをセットして、ループを回っていく中で、どれだけロードできるかでvlの値を更新していくという流れ。
  • 文字列の終端の判定は、char型(8 bit)の0(終端文字列)の検出という形で行っている。
  • セキュリティに配慮してアクセス禁止領域にアクセスするようなロードを行わないようにFault-Only-First Loadsというロード命令が使われている。

その他

  • 上を書いた後に気づいたが、この記事がintrisicの利用例をexampleに沿ってわかりやすく紹介しているので、非常におすすめ。

参考にした記事