ぐだぐだ低レベルプログラミング(38) RISC-V、mulとdiv、RV32M拡張その1

Joseph Halfmoon

今回のRV32Mは拡張命令セットといいつつ、ほとんどの実装で採用されている筈のもの。整数掛け算と割り算であります。しかし命令が存在しても何サイクルで処理できるのかは実装依存です。まずはmulとdiv命令を使ってみて処理サイクル数を数えてみるところから始めました(前にもやったっけ?忘れているからいいか。)

毎回の参照資料へのリンクの再掲載です。

  • RISC-Vの公式ドキュメンテーションはこちら
  • 実験に使用しているGD32VF103VBT6開発ボードはこちら
RISC-Vの整数乗算、除算命令セット

RISC-Vの基本命令セット(必須)であるRV32Iには、乗算、除算は含まれません。つまり掛け算、割り算が無くても胸を張ってRISC-Vだ、と言えるということであります。昔のよゐこ(Z80なんかの時代の子、いまではジジイかオバアか)は、誰もが乗除算ルーチンなどを自作していた筈なので、何の問題もありませぬ?

しかし、この頃は乗除算はあって当然。ということで拡張命令セットである筈のRV32MはほとんどのRISC-V実装に取り入れられているのではないかと思います。GigaDevice社GD32VF103(RISC-VコアはNucleiと台湾Andes Technology合作らしい)についても例外ではありません。RV32Mの概要は以下のとおり

  1. 整数乗算、除算、剰余あり
  2. 符号付き、符号無しに対応
  3. 結果は常に32ビット幅。64ビット幅の結果を得たい場合は2命令組わせる

RV32Mの全命令は次回で並べて実験してみる予定です。今回は、まずは乗算、除算にどのくらいのサイクル数がかかるのか内蔵のサイクルカウンタを使って確認しておきたいと思います。命令セットは規格で決まっているけれども、その性能は実装次第ということで。

実験用のアセンブラ関数

以下に実験で使用したアセンブラ関数のコードを示します。

  1. 計測対象の mul または div 命令を、サイクルカウンタ読み出し命令で挟む
  2. 前後のサイクルカウンタの差をとれば、計測対象+サイクルカウンタ読み出し命令分のサイクル数が求まる
  3. 計測対象のmulまたはdiv命令を2つ重ねたものについても同様に測定する
  4. 第3の結果と第2の結果の差こそ、mulまたはdiv1命令の実サイクル数である

という目論見。テストするのは、多数ある乗除算系命令の中の基本

  • mul命令、32ビット符号付き数を乗じた結果の下32ビットを返す
  • div命令、32ビット符号付き数を32ビット符号付き数で割った結果の商(ゼロ方向に丸め)を返す

の2つです。前述のサイクル計測用に命令1個と命令2個の区別があるので、合計4種類となったアセンブラ関数は以下です。

div1:
    addi    sp,sp,-16
    sw      ra, 12(sp)

    rdcycle  t0
    div      a0, a1, a2
    rdcycle  t1
    sub      t1, t1, t0
    sw       t1, 0(a3)

    lw      ra, 12(sp)
    addi    sp,sp,16
    ret

div2:
    addi    sp,sp,-16
    sw      ra, 12(sp)

    rdcycle  t0
    div      a0, a1, a2
    div      a0, a1, a2
    rdcycle  t1
    sub      t1, t1, t0
    sw       t1, 0(a3)

    lw      ra, 12(sp)
    addi    sp,sp,16
    ret

mul1:
    addi    sp,sp,-16
    sw      ra, 12(sp)

    rdcycle  t0
    mul      a0, a1, a2
    rdcycle  t1
    sub      t1, t1, t0
    sw       t1, 0(a3)

    lw      ra, 12(sp)
    addi    sp,sp,16
    ret

mul2:
    addi    sp,sp,-16
    sw      ra, 12(sp)

    rdcycle  t0
    mul      a0, a1, a2
    mul      a0, a1, a2
    rdcycle  t1
    sub      t1, t1, t0
    sw       t1, 0(a3)

    lw      ra, 12(sp)
    addi    sp,sp,16
    ret
アセンブラ関数の呼び出し側ソース

ヘッダファイルに用意したアセンブラ関数呼び出し用の関数プロトタイプは以下のとおりです。

  • 32ビット符号付きの引数 a0, a1, a2がアセンブラレベルでレジスタa0, a1, a2の初期値となります。
  • a0に呼び出し時におく値は単純に上書きされるだけのダミーです。
  • 第4の引数に与えるポインタの指す先に、測定したサイクル数が格納されます。
int32_t div1(int32_t a0, int32_t a1, int32_t a2, int* adr);
int32_t mul1(int32_t a0, int32_t a1, int32_t a2, int* adr);
int32_t div2(int32_t a0, int32_t a1, int32_t a2, int* adr);
int32_t mul2(int32_t a0, int32_t a1, int32_t a2, int* adr);

今回はサイクル数を測定するのがメインなので、乗除算の引数は安全運転で正の整数ばかりです。掛け算は32ビット幅に収まるし、割り算は割り切れるっと。

  • 0x789A かける 0x1234 = 0x8935348
  • 0x789A 割る 0x1234 = 6

テスト用のコードがこちら。

int32_t a0=0;
int32_t a1=0x789A;
int32_t a2=0x1234;
int cyc1=0;
int cyc2=0;
int32_t resultM1 = mul1(a0, a1, a2, &cyc1);
int32_t resultM2 = mul2(a0, a1, a2, &cyc2);
printf("mul1   = 0x%08x (%d)\n", (unsigned int)resultM1, cyc1);
printf("mul2   = 0x%08x (%d)\n", (unsigned int)resultM2, cyc2);
a0=0;
cyc1=0;
cyc2=0;
int32_t resultD1 = div1(a0, a1, a2, &cyc1);
int32_t resultD2 = div2(a0, a1, a2, &cyc2);
printf("div1   = 0x%08x (%d)\n", (unsigned int)resultD1, cyc1);
printf("div2   = 0x%08x (%d)\n", (unsigned int)resultD2, cyc2);
実機(GD32VF103)上の実行結果

以下の出力の()内に記された10進数値が所要サイクル数です。

MulDivResult上記から、

  1. 32ビット乗算 3-2= 1サイクル
  2. 32ビット除算 75-38= 37サイクル

という値を得ました。乗算器は搭載しているけれども除算はフツーにシフトして1ビットづつ処理している感じかね。この辺はハードウエアの除算器を搭載しているラズパイPicoのRP2040の方が上かもしれないっす(でもRP2040の除算性能をちゃんと計ってないっす。)どうせ除算、よゐこは滅多に使わないか?そうでもないか?

ぐだぐだ低レベルプログラミング(37) RISC-V、無条件JMPもRETも皆CALL へ戻る

ぐだぐだ低レベルプログラミング(39) RISC-V、mul、RV32M拡張その2 へ進む