FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

"Creating an LLVM Backend for the Cpu0 Architecture"をやってみる(12. Int型以外のサポート)

f:id:msyksphinz:20181123225150p:plain

Cpu0のバックエンドをLLVMに追加するプロジェクト、7章では、これまでint型だけであった型のサポートを拡張する。

今回も非常にファイル量が多くて、とりあえずLLVMをビルドするためだけにパッチを作って当てているが、LLVM 7.0は未サポートになっている部分が多く、ソースコードを書き直す必要があった。

  • Chapter7の実装を追加したもの

github.com

ローカル変数ポインタのサポート

ローカル変数のポインタを扱うためには、EffectiveAddressを計算する要素が必要になる。

class EffectiveAddress<string instr_asm, RegisterClass RC, Operand Mem> :
  FMem<0x09, (outs RC:$ra), (ins Mem:$addr),
     instr_asm, [(set RC:$ra, addr:$addr)], IIAlu>;
}

char, short, int, bool型のサポート

それぞれの型をサポートするために以下のdefを追加する。 lb/lhは符号付バイト型、符号付ショート型のための命令であり、lbu/lhuは符号なしバイト型、符号なしショート型のための命令である。

let Predicates = [Ch7_1] in {
defm LB     : LoadM32<0x03, "lb",  sextloadi8>;
defm LBu    : LoadM32<0x04, "lbu", zextloadi8>;
defm SB     : StoreM32<0x05, "sb", truncstorei8>;
defm LH     : LoadM32<0x06, "lh",  sextloadi16_a>;
defm LHu    : LoadM32<0x07, "lhu", zextloadi16_a>;
defm SH     : StoreM32<0x08, "sh", truncstorei16_a>;
}

long long型

Long Longをサポートするために、SHL_PARTS, SRA_PARTS, SRL_PARTSをサポートする。

  // Handle i64 shl
  setOperationAction(ISD::SHL_PARTS,          MVT::i32,   Expand);
  setOperationAction(ISD::SRA_PARTS,          MVT::i32,   Expand);
  setOperationAction(ISD::SRL_PARTS,          MVT::i32,   Expand);

配列と構造体のサポート

配列と構造体は、Chapter7_1の実装ですでにサポートされている。

Vector型(SIMD)のサポート

Vector型は、型としては一応サポートされているが、結果的にはひたすらLoadを繰り返して演算するというコードが生成される。

typedef long   vector8long   __attribute__((__vector_size__(32)));
typedef long   vector8short   __attribute__((__vector_size__(16)));


int test_cmplt_short() {
  volatile vector8short a0 = {0, 1, 2, 3};
  volatile vector8short b0 = {2, 2, 2, 2};
  volatile vector8short c0;
  c0 = a0 < b0; // c0[0] = -2147483647=0x80000001, c0[1] = -2147483647=0x80000001, c0[2] = 0, c0[3] = 0
  
  return (int)(c0[0]+c0[1]+c0[2]+c0[3]); // 2
}


int test_cmplt_long() {
  volatile vector8long a0 = {2, 2, 2, 2, 1, 1, 1, 1};
  volatile vector8long b0 = {1, 1, 1, 1, 2, 2, 2, 2};
  volatile vector8long c0;
  c0 = a0 < b0; // c0[0..3] = {0, 0, ...}, c0[4..7] = {-2147483647=0x80000001, ...}
  
  return (c0[0]+c0[1]+c0[2]+c0[3]+c0[4]+c0[5]+c0[6]+c0[7]); //4
}

Chiselを使って波形を全く使わずにRISC-Vパイプラインプロセッサを設計した

タイトルの通りなのだが、Chiselというハードウェア記述言語を用いて、5ステージのRISC-Vパイプラインプロセッサの設計に挑戦していた。

プロセッサをはじめとするハードウェア設計は、大体の場合はVerilog-HDLやVHDLなどのハードウェア記述言語を使用するか、最近だと高位合成言語を使うケースがある。 ハードウェア記述言語を使う場合、主にデバッグをする際には波形をダンプして、波形を確認しながらデバッグを行うことになる。

ただし、Verilog-HDLやVHDLをはじめとするRTLシミュレータは、ソフトウェアシミュレーションに比べて動作が重いし、市販のもの(といってもSynopsys, Cadence, Mentorくらいしか無い)のものはライセンス料が非常に高い。個人でRTL開発するのに、RTLシミュレータにお金を払うことは不可能だ。

個人で使うならばMentorのQuestaSimの無料版を使うか、Verilatorを使うということになる。 (後述するがChiselで設計したハードウェアは、そのままのフローでVerilatorでシミュレーションすることができる)

そんな中でChiselだ。ChiselはScalaというソフトウェア言語を用いてハードウェアを設計する。 結論から言うとScalaというソフトウェア言語を用いているにもかかわらず、コーディング時はハードウェア脳で考えないと全くうまく書けない。 だからソフトウェア言語と言いつつハードウェアの気分でコーディングしなければならないのだが、利点としてはVerilogではない(最終的にVerilogに変換するが)ので、RTLシミュレータを使わなくてもシミュレーションができるという点だ。

このため、RTLシミュレータの、

  • ライセンスに数の限りがあるためリグレッションテスト時の並列性に制限がある
  • RTLシミュレーションはそもそも遅い

という問題をChiselが解決できるのではないか、と考えて、「Verilogを使わず、全てChiselを使ってパイプラインプロセッサの開発を行う」というものに挑戦し、どのような問題があるのかについて調査することにした。

実装したのは、RISC-Vプロセッサで非常に単純なパイプラインを持っている。5ステージでサポートしているISAはRV32I(非常に基本的な整数命令のみ)で、たぶんこれだったらVerilog-HDLで書くと目を閉じていても設計できるレベルのCPUだ。 GitHub上に公開しているので、興味があったら使ってみてほしい。

github.com

f:id:msyksphinz:20181225224831p:plain
設計した5ステージ RISC-V パイプラインプロセッサ

ただし、今回はChiselというハードウェア記述言語を使う。ChiselはScalaをベースにしてシミュレーションまでできるので、せっかくだから波形を全くダンプせずに、ScalaだけでどこまでCPUを作ることができるのか、挑戦してみることにした。

といっても、すぐに5ステージのパイプラインを作るのは、Chiselの知識も乏しいため自信がない。 そこで、以下のフローを踏んでCPUを設計することにした。

  1. ステートの存在しない、パイプラインの存在しないCPUを設計し、機能的に正しいことを検証する(ここでリグレッションテスト環境を立ち上げる)。
  2. 機能的に正しいことを確認しつつ(リグレッションを通しながら)、ステージを切っていって5ステージパイプラインプロセッサに拡張する。

まず、RISC-Vのテストパタンriscv-testsはセルフチェックのテストなので、全てのテストが成功すると、特定のメモリに特定の値を書いて終了するので、これを検知すればよい。 そうでなければ無限ループに入るか、特定のメモリに違う値を書いて終了するので、それを見ればよい。

で、波形を見ずにどのようにデバッグするかだが、毎サイクル実行した命令と、レジスタへの書き込み情報、メモリアクセスをした情報を1行にしてダンプし、ファイルに書き出す。 これをISSの実行結果と突き合わせて、どこが間違えたかを確認するのが基本となる。

以下のようなトレースファイルをChiselで生成し、これをISSと突き合わせた。

実際にこのようなトレースファイルを出力するためにいろいろと苦労したのだが、まずChisel上ではまともなprintf()が使えない。使えても"%08x"のような高度な書き方ができなかったり、stdoutにしか出力されないのでデバッグが死ぬほどやりにくい。 そこで、DebugPortを宣言してCPUの入出力ポートと別に、シミュレーション時にだけ登場するデバッグポートを生成し、CPU本体ではなく、テストベンチにCPUの情報を取り込んで出力する機構を用意する。一度Chiselから、外のScalaのテストベンチに出てしまえば、あとはJavaの機能などをふんだんに使用できるので、ファイルの書き出しなども思いのままに可能となる (その環境を作るのも結構大変だったが)。

f:id:msyksphinz:20181224132702p:plain
RISC-V CPUに仕込んだ、シミュレーション時にのみ使用するデバッグポートの概要

デバッグポートは以下のようにして記述した。config.debugが有効ならば、デバッグポートが有効になり、そうでないときは全てビット幅が0に設定される。 Chiselの仕様は良く分からないが、どうもビット幅を0にするとそのポートは宣言されないらしい。

