Linuxカーネルで見る最適化の技

この記事はLinux Advent Calendar 2014の13日目です。

LWNACCESS_ONCE() and compiler bugsを読んでなるほどねーと思ったのでその辺について書いてみたいと思います。 最適化の技と言いつつ実際は最適化させすぎない技なんですが\(^o^)/

まずACCESS_ONCEマクロの役割ですが、これは必ずデータを読みたいという場合に使ってます。 例えばこんなコードがあったときに(かなり適当ですが雰囲気は掴めるはず)

while (1) {
    struct foobar *p =  foo;
    if (p->hoge) {
    ....
    }
}

コンパイラはこのように最適化をかけてくる可能性があるんだけど、fooは別のスレッドによって変更されるのでループ毎にデータをfetchして欲してというケースで使うのがACCESS_ONCEマクロみたいです。

    struct foobar *p =  foo;
    while(1) {
        if (p->hoge) {
        ....
        }
     }
}

カーネル3.18ではinclude/linux/compiler.hにて以下のように定義されています。

381 #define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))

基本的にはvolatileを付けることでコンパイラの最適化を抑制していると。

で、今回読んだACCESS_ONCE() and compiler bugsではGCC 4.6,、4.7ではこの辺の挙動が変わり(バグ?)、volatileを付けた変数がスカラー型じゃない場合にvolatileを消してしまうようになったようで、これにより最適化して欲しくないところが最適化されるようになってしまったというのが記事の導入部分です。
これをどのように対処するかということでREAD_ONCE、ASSIGN_ONCEというマクロが提案されて近い将来このマクロが導入されるだろういう展望で記事が終わります。 記事のメインはこれらのマクロの解説ですね。

ということで、これらを見て行きたいのですがREAD_ONCEとASSIGN_ONCEは変数を読むのか、書くのかの違い程度なのでREAD_ONCEを見て行きたいと思います。

実装はこのようになっています。ACCESS_ONCEよりも複雑になっていますがやっていることは渡された変数を無理やりスカラー型として扱うことでvolatileが削除されるというのを防いでますね。

    static __always_inline void __read_once_size(volatile void *p, void *res, int size)
    {
        switch (size) {
        case 1: *(u8 *)res = *(volatile u8 *)p; break;
        case 2: *(u16 *)res = *(volatile u16 *)p; break;
        case 4: *(u32 *)res = *(volatile u32 *)p; break;
    #ifdef CONFIG_64BIT
        case 8: *(u64 *)res = *(volatile u64 *)p; break;
    #endif
        }
    }
    
    #define READ_ONCE(p) \
          ({ typeof(p) __val; __read_once_size(&p, &__val, sizeof(__val)); __val; })

read_once_size()という関数が使われていますがこれはalways_inlineが付いているので常にinline展開されるはずです。また、通常はACCESS_ONCEに渡す変数のサイズはコンパイル時に確定できると思うので最適化オプション-O1でswitch文も無くなるので__read_once_size()の実行コストは0になると思います。なので、余分な実行コスト無しにvolatileを必ずつけた形で変数にアクセスできるようになりますね。

やっていることを分かってしまえばなるほどな〜ってなるんですが、こういったテクニックを思いつくことができるのが流石というところですね。