JITアセンブラXbyakを使ってみる(その1)

はじめに

Xbyak(カイビャック)は、光成滋生さんによるJITアセンブラです。Intelによる深層学習ライブラリoneDNNのエンジン部分の実装に使われたり、AArch64版のXbyakであるXbyak_aarch64が富士通のリポジトリとして公開されたりと、ベンダーによる公式採用が増えています。なんかすごそうなので使ってみましょう、という記事です。

Xbyakの準備

Xbyakは、JITアセンブラです。C++ヘッダオンリーなので、インクルードするだけで使えます。git submoduleとして使うのが良いと思います。

まずは適当なリポジトリxbyak_testを作りましょう。

mkdir xbyak_test
cd xbyak_test
git init

次に、xbyakをsubmodule addしましょう。

git submodule add https://github.com/herumi/xbyak.git

ついでに、インクルードパスにXbyakを追加しておきましょう。

export CPLUS_INCLUDE_PATH=xbyak

これでXbyakが使えるようになります。

Xbyakを使ってみる

Xbyakを使うには、Xbyakをインクルードした上で、Xbyak::CodeGeneratorを継承したクラスを作ります。そのコンストラクタで「自分が作りたい関数」を作ります。とりあえずeaxレジスタに1を入れてretするだけの関数を作りましょう。Xbyakでは、アセンブリをほぼそのまま関数として使えます。

#include <cstdio>
#include <xbyak/xbyak.h>

struct Code : Xbyak::CodeGenerator {
  Code() {
    mov(eax, 1);
    ret();
  }
};

int main() {
  Code c;
  int (*f)() = c.getCode<int (*)()>();
  printf("%d\n", f());
}

ここで、XbyakがIntel記法を採用していることには注意が必要です。Linuxでアセンブリを見る人はgasが採用しているAT&T記法に慣れていることが多いと思いますが、それとはmovの代入が逆になります。

Xbyakが作る関数にアクセスするには、CodeGenerator::getCode()を適切な関数ポインタの型を持つテンプレートとして呼び出し、その返り値を関数ポインタとして受け取ります。

intを返す関数は、返り値をeaxに入れますので、これは1を返す関数になります。コンパイル、実行してみましょう。

$ g++ test1.cpp
$ ./a.out
1

1が返ってきました。

コンストラクタに引数を与え、その引数を使ってコードを作ることもできます。

#include <cstdio>
#include <xbyak/xbyak.h>

struct Code : Xbyak::CodeGenerator {
  Code(int i) {
    mov(eax, i);
    ret();
  }
};

int main() {
  Code c(12345);
  int (*f)() = (int (*)())c.getCode();
  printf("%d\n", f());
}

コンストラクタでint iを受け取り、それをeaxに与えるだけの関数を作りました。main関数内で

Code c(12345);

として関数の実体を作っている(実際に作られるのはgetCodeが呼ばれた時ですが)ので、これで12345を返す関数になります。

$ g++ test2.cpp
$ ./a.out
12345

JITアセンブラであることを確認する

さて、XbyakはJITアセンブラであり、コードを動的に生成します。したがって、コンパイル時には確定していない値でも、実行時には定数になっている値を即値にすることができます。その様子を見てみましょう。先ほど作った関数の返り値を、実行時引数として与えるコードを書いてみます。

#include <cstdio>
#include <xbyak/xbyak.h>

struct Code : Xbyak::CodeGenerator {
  Code(int i) {
    mov(eax, i);
    ret();
  }
};

int main(int argc, char **argv) {
  int i = atoi(argv[1]);
  Code c(i);
  int (*f)() = (int (*)())c.getCode();
  printf("%d\n", f());
}

gdbで見るために、-gオプションをつけてコンパイルしましょう.

g++ -g test3.cpp

まずは動作を確認します。1を食わすと1を、2を食わすと2を表示します。

$ ./a.out 1
1
$ ./a.out 2
2

さて、これがアセンブリでは即値になっていることをgdbを使って確認しましょう。

$ gdb ./a.out

Code::getCodeにブレークポイントを置きましょう。

(gdb) b Code::getCode
Breakpoint 1 at 0x4670: file xbyak/xbyak/xbyak.h, line 1053....

その状態で実行時引数「1」を与えて実行してみます。

(gdb) r 1
Starting program: /home/watanabe/temp/xbyak_test/a.out 1

Breakpoint 1, Xbyak::CodeArray::getCode (this=0x7ffffffed950) at xbyak/xbyak/xbyak.h:1053
1053            const uint8 *getCode() const { return top_; }

止まりました。ここでアセンブリを表示してみましょう1

(gdb) layout asm

image0.png

getCodeのアセンブリが表示されました。ここでnで次に進みます。

image1.png

現在の行が>で表示されていますが、次のcall q *%raxがXbyakが作った関数呼び出しです。si二回で進んでみましょう。

image2.png

これを見てわかるように、

mov $0x1, %eax
retq

と、eaxに即値が入っていることがわかります。r 2として再実行し、同様にXbyakの作った関数を見てみましょう。

image3.png

mov $0x2, %eax
retq

と、即値が入っています。「コンパイル時には決まらないが、実行時には定数であることがわかっている数を、あたかもコンパイル時定数として扱うことができる」のがJITアセンブラの強みの一つです。

作成されたコードの確認

Xbyakがどんなコードを吐いたかは、Xbyak::CodeGenerator::dumpで確認が可能です。先ほどのコードにc.dump()を追加してみましょう。

#include <cstdio>
#include <xbyak/xbyak.h>

struct Code : Xbyak::CodeGenerator {
  Code(int i) {
    mov(eax, i);
    ret();
  }
};

int main(int argc, char **argv) {
  int i = atoi(argv[1]);
  Code c(i);
  int (*f)() = (int (*)())c.getCode();
  c.dump(); // この行を追加
  printf("%d\n", f());
}

実行してみましょう。

$ g++ -g test3.cpp
$ ./a.out 1
B801000000C3
1
$ ./a.out 2
B802000000C3
2

関数fが、B801000000C3B802000000C3といった6バイトのマシン語になったことがわかります。gdbで確認してみましょう。まずはc.dump();の直後にブレークポイントを入れます。

$ gdb ./a.out
(gdb) b 16
Breakpoint 1 at 0x1b4a: file test3.cpp, line 16.

実行時引数1を与えて実行しましょう。

(gdb) r 1
Starting program: /home/watanabe/temp/xbyak_test/a.out 1
B801000000C3

Breakpoint 1, main (argc=2, argv=0x7ffffffee5a8) at test3.cpp:16
16        printf("%d\n", f());

c.dump()が呼ばれた直後の段階で止まりました。ここで、関数fのアドレスを確認しましょう。

(gdb) p f
$1 = (int (*)(void)) 0x7fffff7e0000

fは関数のポインタであり、0x7fffff7e0000を指していることがわかります。このアドレスから6バイトを表示してみましょう。

(gdb) x/6bx f
0x7fffff7e0000: 0xb8    0x01    0x00    0x00    0x00    0xc3

確かにバイト列がb8,01,00,00,00,c3になっていますね。

まとめ

簡単にXbyakの使い方を説明してみました。Xbyakではアセンブリと一対一対応した関数を呼び出すことで関数を「作る」ことができます。なので、インラインアセンブリや組み込み関数でコードを書いたことがある人はすぐに使えるようになりますが、XbyakはJITアセンブラなので、インラインアセンブリや組み込み関数とはかなり異なるコーディング感覚になります。

続く

  1. 僕はgdbのtuiモード好きなんですが、あんまり周りに使ってる人がいない印象ですね・・・