class CpuDebugMonitor [Conf <: RVConfig](conf: Conf) extends Bundle {
  val inst_fetch_req    = if (conf.debug == true) Output(Bool())                 else Output(UInt(0.W))
  val inst_fetch_addr   = if (conf.debug == true) Output(UInt(conf.bus_width.W)) else Output(UInt(0.W))
  val inst_fetch_ack    = if (conf.debug == true) Output(Bool())                 else Output(UInt(0.W))
  val inst_fetch_rddata = if (conf.debug == true) Output(SInt(32.W))             else Output(SInt(0.W))

  val pc_update_cause = if(conf.debug == true) Output(UInt(3.W)) else Output(UInt(0.W))

  val inst_valid = if (conf.debug == true) Output(Bool())                  else Output(UInt(0.W))
  val inst_addr  = if (conf.debug == true) Output(UInt(conf.bus_width.W))  else Output(UInt(0.W))
...

以下のようにしてテストベンチ上でトレースポートを引き出し、ファイルに書き出す。

  val writer = new PrintWriter(new File(pipename))
...
      val inst_fetch_req           = peek(cpu_tb.io.dbg_monitor.inst_fetch_req)
      val inst_fetch_addr : Long   = peek(cpu_tb.io.dbg_monitor.inst_fetch_addr).toLong
...
      if (inst_fetch_req == 1) { writer.printf("[%08x]".format(inst_fetch_addr)) }
      else                     { writer.printf("          ") }

これにより、以下のようなトレースファイルを生成した。これでCPUの内部動作を監視できるようになる。

         2 : x00<=0x0000000000000004 ( 0, 0000000000000000, 0000000000000000)                                : 0x00000000 : INST(0x04c0006f) : j       pc + 0x4c
         4 : x10<=0x0000000000000000 (21, 0000000000000000, 0000000000000000)                                : 0x0000004c : INST(0xf1402573) : csrr    a0, mhartid
         5 :                                                                                                 : 0x00000050 : INST(0x00051063) : bnez    a0, pc + 0
         6 : x05<=0x0000000000000054 ( 1, 0000000000000000, 0000000000000054)                                : 0x00000054 : INST(0x00000297) : auipc   t0, 0x0
         7 : x05<=0x0000000000000064 ( 1, 0000000000000054, 0000000000000010)                                : 0x00000058 : INST(0x01028293) : addi    t0, t0, 16
         8 : x00<=0x0000000000000000 (21, 0000000000000064, 0000000000000000)                                : 0x0000005c : INST(0x30529073) : csrw    mtvec, t0
         9 : x00<=0x0000000000000000 (21, 0000000000000000, 0000000000000000)                                : 0x00000060 : INST(0x18005073) : csrwi   satp, 0
        10 : x05<=0x0000000000000064 ( 1, 0000000000000000, 0000000000000064)                                : 0x00000064 : INST(0x00000297) : auipc   t0, 0x0
        11 : x05<=0x0000000000000080 ( 1, 0000000000000064, 000000000000001c)                                : 0x00000068 : INST(0x01c28293) : addi    t0, t0, 28
        12 : x00<=0x0000000000000064 (21, 0000000000000080, 0000000000000064)                                : 0x0000006c : INST(0x30529073) : csrw    mtvec, t0
        13 : x05<=0xffffffffffffffff ( 1, 0000000000000000, ffffffffffffffff)                                : 0x00000070 : INST(0xfff00293) : li      t0, -1
        14 : x00<=0x0000000000000000 (21, ffffffffffffffff, 0000000000000000)                                : 0x00000074 : INST(0x3b029073) : csrw    pmpaddr0, t0
        15 : x05<=0x000000000000001f ( 1, 0000000000000000, 000000000000001f)                                : 0x00000078 : INST(0x01f00293) : li      t0, 31
        16 : x00<=0x0000000000000000 (21, 000000000000001f, 0000000000000000)                                : 0x0000007c : INST(0x3a029073) : csrw    pmpcfg0, t0
        17 : x05<=0x0000000000000080 ( 1, 0000000000000000, 0000000000000080)                                : 0x00000080 : INST(0x00000297) : auipc   t0, 0x0

5ステージパイプラインへの拡張

ここから、5ステージのパイプラインに拡張していくのだが、これも同様に波形をダンプしない。 波形をダンプしない代わりに、ファイルにパイプラインをどんどん書き出していき、各ステージの状況がどのようになっているのかテキストファイルを見ればわかるようにする。

f:id:msyksphinz:20181222194054p:plain
5ステージパイプラインCPUのパイプライントレース

これで、ALUの演算の種類、ALUに入力されたオペランドレジスタリードでの読み込みデータ、レジスタの書き込み情報、ストールの発生状況などを確認する。

パイプラインのフォワーディングミスとか、レジスタ書き込みのミスとか確認するのはちょっとつらかったが、Chiselのコードを冷静に見直しながらシミュレーションすることで、無事にすべてのリグレッションテストを通すことができた。

デバッグに波形を使わないことで、「とりあえず実行してみる」ということがなくなる

「とりあえず波形を出してからデバッグしよう」ということができなくなり、冷静に自分の作ったデザインを見直して、どういう状況が発生しうるかを見なければならない。これはある意味時代錯誤だ。

ここから分かることは、便利な(波形という)ツールがあるのならばそれは使った方がよい。ただし、シミュレータのライセンス数や、RTLシミュレーションに時間がかかるのであれば、RTLを使わないChiselをうまく活用すべきだ。

Scalaを使った場合のシミュレーション速度はどうなのか?

Chiselを使った場合の利点として、Scalaのコードのままシミュレーションを実施できるということだ。 今回の目標は、RTLシミュレータを一切使わずにRISC-Vプロセッサを設計することだ。したがって、リグレッション環境もScalaを使って構築することになる。

流さなければならないのは、rv64ui系のテストパタン(60本くらい)だ。これを実行するためには、60種類のhexファイルをRTLにロードしてはシミュレーションし、結果を回収することになる。

1本のテストパタンを流すためには、以下のようなクラスを作成してsbtで実行する。

class Tester_rv64ui_p_add extends ChiselFlatSpec {
  "Basic test using Driver.execute" should "be used as an alternative way to run specification" in {
    implicit val conf = RV64IConf()
    iotesters.Driver.execute(Array(), () => new CpuTop()) {
      c => new CpuTopTests(c, "tests/riscv-tests/isa/rv64ui-p-add.hex", "pipetrace.rv64ui_p_add.log")
    } should be (true)
  }
}

いろいろ試行したのだが、コマンドラインから引数を渡して、hexファイルなどを指定できるように頑張ったのだが、どうもやり方が良く分からなかった。 そこで、スクリプトを使って60本分のテストパタン分のクラスを自動生成するようにした。

以下のようなファイルを60個、スクリプトで自動生成する。

  • Test_rv64ui-p-add.scala
class Tester_rv64ui_p_add extends ChiselFlatSpec {
  "Basic test using Driver.execute" should "be used as an alternative way to run specification" in {
    implicit val conf = RV64IConf()
    iotesters.Driver.execute(Array(), () => new CpuTop()) {
      c => new CpuTopTests(c, "tests/riscv-tests/isa/rv64ui-p-add.hex", "pipetrace.rv64ui_p_add.log")
    } should be (true)
  }
}
  • Test_rv64ui-p-sub.scala
class Tester_rv64ui_p_sub extends ChiselFlatSpec {
  "Basic test using Driver.execute" should "be used as an alternative way to run specification" in {
    implicit val conf = RV64IConf()
    iotesters.Driver.execute(Array(), () => new CpuTop()) {
      c => new CpuTopTests(c, "tests/riscv-tests/isa/rv64ui-p-sub.hex", "pipetrace.rv64ui_p_sub.log")
    } should be (true)
  }
}

そして、リグレッションテストを実行するためのクラスを作成し、これを実行した。

