正確に言えば、RISC-Vにはx86のFlagsのような演算フラグレジスタは無い、というべきでしょうか。RISC-Vにも制御フラグは制御レジスタの中にあります。それでも、比較命令やって、その後分岐命令やって、みたいな古い頭のコーディングに慣れていると、最初は戸惑います。でも Flags なんて無きゃ無いで済んだんだ。。。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
x86のFlagsレジスタの下8ビットが良い例(悪いケース)だと思うのですが、キャリー、パリティ、補助キャリー(昔の言い方)、ゼロ、サインと5つも演算フラグが並んでいます。演算命令により、それらフラグの幾つかが更新されたり、されなかったり。レジスタの更新とは別なロジックの「副作用」的存在。まあこの部分をさかのぼって行くと初代8086どころか8ビットにまで行き着くので致し方ありません。
フラグは歴史を引きづっているだけでなく、プログラムの機械語実行履歴を引きずる存在でもあるので、「スペキュラティブ」な実装など考えるとメンドイに違いありません。
その点RISC-Vはバッサリです。演算結果をフラグに反映するというのを止めてます。じゃ、分岐はどうするのかと言えば、
条件分岐命令は自分でレジスタの内容を判断して分岐する
のです。その場で判断して分岐するので演算フラグのような存在はアカラサマには登場しません。なお、条件分岐については次回取り上げさせていただく予定であります。今回は「分岐をしない」比較命令を動かして見てみたいと思います。
毎度の参照資料へのリンクを再掲載いたします。
RISC-Vの比較命令
条件分岐命令が自身で比較演算を行ってしまうので、x86のCMP命令(コンペア命令)みたいな命令と、RISC-Vの比較命令の趣は異なります。x86のCMP命令が「レジスタの内容はそのままにフラグだけ操作する」命令であるのに比べると、RISC-Vの比較命令は「比較結果の真偽値をレジスタに格納する」ための命令です。C言語などでも真偽値をいったん変数に格納しておくことはあると思います。この手の命令が無いと不便でしょう。2つのソース・レジスタまたは一つのソース・レジスタと即値を比較して結果をデスティネーション・レジスタに書き込むことができます。ただし、条件分岐に比べると明らかに使用頻度は低そうです。そのため結構冷遇されています(個人の感想です。てへぺろ。)
-
- 比較命令にはRV32Cの16bit短縮命令は存在しない。いつもRV32Iの32bit命令が生成される
- 比較には符号付きと符号無の両方があるが、比較は小なりしかない。
レジスタオペランドをとるので、レジスタを交換すれば「大なり」は「小なり」に変換できるよね、引き算して結果がゼロなら一致(イコール)は分かるよね、という割り切りのようです。RISC-V、オペコード空間の節約には貪欲です。
ソースコード
さて、実際に比較命令を使うアセンブラ関数を書いてみました(差分のみ、全文は第28回に掲載。)
いつもはスタックフレームを用意して、戻り値のリンクレジスタを保存、末尾で戻すみたいな「型」を踏襲していたのですが、今回は1命令、1関数なので「型」を踏み外しました。命令1個やってすぐretです(以前やりましたが、RISC-Vで ret は疑似命令<表記上の命令で、実体は命令は違う>です。)
関数を呼び出すとレジスタ a0, a1, a2…という具合に引数が渡されます。その間で比較演算をして結果を戻しています。戻り値はa0です(よって引数のときのa0の値は無視されます。)
肝心なことをここまで書き忘れていました。slt = set less than の略です。後ろに i がつくと即値をオペランドにとり、uであると符号無比較を行います(u無なら符号付き。)
slt1: slt a0, a1, a2 ret slti1: slti a0, a1, 1 ret sltu1: sltu a0, a1, a2 ret sltiu1: sltiu a0, a1, 1 ret
いつもだと、オペランドのレジスタを変更してみて、RV32Cのコードが生成されたとかRV32Iのままだとか、オブジェクトコードをダンプして確認するのですが今回は省略です。みな32ビット幅のRV32I命令です。
Cのmain()関数から呼び出すのに使っているヘッダファイルに書き加えた関数プロトタイプは以下のようです。結果は bool としたいところですが、Cなので、「テキトー」に int としておきました。
int slt1(int32_t arg0, int32_t arg1, int32_t arg2); int slti1(int32_t arg0, int32_t arg1); int sltu1(uint32_t arg0, uint32_t arg1, uint32_t arg2); int sltiu1(uint32_t arg0, uint32_t arg1);
Cのmain()関数内の「アンダーテスト」部分が以下です。
a0 = 0; a1 = -1; a2 = -4; printf("slt %d < %d : %d\n", a1, a2, slt1(a0, a1, a2)); a0 = 0; a1 = -4; a2 = -1; printf("slt %d < %d : %d\n", a1, a2, slt1(a0, a1, a2)); a0 = 0; a1 = 1; printf("slti %d < 1 : %d\n", a1, slti1(a0, a1)); a0 = 0; a1 = -1; printf("slti %d < 1 : %d\n", a1, slti1(a0, a1)); a0 = 0; a1 = 1; a2 = 4; printf("sltu %d < %d : %d\n", a1, a2, slt1(a0, a1, a2)); a0 = 0; a1 = 4; a2 = 1; printf("sltu %d < %d : %d\n", a1, a2, slt1(a0, a1, a2)); a0 = 0; a1 = 1; printf("sltiu %d < 1 : %d\n", a1, sltiu1(a0, a1)); a0 = 0; a1 = 0; printf("sltiu %d < 1 : %d\n", a1, sltiu1(a0, a1));
実行結果
これまたいつもだと、アセンブラ関数の中をステップ実行して、レジスタが書き換えられる様子を追うのですが、今回は1命令毎に戻ってくるので、それも省略です。
動作結果はこちら。
slt -1 < -4 : 0 slt -4 < -1 : 1 slti 1 < 1 : 0 slti -1 < 1 : 1 sltu 1 < 4 : 1 sltu 4 < 1 : 0 sltiu 1 < 1 : 0 sltiu 0 < 1 : 1
比較した結果の真偽値(0が偽、1が真)が戻ってきてます。OKっと。
次回こそ、分岐命令です。