いさぽん部屋(isapon.com)

ゲーム系プログラマによる特に方針のないブログ。技術系とカレー、ラーメンネタ多めだったはずが、最近はダイエットネタ多め。

C++11のラムダ式の最適化について調べる

C++11 のラムダ式は便利だ

最近ではJavaScriptなどC++11のラムダ式と近い書式をを見かけることも多くなったので、あまり書式の特異性を感じなくなりました。

とはいえ、キャプチャー部分など、ラムダ式はC++の構文の中では特異な方です。

あまり多用するとそれはそれで見難くなってしまうのですが、やっぱり便利です。

でもパフォーマンスってどうなんよ?

ちゃんと調べてなかった実行時のパフォーマンス。

中身を知らずに作ることは結構怖いのだ!

と、いうわけで次のようなプログラムを作ってコンパイル&アセンブリコードを出力させてみます。

プログラムの内容自体は割とどうでもよく、ラムダ式と、単なるインライン関数をコールバックとして呼び出したらどうなるか?

というのが評価目標です。

評価用コード

中身はテケトーですが……eachというテンプレート関数に、インライン関数、ラムダ式を渡してコールバック関数のように扱います。

※すべて静的な値で書くと最適化の段階でコードが削除されてしまったので、動的な値としてコマンドラインからの引数を渡しています。(うれしいけど、実験の時は悲しい)

#include <stdio.h>
#include <functional>


// 指定された文字列から1文字ずつ読み取ってコールバックを読み出す
template<typename _ArgTypes>
void each(
        int (*_callback)(char, _ArgTypes),
        const char* _string,
        _ArgTypes _argv)
{
    while (*_string)
    {
        char   c = *_string++;
        _callback(c, _argv);
    }
}


// インライン関数版
static inline int char_count(char c, int* counter_ref)
{
    (*counter_ref) += c + 0x11223344;  // 即値はアセンブラ化時の目印
    return    0;
}


int main(int argc, char** argv)
{
    const char*   target              = argv[1];
    int            optimize_blocker    = 0;

    // 特に意味はない try/catch.
    try
    {
        // インライン関数版
        each<int*>(&char_count, target, &optimize_blocker);

        // ラムダ式版
        each<int*>([](char c, int* counter_ref) -> int
        {
            (*counter_ref) += c + 0x55667788;  // 即値はアセンブラ化時の目印
            return    0;
        }, target, &optimize_blocker);
    }
    catch (...)
    {
    }


    // これがないと、最適化の段階で上記のコードが削除される。
    ::printf("%d\n", optimize_blocker);

    // アセンブラ化したときにわかりやすい目印
    return    0x99aabbcc;
}

評価方法

VC++(2015 Community+Windows), clang++(3.7.0+FreeBSD11.0), gnu c++(ver.5+FreeBSD11.0) で最大限の最適化を行ってアセンブリコードを出力します。コンパイラオプションは以下の通り。

cl.exe /Ox /FAs /D"CONSOLE" test.cpp
clang++ -std=c++11 -O3 -S test.cpp

また、例外を有効にした場合はどうなるかも見てみます。

cl.exe /EHsc /Ox /FAs /D"CONSOLE" test.cpp
clang++ -std=c++11 -fexceptions -O3 -S test.cpp

結果

基本的にはインライン展開されて最適化される。

ただし、VC++の場合のみ、例外を有効にした場合はラムダ式版だけ関数呼び出しになりました。

一応出力されるコードで細かい違いを言えば、

(*counter_ref) += c + 即値;

の部分が、VC++では add命令2回になっていたのが、clang/gccだと lea 命令に置き換わっていたことくらいですかね?

lea命令の方がうれしいわけですが。この辺りは Intel コンパイラを持っていれば試したいところ。

まとめ

C++11になると例外を多用する面もあるので、例外オプションなしは実質使えない気もします。

そう考えると、現状ではインライン関数で済むならインライン関数を使うのが良い選択な気がしますが……

今後のVCのアップデートでラムダ式の最適化具合も良くなると思うので、そこまで神経質にならなくてもいいかな?

VisualC++の最適化に関する感想

VC++の場合は、オプションで色々と出力するコードを変えられる故の弱点ですかね?

ぶっちゃけコードサイズ優先にするより、/Oxで出力したほうがコードサイズが小さくなったりすることもあるなど、正直他の最適化オプションは使わないような気もします。

あれこれオプションを付けずに全体的な最適化一本に絞ってもいいんじゃないかな?と思いました。

clangとgccに関する感想

アセンブリコードの書式の違いはあれど、出力されるアセンブリコードの内容はほぼ同じ。

と、いうか全く同じでした。

これくらいのコードじゃ差は出ないのでしょうが、gccも頑張ってる感じがありますね。

とはいえ、Win/FreeBSDな人なので使うのは結局 VC, clang なんですけど。

おまけ

今回の評価コードの each ですが、最初は template<typename... _ArgTypes> のように可変引数テンプレートにしていました。

しかし、可変にするとラムダ式を素直に渡せず(型推論ができなくなるため)、わざわざコールバックの型を指定しないとならないなど逆に扱いにくくなる面もありました。

総合的に考えると、ローカル関数のテクニックなどを用い、ラムダ式を使わないなら使わないで済ませるほうがもしかしたら(しばらくは)幸せなのかもしれません。

でも、ラムダ式のほうが見やすい面も多いので、適材適所で使いたいところです。

余談 (std::function)

サンプルコードの中に があるのですが、最初は std::function<> も評価してみようと思っていました。

が、あたりまえですが、std::function は引数の情報などをメモリに確保するなどインライン化は不可能な実装になっているので、今回は評価から外しました。

std::function は使わないで済むなら使わないに越したことはない。

と、いうことです(使ったほうがいい場合もたくさんあるので、そういうときは素直に使う)。

PS. 先日、あれ?ブログにアクセスできない!!

と思ったら、ドメインが切れていました。10年分まとめて契約していたので、すっかり期間を忘れてました。