  • Test_AllPatterns.scala
class Tester_AllPattern extends ChiselFlatSpec {
  "rv64ui_p_add test using Driver.execute" should "be used as an alternative way to run specification" in {
    implicit val conf = RV64IConf()
    iotesters.Driver.execute(Array(), () => new CpuTop()) {
      c => new CpuTopTests(c, "tests/riscv-tests/isa/rv64ui-p-add.hex", "pipetrace.rv64ui_p_add.log")
    } should be (true)
  }
  "rv64ui_p_addi test using Driver.execute" should "be used as an alternative way to run specification" in {
    implicit val conf = RV64IConf()
    iotesters.Driver.execute(Array(), () => new CpuTop()) {
      c => new CpuTopTests(c, "tests/riscv-tests/isa/rv64ui-p-addi.hex", "pipetrace.rv64ui_p_addi.log")
    } should be (true)
  }
  "rv64ui_p_addiw test using Driver.execute" should "be used as an alternative way to run specification" in {
    implicit val conf = RV64IConf()
...

現時点ではすべてのテストはシーケンシャルに流れる。並列に実行したいが、これはまた後日。

同じような構成で、RTLを生成してVerilatorを実行し、VCDを取得することもできる。 こちらと処理速度を比較した。

  • Verilator実行 : 225秒
  • Chisel実行 : 257秒

あれ、Verilatorの方が早いじゃないか?まあ、確かにデザインが小さいし、リグレッションテスト自体は短いからかもしれない。小さなデザインで短いテストを流すならば、Verilatorで十分かもしれないということが分かった。

Chisel実行を並列化した

なんかおかしいので、Chiselのテストパタン実行を並列化してみた。以下のようにしてループを並列化して、並列に実行できるようにしてみた。

  (0 to 50).par foreach { idx =>
    iotesters.Driver.execute(Array(), () => new CpuTop(new RV64IConfig)) {
      c => new CpuTopTests(c, pattern_path(idx), log_path(idx))
    } should be (true)
  }

4コアCPU、8スレッドで実行して結果は以下となった。

  • Verilator実行 : 225秒
  • Chisel実行 : 257秒
  • 4コア Chisel実行 : 202秒

予想外に伸びない。Scalaの環境は、sbtの依存関係の関係上makeの並列実行をすることができない。 Chiselの実行環境については、もう少し改善の余地はありそうな気がする。 ちなみに、同じ方法でVerilatorでVCDを取ろうとしたが、途中でハングしてしまい実行できなかった。

まとめ

  • Chiselでも、パイプラインCPUを設計できる。
    • Verilogでの設計が簡単なのは確か。Chiselが普及するためには、デバッガビリティやシミュレーション速度の向上がカギとなる。
  • Chiselで設計し、シミュレーションまでScala上で実行することで、RTLシミュレーションで問題となるライセンスの問題を解決できる。
  • Scalaでのシミュレーション速度に改善の余地が残される。特にVerilatorと対等の処理速度というのは問題。
  • Scalaでの並列実行はもう少しスケーリングしてほしい。

"Creating an LLVM Backend for the Cpu0 Architecture"をやってみる(11. グローバル変数のサポート)

Cpu0のバックエンドをLLVMに追加するプロジェクト、6章では、グローバル変数のアクセスをサポートする。

今回も非常にファイル量が多くて、とりあえずLLVMをビルドするためだけにパッチを作って当てているが、LLVM 7.0は未サポートになっている部分が多く、ソースコードを書き直す必要があった。

  • Chapter6の実装を追加したもの

github.com

今回はグローバル変数のサポート。グローバル変数のサポートには4種類があって、それぞれどのような処理が必要なのか見ていく。

  • リロケーションモード : static, Smallsection : 未使用
  • リロケーションモード : static, Smallsection : 使用
  • リロケーションモード : PIC, Smallsection : 未使用
  • リロケーションモード : PIC, Smallsection : 使用

読んだのは以下の章。


グローバル変数

これまではローカル変数についてみてきたが、今回はグローバル変数のアクセス変換について学ぶ。

グローバル変数のDAG変換は、過去に学んだDAG変換とは異なる。バックエンドのC++コードが、llc -relocation-moelオプションに基づいてIR DAGノードを作成数。一方で、他のDAGはIR DAGを直接マシンコードのDAGに変換する。マシンコードのIR DAGはIR DAGの入力ファイルに基づく(例外的に、 Chapter3_4で使用するRetLR疑似命令は除く)。加えて、バックエンドを持っている場合は、アセンブリディレクティブ(マクロ)に関連するグローバル変数のための関数を出力するためのマシン命令について考えておく必要がある。

Chapter6_1グローバル変数をサポートしている。ch6_1.cppコンパイルしてみよう。

./bin/clang -c -target mips-unknown-linux-gnu ../lbdex/input/ch6_1.cpp -emit-llvm -o ch6_1.bc
./bin/llvm-dis ch6_1.bc -o -

...
@gStart = dso_local global i32 3, align 4
@gI = dso_local global i32 100, align 4

; Function Attrs: noinline nounwind optnone
define dso_local i32 @_Z11test_globalv() #0 {
entry:
  %c = alloca i32, align 4
  store i32 0, i32* %c, align 4
  %0 = load i32, i32* @gI, align 4
  store i32 %0, i32* %c, align 4
  %1 = load i32, i32* %c, align 4
  ret i32 %1
}

Cpu0グローバル変数のオプション

Cpu0のグローバル変数の扱いオプションには、以下の4つが存在する。

  • llc -relocation-model=static -cpu0-use-small-section=false
        lui     $2, %hi(gI)
        ori     $2, $2, %lo(gI)
        ld      $2, 0($2)
        st      $2, 4($sp)
        ld      $2, 4($sp)
  • llc -relocation-model=static -cpu0-use-small-section=true
        ori     $2, $gp, %gp_rel(gI)
        ld      $2, 0($2)
        st      $2, 4($sp)
        ld      $2, 4($sp)
  • llc -relocation-model=pic -cpu0-use-small-section=false

    assembly lui $2, %got_hi(gI) addu $2, $2, $gp ld $2, %got_lo(gI)($2) ld $2, 0($2) st $2, 4($sp) ld $2, 4($sp)

  • llc -relocation-model=pic -cpu0-use-small-section=true

