pixiv insideは移転しました! ≫ https://inside.pixiv.blog/

未来のCPUの機能をカーネルのちからで現代のCPU上でも実行可能にする

ピクシブ株式会社 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では一応動くが・・・

*2:http://wiki.osdev.org/Exceptions#Invalid_Opcode