ピクシブ株式会社 Advent Calendar 2015の3日目の記事になります。
いま画像処理の仕事をしているエンジニアの saturday06 です。 最近仕事でアセンブラに触れたので、この記事ではそれに関連して趣味でやった開発について書きます。
未来のCPUの機能・AVX512命令
AVX512命令という、512ビットのデータをいっぺんに扱うことができるCPUの機能がIntelから発表されています。
AVX512命令はいわゆるSIMDと呼ばれる系列の命令です。最近画像処理系の仕事でSIMDが必要になったり、また7月に社内で行われた勉強会 でもSIMD押しだったため、最新な命令であるAVX512もちょっと見ていました。
ふつうのCPUでAVX512命令を呼び出してみる
例えば512ビットのデータを32ビットの整数に16分割したデータを2つ用意し、そいつらをAVX512を利用して各々のデータを足し算し、結果を表示する処理は次のようになります。
uint32_t left[16] __attribute__((aligned(64))) = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; uint32_t right[16] __attribute__((aligned(64))) = {0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500}; uint32_t result[16] __attribute__((aligned(64))) = {0}; int main(int argc, char** argv) { __asm__ ( "vmovdqa32 (%0), %%zmm0;" // leftの値を読み込み "vpaddd (%1), %%zmm0, %%zmm0;" // leftとrightのそれぞれの値を加算 "vmovdqa32 %%zmm0, (%2);" // 加算結果をresultへ書き出し ::"a"(left), "b"(right), "c"(result)); // 計算結果をコンソールに表示 for (int i = 0; i < 16; i++) { printf("result[%d] = %d\n", i, result[i]); } return 0; }
__asm__
文内に書いてある vmovdqa32
とか vpaddd
がAVX512命令です。こいつらがグローバルに定義されている
32ビット整数×16個の変数 left
と同 right
の各々の要素を足して、結果を result
変数に出力してくれます。
printfの出力結果は、うまくいけば下記のようになるはずです。
result[0] = 0 result[1] = 101 result[2] = 202 result[3] = 303 result[4] = 404 result[5] = 505 result[6] = 606 result[7] = 707 result[8] = 808 result[9] = 909 result[10] = 1010 result[11] = 1111 result[12] = 1212 result[13] = 1313 result[14] = 1414 result[15] = 1515
しかし、私の手元のPC (Ubuntu Linux 15.10/Intel Core i7) で実行しようとすると、次のようになり失敗します。
$ gcc app.c -o app && ./app [1] 5748 illegal hardware instruction (core dumped) ./app
GDBでエラーの詳細を追ってみます。
┌──app.c─────────────────────────────────────────────────────┐ │10 int main(int argc, char** argv) { │ >│11 __asm__ ( │ │12 "vmovdqa32 (%0), %%zmm0;" │ │13 "vpaddd (%1), %%zmm0, %%zmm0;" │ ┌────────────────────────────────────────────────────────────┐ │0x400580 <main+26> mov $0x601140,%ecx │ │0x400585 <main+31> mov %rdx,%rbx │ >│0x400588 <main+34> vmovdqa32 (%rax),%zmm0 │ │0x40058e <main+40> vpaddd (%rbx),%zmm0,%zmm0 │ │0x400594 <main+46> vmovdqa32 %zmm0,(%rcx) │ └────────────────────────────────────────────────────────────┘ native process 6208 In: main L12 PC: 0x400588 (gdb) r Starting program: /home/saturday06/Projects/avx512emulation/app Program received signal SIGILL, Illegal instruction. 0x0000000000400588 in main (argc=1, argv=0x7fffffffda98) at app.c:12
先ほど言及したAVX512命令である vmovdqa32
を実行しようとして失敗していることがわかります。
そう、実はAVX512命令は新しすぎてほとんどのCPUでは使えないのです。WindowsとかMac, Linuxなんかが乗るようなCPUでAVX512が動くものはまだ発売すらされていない*1・・・。
LinuxカーネルのちからでAVX512を実行できるようにする
そこで、Linuxカーネルのちからを借りて無理やり実行できるようにしてみます。手元のUbuntu Linux 15.10とopenSUSE Tumbleweedで動作確認しています。
実装方針
先ほどのようにアプリが存在しない命令を実行した際、IntelのCPUはアプリを一時中断しOSにその旨を通知します*2。
通知を受けたOSはLinuxの場合はアプリに対してSIGILLというシグナルを飛ばしますが、その直前に 存在しないAVX512命令が原因の場合は、
カーネル内でAVX512命令と同じ処理を代わりに実行してSIGILLを飛ばさず何事もなかったかのごとくアプリを続行させる という処理を埋め込みます。
x86_64版Linuxカーネルにおいて、当該処理は do_invalid_op
関数で行われるため、そこに対応コードを挿入します。
static void do_invalid_op(struct pt_regs *regs, long error_code) { + if (存在しないAVX512命令を実行しようとしたことが原因のエラー) { + 実行しようとしたAVX512命令と同じ処理(); + return; + } /* デフォルトの処理 */ ... }
エミュレーターを実装
方針に従い実装を行います。真面目に実装するととても大変なため、さしあたっては下記のとおり手抜きの限りを尽くしております。
- 対応する命令とオペランドは決め打ち
- zmm0レジスターはグローバル変数
- xmm0やymm0レジスターとの同期はとらない
- 16要素の同時足し算は、16回のループで実装
- エミュレーション中にエラーが発生したら、黙ってデフォルトの処理に移行
static u32 zmm0_register[16]; static void do_invalid_op(struct pt_regs *regs, long error_code) { u8 inst[6] = {0}; u8 vmovdqa32_rax_zmm0[] = {0x62, 0xf1, 0x7d, 0x48, 0x6f, 0x00}; u8 vpaddd_rbx_zmm0_zmm0[] = {0x62, 0xf1, 0x7d, 0x48, 0xfe, 0x03}; u8 vmovdqa32_zmm0_rcx[] = {0x62, 0xf1, 0x7d, 0x48, 0x7f, 0x01}; int error_bytes = copy_from_user(inst, (void*)regs->ip, sizeof(inst)); if (error_bytes != 0) { goto no_emulation; } else if (memcmp(inst, vmovdqa32_rax_zmm0, sizeof(inst)) == 0) { if (copy_from_user(zmm0_register, (void*)regs->ax, sizeof(zmm0_register)) != 0) { goto no_emulation; } } else if (memcmp(inst, vpaddd_rbx_zmm0_zmm0, sizeof(inst)) == 0) { u32 addition[16]; int i; if (copy_from_user(addition, (void*)regs->bx, sizeof(addition)) != 0) { goto no_emulation; } for (i = 0; i < 16; i++) { zmm0_register[i] += addition[i]; } } else if (memcmp(inst, vmovdqa32_zmm0_rcx, sizeof(inst)) == 0) { if (copy_to_user((void*)regs->cx, zmm0_register, sizeof(zmm0_register)) != 0) { goto no_emulation; } } else { goto no_emulation; } regs->ip += sizeof(inst); return; no_emulation: do_error_trap(regs, error_code, "invalid opcode", X86_TRAP_UD, SIGILL); }
この状態でカーネルをコンパイル・再起動し先ほどのアプリを実行すると、正しい出力が得られます。
カーネルモジュールとして分割
カーネルの再コンパイルは時間がかかるしヘタしたらOSが起動しなくなったりするので、カーネルモジュールの機構を使って
外からエミュレーション機能を既存のカーネルへ追加できるようにします。また、先ほどのように do_invalid_op
関数を書き換えるには、今年リリースのLinux4.0で追加された
Kernel Live Patchingという機能を使います。これは実行中のカーネルのプログラムを書き換えてしまう非常に強力な機能です。怖いですね。
今回は do_invalid_op
関数を自前で定義した avx512emulation_do_invalid_op
関数で書き換えます。
static void avx512emulation_do_invalid_op(struct pt_regs *regs, long error_code) { // 前項で作ったエミュレーションコード } static struct klp_func funcs[] = { { .old_name = "do_invalid_op", .new_func = avx512emulation_do_invalid_op, }, { } }; static struct klp_object objs[] = { { /* name being NULL means vmlinux */ .funcs = funcs, }, { } }; static struct klp_patch patch = { .mod = THIS_MODULE, .objs = objs, }; static int avx512emulation_init(void) { int ret; ret = klp_register_patch(&patch); if (ret) return ret; ret = klp_enable_patch(&patch); if (ret) { WARN_ON(klp_unregister_patch(&patch)); return ret; } return 0; } module_init(avx512emulation_init);
その後所定の手順でカーネルモジュールを作成します。
$ git clone https://github.com/saturday06/avx512emulation.git $ cd avx512emulation $ sudo -s make LDFINAL [M] /home/hogeyamahogetaro/avx512emulation/avx512emulation.ko
アプリを実行してみる
できたモジュールをinsmodコマンドでカーネルに埋め込むと、めでたく未来の命令が現代のCPUで動き出します。
$ gcc app.c -o app && ./app # まずは素で実行してみる [1] 5748 illegal hardware instruction (core dumped) ./app $ sudo insmod avx512emulation.ko # さっき作ったカーネルモジュールを適用 $ ./app result[0] = 0 result[1] = 101 result[2] = 202 result[3] = 303 result[4] = 404 result[5] = 505 result[6] = 606 result[7] = 707 result[8] = 808 result[9] = 909 result[10] = 1010 result[11] = 1111 result[12] = 1212 result[13] = 1313 result[14] = 1414 result[15] = 1515
やったね!
ソースコードはこちら → saturday06/avx512emulation · GitHub
Intel® Software Development Emulator
ところで、AVX512でぐぐるとだいたいIntelが出している公式のエミュレーターを使おうぜって記事がヒットします。
Intel® Software Development Emulator | Intel® Developer Zone
さっそく使ってみます。
$ ./app # まずは素で実行してみる [1] 21662 illegal hardware instruction (core dumped) ./app $ sde64 -- ./app # Intel純正エミュレーター経由で実行 result[0] = 0 result[1] = 101 result[2] = 202 result[3] = 303 result[4] = 404 result[5] = 505 result[6] = 606 result[7] = 707 result[8] = 808 result[9] = 909 result[10] = 1010 result[11] = 1111 result[12] = 1212 result[13] = 1313 result[14] = 1414 result[15] = 1515
完璧ですね。普段の開発ではこちらを使うことにします。
オチがついたところで、本記事は以上になります。
次は @lainbsd です。BSDという単語が入っていて期待大ですね!
*1:Xeon Phiでは一応動くが・・・