        ld      $2, %got(gI)($gp)
        ld      $2, 0($2)
        st      $2, 4($sp)
        ld      $2, 4($sp)

以下にオプションをまとめる:

オプション名 デフォルト 選択可能なオプション値 説明
-relocation-model pic static pic: 位置独立アドレス, static: 絶対アドレス
-cpu0-use-small-section false true false: .data or .bss, 32‐bitアドレス, true: .sdata or .sbss, 16‐bitアドレス
  • -reloction-model=static時のCpu0 DAGと命令
オプション: cpu0-use-small-section false true
アドレッシングモード 絶対 $gp 相対
アドレッシング 絶対 $gp+オフセット
Legalized selection DAG (add Cpu0ISD::Hi<gI offset Hi16> Cpu0ISD::Lo<gI offset Lo16>) (add register %GP, Cpu0ISD::GPRel<gI offset>)
Cpu0 lui $2, %hi(gI); ori $2, $2, %lo(gI); ori $2, $gp, %gp_rel(gI);
リロケーションレコードの解決 リンク時 リンク時
  • -relocation-model=pic時のCpu0 DAGと命令
オプション: cpu0-use-small-section false true
アドレッシングモード $gp 相対 $gp 相対
アドレッシング $gp+オフセット $gp+オフセット
Legalized selection DAG (load (Cpu0ISD::Wrapper register %GP, <gI offset>)) (load EntryToken, (Cpu0ISD::Wrapper (add Cpu0ISD::Hi<gI offset Hi16>, Register %GP), Cpu0ISD::Lo<gI offset Lo16>))
Cpu0 ld $2, %got(gI)($gp); lui $2, %got_hi(gI); add $2, $2, $gp; ld $2, %got_lo(gI)($2);
リロケーションレコードの解決 リンク・ロード時 リンク・ロード時

-relocation-model=static-cpu0-use-small-section=falseの時には、レジスタ$g0はグローバル変数のスタート位置に設定されている。

To support global variable, first add UseSmallSectionOpt command variable to Cpu0Subtarget.cpp. After that, user can run llc with option llc -cpu0-use-small-section=false to specify UseSmallSectionOpt to false. The default of UseSmallSectionOpt is false if without specify it further. About the cl::opt command line variable, you can refer to here [1] further.

グローバル変数をサポートするためには、Cpu0Subtarget.cppにまずはUseSmallSectionOpt コマンド変数を追加する。そのあとに、llcを`llc -cpu0-use-small-section=false で実行することでUseSmallSectionOptをfalseに設定する。UseSmallSectionOptのデフォルト値はfalseである。cl::optコマンドライン変数についてはさらに1(https://jonathan2251.github.io/lbd/globalvar.html#id6) を参照のこと。

setOperationAction(ISD::GlobalAddress, MVT::i32, Custom)Cpu0TargetLowering::LowerOperation()で実装したグローバルアドレスの計算関数をllcに通知する。LLVMはこの関数を使ってIR DAGのグローバル変数のロード処理をマシンコードに変換するときにのみ使用する。

コンストラクト関数Cpu0TargetLowering()内のsetOperationAction(ISD::XXX, MVT::XXX, Custom)により設定されたすべてのIR操作のカスタムタイプが"Legalized selection DAG"ステージ中でargetLowering::LowerOperation()を呼び出したとしても、グローバルアドレスのアクセス操作は、DAG NodeがISD::GlobalAddressかどうかをチェックして識別することができる。

スタティックモード

Cpu0のグローバル変数のオプションでは、cpu0-use-small-section=falseを設定することにより、グローバル変数をdata/bssに配置する。一方で、cpu0-use-small-section=trueを設定すると、グローバル変数をsdata/sbssに配置される。sdataはsmall data areaの略である。セクションデータとsdataは初期値を持ったグローバル変数(int gl = 100のようなもの)が格納される。一方で、初期値を持たないグローバル変数(int gl;)はbssおよびsbssに格納される。

data or bss

スタティックモードかつ、cpu0-use-small-section=false、まずはglのPC相対アドレスの上位アドレスをレジスタ\$2に設定し、16ビットシフトする。レジスタ\$2にglのPC絶対アドレスの下位アドレスを設定する。

llc -relocation-model=static -cpu0-use-small-section=falseによりlowerGlobalAddress()はDAG(GlobalAddress<i32* @gI> 0)(add Cpu0ISD::Hi<gI offset Hi16> Cpu0ISD::Lo<gI offset Lo16>)に変換する。

Pat<(add CPURegs:\$hi, (Cpu0Lo tglobaladdr:\$lo)), (ORi CPURegs:\$hi, tglobaladdr:\$lo)>(add Cpu0ISD::Hi, Cpu0ISD::Lo)(ori Cpu0ISD::Hi, Cpu0ISD::Lo)に変換する。

sdata or sbss

sdata/sbssは16ビット長で表現できるアドレス領域であり、ELF内で高速アクセスできる領域に配置されている。cpu0-use-small-section=trueにて生成できる。

llc -relocation-model=static -cpu0-use-small-section=trueを実行することにより、DAG(GlobalAddress<i32* @gI> 0)(add register %GP Cpu0ISD::GPRel<gI offset>)に変換される。

PICモード

sdata or bss

llc -relocation-model=pic -cpu0-use-small-section=trueに設定することで、sdata,sbssを使用したPICのコードを出力できるようになる。

  ld  $2, %got(gI)($gp)

The .cpload is the assembly directive (macro) which will expand to several instructions. Issue .cpload before .set nomacro since the .set nomacro option causes the assembler to print a warning message whenever an assembler operation generates more than one machine language instruction, reference Mips ABI [2].

Following code will exspand .cpload into machine instructions as below. “0fa00000 09aa0000 13aa6000” is the .cpload machine instructions displayed in comments of Cpu0MCInstLower.cpp.

.cploadは複数の命令を展開するためのアセンブリディレクティブ(マクロ)である。.cploadは、.set nomacroの前に宣言する必要がある。.cploadはマシンコードに展開される。これは、gp_dispからの相対位置を計算し\$gpに格納される。

// Lower ".cpload $reg" to
//  "lui   $gp, %hi(_gp_disp)"
//  "addiu $gp, $gp, %lo(_gp_disp)"
//  "addu  $gp, $gp, $t9"

上記の_gp_dispはリロケーションレコードである。これは、アセンブリori $gp, $zero, %hi(_gp_disp)およびアセンブリori $gpと同等のマシン命令0da00000 (オフセット0) と0daa0000(オフセット8)に変換される。$gp, %lo(_gp_disp)は、_gp_dispに依存するリロケーションレコードである。ローダまたはOSは、動的関数をメモリxにロードするときに_gp_dispを(x - .dataの開始アドレス)で計算し、これら2つの命令を正しく調整する。この関数が呼び出されると共有関数がロードされるため、再配置レコードld $ 2, %got(gI)($gp)」はリンク時に解決できない。リロケーションレコードはロード時に解決されるが、リンカはメモリアドレスをローダに渡し、ローダはオフセットを直接計算するだけでこれを解決できるため、名前バインディングは静的である。メモリ参照は、リンク時に_gp_dispのオフセットでバインドされる。ELFの再配置記録は、ELFサポートの章で紹介する。

以下の対応するオプションllc -relocation-model=picとしてのlowerGlobalAddress()のコードフラグメントは、DAG(GlobalAddress <i32 * @gI> 0)(load EntryToken, (Cpu0ISD::Wrapper Register %GP, TargetGlobalAddress<i32* @gI> 0))に変換する。

最後に、Cpu0のld命令のパタンは、DAG(load EntryToken, (Cpu0ISD::Wrapper Register %GP, TargetGlobalAddress<i32* @gI> 0))ld $2, %got(gI)($gp)に変換する。

PICモードでは、Cpu0は".cpload"とld $2, %got(gl)($gp)を使用してグローバル変数にアクセスすることを思い出そう。コストは、レジスタ\$gpが常にアドレス.sdataに割り当てられ、そこで固定されるとは想定していないことから生じる。この関数に\$gpを予約していても、\$gpレジスタは他の関数で変更することができる。最後のサブセクションでは、\$gpはどの関数でも保存されると想定されいる。実行時に\$gpが固定されている場合は、ここで ".cpload"を削除し、グローバル変数アクセスで命令コストを1つだけにすることができる。" .cpload” を削除することの利点は、変数に割り当てることができる汎用レジスタ\$gpを使用しなくてもよいということである。最後のサブセクションである.sdataモードでは、静的リンクであるため".cpload"を削除する。PICモードでは、動的ロードは時間がかかり過ぎる。すべての関数で1つの汎用レジスターを失うことを犠牲にして".cpload"を削除することは、ここでは意味がない。この関数を静的リンクでリンクしたい場合はllc -relocation-model=picからの".cpload"の再配置レコードもリンク段階で解決できる。

data or bss

llc -relocation-model=picを指定した場合のlowerGlobalAddress()は、(GlobalAddress<i32* @gI> 0)DAGを(load EntryToken, (Cpu0ISD::Wrapper (add Cpu0ISD::Hi<gI offset Hi16>, Register %GP), TargetGlobalAddress<i32* @gI> 0))に変換する。

グローバル変数のプリントをサポートする

LowerSymbolOperand関数を追加して、グローバル変数オペランド出力をサポートする。

サマリ

"Creating an LLVM Backend for the Cpu0 Architecture"をやってみる(10. Objectファイルのサポート)

Cpu0のバックエンドをLLVMに追加するプロジェクト、第5章に入った。オブジェクトファイルのサポートを行う。

今回も非常にファイル量が多くて、とりあえずLLVMをビルドするためだけにパッチを作って当てているが、LLVM 7.0は未サポートになっている部分が多く、ソースコードを書き直す必要があった。

  • オリジナルのパッチ。これをあてただけでは動かない。

github.com

  • パッチをさらに修正したもの。関数を一部書き直している。

github.com

今回はオブジェクトファイルのサポート。これまでは基本的にバックエンドはアセンブリファイルを出力していたが、オブジェクトファイルを出力できるように変更する。

読んだのは以下の章。


オブジェクトファイルへの変換

これまでは、LLVM IRのコードをアセンブリファイルに変換していた。オブジェクトコードに変換しようとすると、現状ではエラーを出す。

$ ./bin/llc -march=cpu0 -relocation-model=pic -filetype=obj ch4_1_mult.bc -o ch4_1_mult.cpu0.o
./bin/llc: warning: target does not support generation of this file type!

そこでオブジェクトコードの生成をサポートする。Chapter5_1のファイルを実装した。

$ ./bin/llc -march=cpu0 -relocation-model=pic -filetype=obj ch4_1_mult.bc -o ch4_1_mult.cpu0.o
$ objdump -s ch4_1_mult.cpu0.o

ch4_1_mult.cpu0.o:     file format elf32-big

Contents of section .text:
 0000 09ddfff8 0920000b 022d0004 012d0004  ..... ...-...-..
 0010 09220001 0f302aaa 0d33aaab 41230000  ."...0*..3..A#..
 0020 46300000 1f43001f 1d330001 11334000  F0...C...3...3@.
 0030 0940000c 17334000 12223000 022d0004  .@...3@.."0..-..
 0040 012d0004 09dd0008 3ce00000           .-......<...
Contents of section .comment:
 0000 00636c61 6e672076 65727369 6f6e2036  .clang version 6
 0010 2e302e30 2d317562 756e7475 32202874  .0.0-1ubuntu2 (t
 0020 6167732f 52454c45 4153455f 3630302f  ags/RELEASE_600/
 0030 66696e61 6c2900                      final).
$ ./bin/llc -march=cpu0el -relocation-model=pic -filetype=obj ch4_1_mult.bc -o ch4_1_mult.cpu0el.o
$ objdump -s ch4_1_mult.cpu0el.o

ch4_1_mult.cpu0el.o:     file format elf32-big

Contents of section .text:
 0000 f8ffdd09 0b002009 04002d02 04002d01  ...... ...-...-.
 0010 01002209 aa2a300f abaa330d 00002341  .."..*0...3...#A
 0020 00003046 1f00431f 0100331d 00403311  ..0F..C...3..@3.
 0030 0c004009 00403317 00302212 04002d02  ..@..@3..0"...-.
 0040 04002d01 0800dd09 0000e03c           ..-........<
Contents of section .comment:
 0000 00636c61 6e672076 65727369 6f6e2036  .clang version 6
 0010 2e302e30 2d317562 756e7475 32202874  .0.0-1ubuntu2 (t
 0020 6167732f 52454c45 4153455f 3630302f  ags/RELEASE_600/
 0030 66696e61 6c2900                      final).

実装は、以下のファイルを変更・追加した。

  • lib/Target/Cpu0/Cpu0MCInstLower.h
  • lib/Target/Cpu0/Cpu0Subtarget.h
  • lib/Target/Cpu0/Cpu0TargetStreamer.h
  • lib/Target/Cpu0/InstPrinter/Cpu0InstPrinter.cpp
  • lib/Target/Cpu0/MCTargetDesc/CMakeLists.txt
  • lib/Target/Cpu0/MCTargetDesc/Cpu0AsmBackend.cpp
  • lib/Target/Cpu0/MCTargetDesc/Cpu0AsmBackend.h
  • lib/Target/Cpu0/MCTargetDesc/Cpu0BaseInfo.h
  • lib/Target/Cpu0/MCTargetDesc/Cpu0ELFObjectWriter.cpp
  • lib/Target/Cpu0/MCTargetDesc/Cpu0FixupKinds.h
  • lib/Target/Cpu0/MCTargetDesc/Cpu0MCCodeEmitter.cpp
  • lib/Target/Cpu0/MCTargetDesc/Cpu0MCCodeEmitter.h
  • lib/Target/Cpu0/MCTargetDesc/Cpu0MCExpr.cpp
  • lib/Target/Cpu0/MCTargetDesc/Cpu0MCExpr.h
  • lib/Target/Cpu0/MCTargetDesc/Cpu0MCTargetDesc.cpp
  • lib/Target/Cpu0/MCTargetDesc/Cpu0MCTargetDesc.h
  • lib/Target/Cpu0/MCTargetDesc/Cpu0TargetStreamer.cpp

ELFのエンコーダは上記の図に示す関数群を呼び出す。AsmPrinter::OutStreamerは、llc -filetype=objが呼び出されたときに、MCObjectStreamerを設定する。

命令オペランドの情報は、上記の図のようにして渡される。以下のステップを踏む。

  1. encodeInstruction()はMI.OpcodeをgetBinaryCodeForInstr()に渡す。
  2. getBinaryCodeForInstr()はMI.Operand[n]をgeMachineOpValue()に渡す。
  3. getMachineOpValue()を呼び出すことでレジスタ番号を取得する。
  4. getBinaryCodeForInstr()が、全てのレジスタ番号をencodeInstruction()に返す。

MI.Opcodeは命令センタ悪ステージで設定される。テーブル生成関数getBinaryCodeForInstr()は全てのオペランドの情報をtdファイルから取得する。

例えば、Cpu0のバックエンドが"%0 = add %1, %2" IRから "addu \$v0, \$at, \$v1"を生成したとすると、LLVMレジスタ%0, %1, %2のためにそれぞれ \$v0, \$at, $v1を割り当てている。MCOperand構造体は、MI.Operands[]にはレジスタ番号の集合が格納されており、LLVMgetMachineOpValue()から取得されるレジスタを割り当てる。

getMachineOpValue()内のgetEncodingValue(Reg)関数は、AT, V0, V1などのレジスタの名前から、レジスタ番号を取得する。これはCpu0RegisterInfo.tdから取得される。

Cpu0AsmBackend.cpp内のapplyFixup()は、jeq, jub命令などの、「アドレス制御フロー文」や「関数呼び出し文」の制御を行う。Cpu0ELFObjectWriter.cpp内のneedsRelocateWithSymbol()のリロケーションレコードをtrue/falseに設定することで、リンク時にアドレスの調整が必要かどうかが決まる。もしtrueならば隣家はアドレス値を正しい情報に修正できるチャンスがある。一方でfalseだった場合、隣家はリロケーション情報を調整するための正しい情報を持っていない。リロケーションレコードについては、ELFサポートの章で説明する。

ELFオブジェクト形式の命令を生成すると、Cpu0MCCodeEmitter.cpp内のEncodeInstruction()関数が呼び出される。

バックエンドのターゲット登録構造

Cpu0MCTargetDesc.cppは、前の章で説明したように、ターゲットの登録を行い、アセンブリ命令の出力を行う。同様にELFオブジェクトの登録も、以下のようにして実施する。

elf streamerによる関数の登録

// Register the elf streamer.
TargetRegistry::RegisterELFStreamer(*T, createMCStreamer);

  static MCStreamer *createMCStreamer(const Triple &TT, MCContext &Context,
                                      MCAsmBackend &MAB, raw_pwrite_stream &OS,
                                      MCCodeEmitter *Emitter, bool RelaxAll) {
    return createELFStreamer(Context, MAB, OS, Emitter, RelaxAll);
  }

  // MCELFStreamer.cpp
  MCStreamer *llvm::createELFStreamer(MCContext &Context, MCAsmBackend &MAB,
                                      raw_pwrite_stream &OS, MCCodeEmitter *CE,
                                      bool RelaxAll) {
    MCELFStreamer *S = new MCELFStreamer(Context, MAB, OS, CE);
    if (RelaxAll)
      S->getAssembler().setRelaxAll(true);
    return S;
  }

Cpu0MCCodeEmitterは、ビッグエンディアン用とリトルエンディアン用を登録する。RegisterELFStraemer()がelf streamerクラスを再利用する間に、オブジェクトフォーマットについてチェックをする。

読者は以下のような疑問を持つかもしれない: 「createCpu0MCCodeEmitterEB(const MCInstrInfo &MCII, const MCSubtargetInfo &STI, MCContext &Ctx) の実際の引数は何なのか?」「いつ引数が設定されるのか?」そう、この場所では引数は設定されず、createXXX()関数は関数ぽいなtのみが設定される(C言語では、TargetRegistry::RegisterXXX(TheCpu0Target, createXXX())で、createXXXは関数ポインタである)。LLVMは、ターゲット登録の際に関数ポインタをcreateXXX()に保持し続け、ターゲット登録処理中、つまりRegisterXXX()を呼び出し中の引数を割り当てる際に、createXXX()関数を呼び出すことになる。

asm backendの関数登録

Cpu0AsmBackendクラスはアセンブラからオブジェクトへの橋渡しを行う。ビッグエンディアンとリトルエンディアンについてそれぞれ考えておく必要がある。これらはMCAsmBcakendの派生クラスである。オブジェクトファイルを生成するコードのほとんどは、MCELFStreamerクラスとその親クラスであるMCAsmBackendクラスで実装されている。

"Creating an LLVM Backend for the Cpu0 Architecture"をやってみる(9. 算術演算命令の追加)

Cpu0のバックエンドをLLVMに追加するプロジェクト、今回からは4章に入って、算術論理演算命令を追加する。まずは算術演算から。

算術演算において、特殊な演算ケースの場合は演算を最適化できる場合がある。この方式についても見ていく。

実装は以下。cpu0_chapter4ブランチに実装した。

github.com

以下の章を参考にした。

Arithmetic and Logic Instructions

Cpu0の算術演算を追加する。本章ではIRをGraphvizで表示する。DAGの変換のいくつかはGraphvizを使って説明する。算術演算命令を追加した後に、論理演算命令を追加する。

C言語のプログラムをコンパイルする中で、読者はC言語のプログラムがどのように変換されるのかについてみる。バックエンドの構造に加えて、読者はC言語演算子LLVM noIRにどのようにマッピングするのかについて説明する。本章ではHILOとC0レジスタクラスを追加する。読者は、汎用レジスタクラスだけでなく、このような特殊なレジスタの使用方法について学ぶ。

算術演算

+, -, *, <<, >> 演算子

+, -, *, SHL, SHLV(<<), SRA, SRAV, SHR, SHRV(>>)のための演算子が定義されている。

Chapter4_1では、C言語で言う +, -, *, <<, >> を扱うことができる。LLVM IRでadd, sub, mul, shl, ashrが定義されている。ashr命令(arithmetic shift right)命令は第1オペランドを符号拡張した形で右シフトする。

@@ -304,11 +405,36 @@ let Predicates = [DisableOverflow] in {
 def ADDu    : ArithLogicR<0x11, "addu", add, IIAlu, CPURegs, 1>;
 }
 }
+let Predicates = [Ch4_1] in {
+let Predicates = [DisableOverflow] in {
+def SUBu    : ArithLogicR<0x12, "subu", sub, IIAlu, CPURegs>;
+}
+let Predicates = [EnableOverflow] in {
+def ADD     : ArithLogicR<0x13, "add", add, IIAlu, CPURegs, 1>;
+def SUB     : ArithLogicR<0x14, "sub", sub, IIAlu, CPURegs>;
+}
+def MUL     : ArithLogicR<0x17, "mul", mul, IIImul, CPURegs, 1>;
+}

 /// Shift Instructions
+let Predicates = [Ch4_1] in {
+// sra is IR node for ashr llvm IR instruction of .bc
+def ROL     : shift_rotate_imm32<0x1b, 0x01, "rol", rotl>;
+def ROR     : shift_rotate_imm32<0x1c, 0x01, "ror", rotr>;
+def SRA     : shift_rotate_imm32<0x1d, 0x00, "sra", sra>;
+}
 let Predicates = [Ch3_5] in {
 def SHL     : shift_rotate_imm32<0x1e, 0x00, "shl", shl>;
 }
+let Predicates = [Ch4_1] in {
+// srl is IR node for lshr llvm IR instruction of .bc
+def SHR     : shift_rotate_imm32<0x1f, 0x00, "shr", srl>;
+def SRAV    : shift_rotate_reg<0x20, 0x00, "srav", sra, CPURegs>;
+def SHLV    : shift_rotate_reg<0x21, 0x00, "shlv", shl, CPURegs>;
+def SHRV    : shift_rotate_reg<0x22, 0x00, "shrv", srl, CPURegs>;
+def ROLV    : shift_rotate_reg<0x23, 0x01, "rolv", rotl, CPURegs>;
+def RORV    : shift_rotate_reg<0x24, 0x01, "rorv", rotr, CPURegs>;
+}

 def JR      : JumpFR<0x3c, "jr", GPROut>;

例えば、shift_rotate_immは以下のように定義されている。srlなどの演算子OpNodeに格納されれう。"shr"string instr_asmに格納されている。

class shift_rotate_imm<bits<8> op, bits<4> isRotate, string instr_asm,
                       SDNode OpNode, PatFrag PF, Operand ImmOpnd,
                       RegisterClass RC>:
  FA<op, (outs GPROut:$ra), (ins RC:$rb, ImmOpnd:$shamt),
     !strconcat(instr_asm, "\t$ra, $rb, $shamt"),
     [(set GPROut:$ra, (OpNode RC:$rb, PF:$shamt))], IIAlu> {
  let rc = 0;
}
  • lshr: Logical SHift Right
  • ashr: Arithmetic SHift right
  • srl: Shift Right Logically
  • sra: Shift Right Arithmetically
  • shr: SHift Right

x >> 1は、x = x/2であると考えるとすると、演算子>>の実装ではlshrは符号付値では失敗する。同様にashrでは符号なし値で失敗する。したがって、符号付と符号なしの両方で演算を実行するためには、lshrashrの両方が必要になる。

<<実装はlshrは"符号なしx = 1G"を満たすが、"符号付きx = 1G"では失敗することがわかる。 2Gは32ビット符号付き整数範囲(-2G〜2G-1)の外にあるので問題ない。 オーバーフローの場合、正しい結果をレジスタに保持する方法はない。 したがって、レジスタ内の値はすべて問題ない。 すべてのx << 1に対してlshrがx = x * 2を満たし、xの結果が範囲外でないことを確認できる。オペランドxが符号付きまたは符号なし整数であっても問題ない。

  • lbdex/input/ch4_math.ll
  ; Function Attrs: nounwind
  define i32 @_Z9test_mathv() #0 {
    %a = alloca i32, align 4
    %b = alloca i32, align 4
    %1 = load i32, i32* %a, align 4
    %2 = load i32, i32* %b, align 4
  
    %3 = add nsw i32 %1, %2
    %4 = sub nsw i32 %1, %2
    %5 = mul nsw i32 %1, %2
    %6 = shl i32 %1, 2
    %7 = ashr i32 %1, 2
    %8 = lshr i32 %1, 30
    %9 = shl i32 1, %2
    %10 = ashr i32 128, %2
    %11 = ashr i32 %1, %2
  
    %12 = add nsw i32 %3, %4
    %13 = add nsw i32 %12, %5
    %14 = add nsw i32 %13, %6
    %15 = add nsw i32 %14, %7
    %16 = add nsw i32 %15, %8
    %17 = add nsw i32 %16, %9
    %18 = add nsw i32 %17, %10
    %19 = add nsw i32 %18, %11
  
    ret i32 %19
  }
  $ ./bin/llc -march=cpu0 -relocation-model=pic -filetype=asm ../lbdex/input/ch4_math.ll -o -
          .text
          .section .mdebug.abiO32
          .previous
          .file   "ch4_math.ll"
          .globl  _Z9test_mathv           # -- Begin function _Z9test_mathv
          .p2align        2
          .type   _Z9test_mathv,@function
          .ent    _Z9test_mathv           # @_Z9test_mathv
  _Z9test_mathv:
          .cfi_startproc
          .frame  $sp,8,$lr
          .mask   0x00000000,0
          .set    noreorder
          .set    nomacro
  # %bb.0:
          addiu   $sp, $sp, -8
          .cfi_def_cfa_offset 8
          ld      $2, 0($sp)
          ld      $3, 4($sp)
          subu    $4, $3, $2
          addu    $5, $3, $2
          addu    $4, $5, $4
          mul     $5, $3, $2
          addu    $4, $4, $5
          shl     $5, $3, 2
          addu    $4, $4, $5
          sra     $5, $3, 2
          addu    $4, $4, $5
          addiu   $5, $zero, 128
          shrv    $5, $5, $2
          addiu   $t9, $zero, 1
          shlv    $t9, $t9, $2
          srav    $2, $3, $2
          shr     $3, $3, 30
          addu    $3, $4, $3
          addu    $3, $3, $t9
          addu    $3, $3, $5
          addu    $2, $3, $2
          addiu   $sp, $sp, 8
          ret     $lr
          .set    macro
          .set    reorder
          .end    _Z9test_mathv
  $func_end0:
          .size   _Z9test_mathv, ($func_end0)-_Z9test_mathv
          .cfi_endproc
                                          # -- End function
  
          .section        ".note.GNU-stack","",@progbits

オーバフローの検出を行う。

$ ./bin/clang -target mips-unknown-linux-gnu -c ch4_1_addsuboverflow.cpp -emit-llvm -o ch4_1_addsuboverflow.bc
$ llvm-dis ch4_1_addsuboverflow.bc -o -
  %add = add nsw i32 %0, %1
  %sub = sub nsw i32 %0, %1
  • -cpu0-enable-overflow=true を追加した場合
$ ./bin/llc -march=cpu0 -relocation-model=pic -filetype=asm -cpu0-enable-overflow=true ch4_1_addsuboverflow.bc -o -
        add     $3, $3, $4
        ...
        sub     $3, $3, $4
  • -cpu0-enable-overflow=falseを追加した場合(デフォルト)
$ ./bin/llc -march=cpu0 -relocation-model=pic -filetype=asm -cpu0-enable-overflow=true ch4_1_addsuboverflow.bc -o -
        addu    $3, $3, $4
        ...
        subu    $3, $3, $4

現代のCPUでは、プログラマは加減算においてオーバフローを省略できる命令を利用することができる。しかし-cpu0-enable-overflow=trueを追加することにより、プログラマはオーバフロー例外を発生させることができる。通常、このオプションはデバッグ目的に利用する。

Graphvizでグラフを書き出す

$ bin/clang -target mips-unknown-linux-gnu -c ../lbdex/input/ch4_1_mult.cpp -emit-llvm -o ch4_1_mult.bc
$ bin/llc -view-dag-combine1-dags -march=cpu0 -relocation-model=pic -filetype=asm ./ch4_1_mult.bc -o ch4_1_mult.cpu0.s
f:id:msyksphinz:20181222010455p:plain

%演算子と/演算子

%演算子srem IRに相当する。

LLVMはsrem除算を乗算に変換し、DIVの演算コストを削減する。例えば、"int b = 11; b = (b+1)%12" はDAG上で図18のように最適化される。

この変換は少しややこしい。0xC x 0x2AAAAAAB = 0x2_00000004 となる(mulhs 0xC, 0x2AAAAAAAB) は、符号付乗算の上位32ビットを取得することを意味す。1ワードずつの数値の乗算結果は、通常、2ワード分の値を生成する(0x2, 0xAAAAAAAB)。この場合の上位の値、つまり0x2である。最終的な結果は(sub 12, 12)となり結果は0となる。これは(0x11+1)%12に相当する。

どうもこの0x2AAAAAABというのはマジック値であるらしい。%12で割り算をするためには、このような方法がとられるのか。

f:id:msyksphinz:20181222010510p:plain

Armの解法

/// Multiply and Divide Instructions.
def SMMUL   : ArithLogicR<0x41, "smmul", mulhs, IIImul, CPURegs, 1>;
def UMMUL   : ArithLogicR<0x42, "ummul", mulhu, IIImul, CPURegs, 1>;
//def MULT    : Mult32<0x41, "mult", IIImul>;
//def MULTu   : Mult32<0x42, "multu", IIImul>;

SMMUL命令とUMMUL命令を使用して、乗算の結果から上位の32bitを取得することができる。

  addiu $2, $zero, 12
  smmul $3, $2, $3
  shr $4, $3, 31

Mipsの解法

def mulhs    : SDNode<"ISD::MULHS"     , SDTIntBinOp, [SDNPCommutative]>;
def mulhu    : SDNode<"ISD::MULHU"     , SDTIntBinOp, [SDNPCommutative]>;

MIPSの場合は、HILOレジスタに乗算命令の結果を格納するので、HIレジスタから値を持ってくればよい。

  addiu $2, $zero, 12
  mult  $2, $3
  mfhi  $3
  shr $4, $3, 31

%演算と/演算の完全サポート

上記の演算をより一般化して、"(b+1)%a"を計算する場合、LLVMはエラーを出してしまう。上記の剰余を乗算に変換する計算は、一般化したコードとして出力でき兄からだ。したがって、一般的には、上除算命令を実行した後にHILOレジスタから値を取ってくる方法が取られる。"c = a / b"は、"div a, b"と、"mflo c"の2つに分割される。

完全な"%"と"/"をサポートするためには、Chapter4_1のように以下を追加する。

  1. SDIV, UDIVをCpu0InstrInfo.tdに追加する。
  2. copyPhysReg()を追加する。
  3. setOperationAction(ISD::SDIV, MVT::i32, Expand), ... setTargetDAGCombine(ISD::SDIVREM)と、PerformDivRemCombine()PerformDAGCombine()を追加する。
static SDValue performDivRemCombine(SDNode *N, SelectionDAG& DAG,
                                    TargetLowering::DAGCombinerInfo &DCI,
                                    const Cpu0Subtarget &Subtarget) {
...
  // insert MFLO
  if (N->hasAnyUseOfValue(0)) {
    SDValue CopyFromLo = DAG.getCopyFromReg(InChain, DL, LO, Ty,
                                            InGlue);
    DAG.ReplaceAllUsesOfValueWith(SDValue(N, 0), CopyFromLo);
    InChain = CopyFromLo.getValue(1);
    InGlue = CopyFromLo.getValue(2);
  }
...
  return SDValue();
}

以下のコードを実行すると、div命令とmfhiが出力された。

  • lbdex/input/ch4_1_mult2.cpp
int test_mult()
{
  int b = 11;
  int a = 12;

  b = (b+1)%a;

  return b;
}
        ld      $2, 4($sp)
        addiu   $2, $2, 1
        ld      $3, 0($sp)
        div     $2, $3
        mfhi    $2
        st      $2, 4($sp)

ローテート命令

ローテート命令も同様に追加した。演算子自体は、以下で追加しているものと思われる。これで、Cのコードからローテートを推論できる。

 /// Shift Instructions
let Predicates = [Ch4_1] in {
// sra is IR node for ashr llvm IR instruction of .bc
def ROL     : shift_rotate_imm32<0x1b, 0x01, "rol", rotl>;
def ROR     : shift_rotate_imm32<0x1c, 0x01, "ror", rotr>;
def SRA     : shift_rotate_imm32<0x1d, 0x00, "sra", sra>;
}

論理演算子

&, |, ^, ! ==, !=, <, <=, >, >= 演算子をサポートする。以下のファイルに実装の追加を行う。

  • lib/Target/Cpu0/Cpu0ISelLowering.cpp
  • lib/Target/Cpu0/Cpu0InstrInfo.td
  • lib/Target/Cpu0/Cpu0Subtarget.h

-mcpu=cpu032I-mcpu=cpu032IIで生成されるプログラムが異なる。cpu032Iはcmp命令で処理、cpu032IIは比較命令で処理。cpu032Iとcpu032IIの命令セットの違いは、以下を確認。

        ld      $2, 28($sp)
        ld      $3, 24($sp)
        cmp     $sw, $2, $3
  • cpu032II
        ld      $2, 28($sp)
        ld      $3, 24($sp)
        slt     $2, $2, $3

Chiselを使ってCPUを作ろう(17. 5ステージにCPUにフォワーディングパスを追加する)

f:id:msyksphinz:20181220010907p:plain

Chiselを使って、非常にシンプルなCPUを作ってみるプロジェクト。5ステージまで拡張したので、次はフォワーディングパスを追加して正常に演算を実行できるようにする。

github.com

ALUのフォワーディングパスは、

  1. WBステージ → EXステージ
  2. MEMステージ → EXステージ
  3. EXステージ → EXステージ

の3種類のフォワードパスを追加して、書き込み先アドレスと使用アドレスが同一ならば、フォワードするようにする。 Verilogと書き方は全く同じだ。

  • minicpu/src/main/scala/cpu/cpu.scala
  switch(ex_op0_sel) {
    is (OP0_RS1) {
      // Register Forwarding
      when (mem_inst_valid & mem_inst_wb_en & (mem_inst_rd === ex_inst_rs0)) {
        ex_alu_op0 := mem_alu_res
      } .elsewhen (wb_inst_valid & wb_inst_wb_en & (wb_inst_rd === ex_inst_rs0)) {
        ex_alu_op0 := Mux (wb_ctrl_mem_cmd === MCMD_RD, wb_mem_rdval, wb_alu_res)
      } .otherwise {
        ex_alu_op0 := ex_rdata_op0
      }
...
  switch(ex_op1_sel) {
    // Register Forwarding
    is (OP1_PC ) { ex_alu_op1 := ex_inst_addr.asSInt }
    is (OP1_RS2) {
      when (mem_inst_valid & mem_inst_wb_en & (mem_inst_rd === ex_inst_rs1)) {
        ex_alu_op1 := mem_alu_res
      } .elsewhen (wb_inst_valid & wb_inst_wb_en & (wb_inst_rd === ex_inst_rs1)) {
        ex_alu_op1 := Mux (wb_ctrl_mem_cmd === MCMD_RD, wb_mem_rdval, wb_alu_res)
      } .elsewhen (ex_csr_wbcsr =/= CSR.X) {
        ex_alu_op1 := u_csrfile.io.rw.rdata.asSInt
      } .otherwise {
        ex_alu_op1 := ex_rdata_op1
      }
...

次に、ロードユースのフォワーディングだ。残念ながら、ロードした命令を次サイクルで次の命令が利用することはできない。メモリにアクセスするので、このフォワードは無効なのだ。 そこで、メモリアクセスが入ると、自動的に1サイクルストールさせるようにする。

  • minicpu/src/main/scala/cpu/cpu.scala
  val dec_stall_en  = dec_inst_valid & (u_cpath.io.ctl.mem_cmd =/= MCMD_X)
        49 :   [00000108]           | 1,X[ 1]=>0000000000002000,X[00]=>0000000000000000|                          |                               |X01<=0x0000000000002000|x01<=0x00000000000020fc  : 0x000000fc : INST(0x00002097) : auipc   ra, 0x2
        50 :   [0000010c]  00ff0eb7 |                                                  |                          | [00002000]=>0x0000000000000000|X30<=0x0000000000002000|x01<=0x0000000000002000  : 0x00000100 : INST(0xf0408093) : addi    ra, ra, -252
        51 :   [00000110]  0ffe8e9b |15,X[ 0]=>0000000000ff0000,X[15]=>0000000000000000|                          |                               |                       |x30<=0x0000000000ff00ff  : 0x00000104 : INST(0x0000af03) : lw      t5, 0(ra)
        52 :   [00000114]  00200193 |16,X[29]=>0000000000ff0000,X[00]=>00000000000000ff|                          |                               |X29<=0x0000000000ff0000|
        53 :   [00000118]  27df1a63 | 1,X[ 0]=>0000000000000000,X[00]=>0000000000000002|                          |                               |X29<=0x0000000000ff00ff|x29<=0x0000000000ff0000  : 0x00000108 : INST(0x00ff0eb7) : lui     t4, 0xff0
        54 :   [0000011c]  00002097 |11,X[30]=>0000000000ff00ff,X[29]=>0000000000ff00ff|                          |                               |X03<=0x0000000000000002|x29<=0x0000000000ff00ff  : 0x0000010c : INST(0x0ffe8e9b) : addiw   t4, t4, 255

これらのフォワーディングパスを追加することにより、無事にrv64ui_p_***のパタンがすべてPassするようになった。

Chiselを使った場合、Verilogを使った場合でどちらとも無事にPassしたので、たぶん大丈夫。

毎日技術ブログを書いていたら、権威のある国際学会(の併設ワークショップ)で講演することになった話

年末なので技術以外のことも日記に書きたいと思います。1年に1回くらいは、エモい記事を書いてもいいでしょう。 まあ今年のまとめみたいな記事です。

今年も飽きもせずほぼ毎日技術ブログを書いていました。今見たら今年の投稿数は340件に近いので、まあほとんど毎日といってもいいと思います。

書いているテーマは多岐に渡っている、と自分では思っていますが、結局ほとんどの内容はコンピュータアーキテクチャの話だったり、RISC-Vの話だったり、あとは量子コンピュータも少しハマりました。 そんな感じでハードウェアばっかり触っています。 「FPGA開発日記」のはずなんですが、業務も含めプライベートでもFPGAはあまり触っておらず、もっぱらソフトウェア作って遊んでいるか、ハードウェア作ってもVivadoで論理合成して終わっているか、シミュレーションして終わりです。 全然FPGA関係ありませんね。

ブログを始めるきっかけと続けるモチベーション

さて、私はもともとコンピュータアーキテクチャについて勉強するのが好きで、学生時代から一貫してハードウェア設計を専門に勉強しています。

合計して3回くらい卒業論文を書いたことになるのですが、全てハードウェア設計がらみ、しかもプロセッサ開発なのでブレないですね。でもあまりその分ちゃんと知識がついたかというとそんなことはなくて、まだまだ勉強しなければならないことはたくさんある。

ブログも含め、常に重視していることは常に手を動かすこと。教科書を読めば何となくわかった気にはなるけども、それは見せかけで、実際に自分で手を動かしてみるとうまくいかないケースは良くある。 だから一般論を学ぶだけでなく、自分で手を動かした結果をしっかり残す。これが私のブログを書くモチベーションになったりしています。

さて、そんな訳で飽きもせずずっと技術的なことをブツブツとネット上に書き散らしていたおかげで、最近はいろんなところに呼んでもらってしゃべる機会が増えました。

2017年は、

  • Design Solution Forum 2017
  • RISC-V Day Tokyo

そして2018年は、

さらに今年は2本の連載(CQ出版インターフェース、APS)も書かせてもらい、いろんな場所で出しゃばりました。 ちなみに各所で、「このアカウント名は何と読むのか?」と聞かれましたが、本人も分かっていません。最初にアカウントを取ったときはこんなことになるとは思っていなかったので、特に考えていませんでした。

f:id:msyksphinz:20181115000946p:plain

気が付いたら権威のある国際学会(の併設ワークショップ)で話をすることになった

タイトルに書いた「権威のある国際学会」というのはMICRO51のことで、日本で今後私の生きているうちで開催されることは無いんじゃないか、というくらいの国際学会です。

MICRO51では併設してワークショップも開催しており、そのうちの1つでRISC-V Workshopを実施した、ということで、それに呼ばれました。 10/20日の土曜日、発表時間はたったの15分。 別に本発表でもないし、たぶんこのワークショップの発表審査なんてザルでしょうから、別に凄くもなんとも無いんですけど、その場に立って話ができたというのは、普段出不精の私にとっては非常に光栄なことでした。

まあ、でも個人参加だったので宿泊から移動まで全部自費だったんですけどね。たぶんもう行かない。 ついでに、発表資料も全部回収されたのですがいまだにウェブサイトにアップロードされていません。良く分からんな。まあ私のはアップロードしているので、自由に見てください。

ちなみに、RISC-V Workshopのウェブサイトで私のプロフィール紹介が載っているんですが、だれが書いてくださったのか分かりませんが非常に面白くて笑ってしまいました。

tmt.knect365.com

Mr. "@msyksphinz" is the author of FPGA Developer's Diary in Japan and known as the cutting-edge influencer in the society of computer architects. Despite of his major presence in the industry, he chooses to stay anonymous.

「@msykshinz氏は日本のFPGA開発日記の著者で、コンピュータアーキテクチャの先端技術のインフルエンサーとして知られています。産業界における彼の大きなプレゼンスにも関わらず、かれは匿名であり続けることを選択しています。」

私がこの技術ブログを書き続けてよかったなと思うことは、意外にも業界の著名な方が読んでくださっているということ(つまり適当なことは書けない!)、そしてRISC-Vをはじめとする業界の凄い人たちに関わることができたということ。 単なる趣味人間がここまでいろんな人に関わってこれたのは、頭が悪いなりに毎日アウトプットし続けたからかな、と思っています。

今後はどうするのか、という話ですが、これまで通り相変わらず自分のペースで自分の好きなことをブログに書いていきたいと思います。 いつも自分に心がけているのは、概念的な大枠だけを語って偉そうにしている「評論家」にはなりたくない、ということ。 評論家はインフルエンサーとして重要な役割があり、そんな人は一定数必要だとは思いますが、私はそういう立場は似合わない。

「評論家」にならないための心構え

皆さんの会社にも時々いますよね。通称評論家。

「手を全く動かさずに、口だけ動かす人」のことを評論家と呼んでいます。何となくそれっぽいことを言っているように思えるけど、ちゃんと聞いてみるとあんまり大したことは言っていない人。

持論ですが、そういう人は、昔はバリバリに働いていたんだと思う。が、歳を取るにつれて偉くなり、手を動かさなくなった。

偉くなった分いろんなところで話をしたり講演したりする機会は増えたけど、手を動かさなくなったので深入りする話ができなくなった、そんな感じだと思います。

自分はそうはなりたくない。なるべくなら一生手を動かして最前線でいたいと思っています。 だから、常にアウトプットを出し続けるという、これは自分にとってのノルマかな、と思っています。

目標を設定し、それに向かって活動するということ

Youtuberのヒカキン氏も、「毎日継続して動画を投稿する人の方が消えない」と言っていたように、内容がまとまっていなくても、毎日何か文章を書き起こすというのは、非常に重要なルーチンとして自分の中にあります。

ただし、これが万人に言えるかどうかは分からない。毎日変に続けるよりも、時間を空けつつきっちりまとめ上げる方が得意な人もいる。 なので「毎日続けること」が、万人にとっての解決策である、とは言うつもりはありません。

私が思う、第一線で活躍し続けるために重要なことは、「何かを成し遂げること」だと思う。

それが、「毎日ブログを書くこと」でもいいし、「OSSとして何かをリリースする」でも良いし、別にコンピュータ関係でない目標でもいいし、目標を設定して、それに対して自分が何をするのか、ブレークダウンするという能力が、必要なスキルではないかと思っています。

そういえば昔GAFAの本を読んでそんなことが書いてあった気がする。

msyksphinz.hatenablog.com

という訳で、エモい記事でございました。今年の目標振り返りは、年末にやりたいと思います。思い出を語る日記でした。