30
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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では、アセンブリをほぼそのまま関数として使えます。

test1.cpp
#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が返ってきました。

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

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

test3.cpp
#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

gdb1.png

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

gdb2.png

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

gdb3.png

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

mov $0x1, %eax
retq

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

gdb4.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モード好きなんですが、あんまり周りに使ってる人がいない印象ですね・・・ 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
30
Help us understand the problem. What are the problem?