なぜラムダは単純な関数よりもコンパイラによって最適化できるのですか?


171

彼の本の中でThe C++ Standard Library (Second Edition)Nicolai Josuttisは、ラムダは単純な関数よりもコンパイラによってより最適化できると述べています。

さらに、C ++コンパイラは、通常の関数よりもラムダを最適化します。(213ページ)

何故ですか?

インライン化に関しては、これ以上何の違いもないはずだと思いました。私が考えることができる唯一の理由は、コンパイラーがラムダを使用したより良いローカルコンテキストを持っている可能性があり、そのようなことがより多くの仮定を行い、より多くの最適化を実行できるためです。



基本的に、ステートメントはラムダだけでなく、すべての関数オブジェクトに適用されます。
newacct

4
関数ポインタも関数オブジェクトであるため、これは正しくありません。
Johannes Schaub-litb

2
@litb:私はそれに同意しないと思います。^ W ^ W ^ W ^ W ^ W ^ W(標準を調べた後)私はそのC ++主義に気づいていませんでしたが、一般的な言葉で(そしてウィキペディア)、関数オブジェクトと言うとき、人々は呼び出し可能なクラスのインスタンスを意味します。
セバスチャンマッハ

1
いくつかのコンパイラは、プレーンの機能よりも優れた最適化ラムダをすることができ、すべてではない :-(
コーディグレー

回答:


175

その理由は、ラムダは関数オブジェクトであるため、それらを関数テンプレートに渡すと、そのオブジェクト専用の新しい関数がインスタンス化されるためです。したがって、コンパイラーはラムダ呼び出しを簡単にインライン化できます。

一方、関数の場合、古い警告が適用されます。関数ポインターが関数テンプレートに渡され、コンパイラーは従来、関数ポインターを介した呼び出しのインライン化に多くの問題を抱えています。彼らはすることができ、理論的にインライン化されるが、周辺機能も同様にインライン展開されている場合のみ。

例として、次の関数テンプレートを考えます。

template <typename Iter, typename F>
void map(Iter begin, Iter end, F f) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

次のようなラムダで呼び出す:

int a[] = { 1, 2, 3, 4 };
map(begin(a), end(a), [](int n) { return n * 2; });

このインスタンス化の結果(コンパイラーによって作成されます):

template <>
void map<int*, _some_lambda_type>(int* begin, int* end, _some_lambda_type f) {
    for (; begin != end; ++begin)
        *begin = f.operator()(*begin);
}

…コンパイラはそれを知って_some_lambda_type::operator ()おり、それへの呼び出しを簡単にインライン化できます。(そして他のラムダで関数mapを呼び出すと、各ラムダが異なる型を持っているので、の新しいインスタンス化が作成されます。)map

ただし、関数ポインターを指定して呼び出すと、インスタンス化は次のようになります。

template <>
void map<int*, int (*)(int)>(int* begin, int* end, int (*f)(int)) {
    for (; begin != end; ++begin)
        *begin = f(*begin);
}

…そしてここでfは、呼び出しごとに異なるアドレスをポイントしているため、コンパイラが特定の関数に解決できるように周囲の呼び出しもインライン化されていない限り、mapコンパイラは呼び出しをインライン化できません。fmapf


4
おそらく、異なるラムダ式で同じ関数テンプレートをインスタンス化すると、一意の型を持つまったく新しい関数が作成され、欠点となる可能性があることに言及する価値があります。

2
@greggo絶対に。問題は、インライン化できない関数(サイズが大きすぎるため)を処理する場合です。ここでは、コールバックへの呼び出しができ、まだではなく、関数ポインタの場合は、ラムダの場合にインライン化されます。std::sortこれは、関数ポインターの代わりにラムダを使用するこの古典的な例です。ここでは、最大7倍(おそらくそれ以上ですが、そのデータはありません!)パフォーマンスが向上します。
Konrad Rudolph

1
@greggoここで2つの関数を混乱させています。ラムダ渡す関数(たとえばstd::sort、またはmap私の例では)とラムダ自体です。ラムダは通常小さいです。他の機能–必ずしもそうではありません。他の関数内のラムダへの呼び出しインライン化することに関心があります。
Konrad Rudolph

2
@greggo知っています。これは文字通り私の答えの最後の文が言っていることです。
Konrad Rudolph

1
私は(それにつまずいた)好奇心を見つけることは簡単なブール関数で与えられたということであるpredその定義見えるが、とgccのV5.3を使用して、std::find_if(b, e, pred)インラインませんpredが、std::find_if(b, e, [](int x){return pred(x);})ありません。Clangは両方をインライン化できますが、ラムダを使用してg ++ほど高速なコードを生成しません。
rici 2016

26

「関数」をアルゴリズムに渡すと、実際には関数へのポインターが渡されるため、関数へのポインターを介して間接呼び出しを行う必要があります。ラムダを使用する場合、その型用に特別にインスタンス化されたテンプレートインスタンスにオブジェクトを渡し、ラムダ関数の呼び出しは直接呼び出しであり、関数ポインターを介した呼び出しではないため、インライン化される可能性が高くなります。


5
「ラムダ関数の呼び出しは直接呼び出しです」-確かに。ラムダだけでなく、すべての関数オブジェクトにも同じことが当てはまります。それは、たとえそうであっても、それほど簡単にインライン化できない単なる関数ポインタです。
ピートベッカー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.