web-dev-qa-db-ja.com

C ++ 11のラムダ関数のオーバーヘッドを理解する

これはすでに触れられています C++ラムダが複数回呼び出されたときに通常の関数より遅いのはなぜですか? および C++ 0xラムダオーバーヘッド しかし、私の例は前者の議論は後者の結果と矛盾します。

私のコードでボトルネックを検索したところ、値をバッファーにコピーするなど、特定のプロセッサー関数で可変引数リストを処理する、再帰的なテンプレート関数が見つかりました。

template <typename T>
void ProcessArguments(std::function<void(const T &)> process)
{}

template <typename T, typename HEAD, typename ... TAIL>
void ProcessArguments(std::function<void(const T &)> process, const HEAD &head, const TAIL &... tail)
{
  process(head);
  ProcessArguments(process, tail...);
}

このコードをラムダ関数とともに使用するプログラムのランタイムと、移動ポインタを使用して引数をグローバルバッファーにコピーするグローバル関数を比較しました。

int buffer[10];
int main(int argc, char **argv)
{
  int *p = buffer;

  for (unsigned long int i = 0; i < 10E6; ++i)
  {
    p = buffer;
    ProcessArguments<int>([&p](const int &v) { *p++ = v; }, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
  }
}

g ++ 4.6でコンパイルし、-O3でツール時間を測定すると、私のマシンで6秒以上かかります

int buffer[10];
int *p = buffer;
void CopyIntoBuffer(const int &value)
{
  *p++ = value;
}

int main(int argc, char **argv)
{
  int *p = buffer;

  for (unsigned long int i = 0; i < 10E6; ++i)
  {
    p = buffer;
    ProcessArguments<int>(CopyIntoBuffer, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
  }

  return 0;
}

約1.4秒かかります。

時間のオーバーヘッドを説明する舞台裏で何が起こっているのか理解できず、ランタイムを使わずにラムダ関数を利用するために何かを変更できるかどうか疑問に思っています。

28
mcbulba

ここでの問題は、std :: functionの使用法です。コピーで送信し、その内容をコピーします(パラメーターを巻き戻すときに再帰的に行います)。

さて、関数へのポインタの場合、コンテンツはまあ、単なる関数へのポインタです。ラムダの場合、コンテンツは少なくとも、キャプチャした関数+参照へのポインタです。これはコピーする量の2倍です。さらに、std :: functionの型消去のため、データをコピーすると遅くなる可能性があります(インライン化されません)。

ここにはいくつかのオプションがあり、おそらくstd :: functionではなくテンプレートを渡すのが最善でしょう。利点は、メソッド呼び出しがインライン化される可能性が高く、型の消去がstd :: functionによって発生しないこと、コピーが発生しないこと、すべてが非常に優れていることです。そのように:

template <typename TFunc>
void ProcessArguments(const TFunc& process)
{}

template <typename TFunc, typename HEAD, typename ... TAIL>
void ProcessArguments(const TFunc& process, const HEAD &head, const TAIL &... tail)
{
  process(head);
  ProcessArguments(process, tail...);
}

2番目のオプションは同じことですが、processをコピーで送信します。現在、コピーは行われていますが、適切にインライン化されています。

同様に重要なのは、process 'ボディもインライン化できることです(特にラムダの場合)。ラムダオブジェクトのコピーの複雑さとそのサイズに応じて、コピーによる受け渡しは、参照による受け渡しよりも速い場合とそうでない場合があります。コンパイラは、ローカルコピーよりも参照について推論するのに時間がかかる場合があるため、より高速になる可能性があります。

template <typename TFunc>
void ProcessArguments(TFunc process)
{}

template <typename TFunc, typename HEAD, typename ... TAIL>
void ProcessArguments(TFunc process, const HEAD &head, const TAIL &... tail)
{
  process(head);
  ProcessArguments(process, tail...);
}

3番目のオプションは、参照によってstd :: function <>を渡してみることです。これにより、少なくともコピーを回避できますが、呼び出しはインライン化されません。

次に、いくつかのパフォーマンス結果を示します(ideonesのC++ 11コンパイラを使用)。予想どおり、インラインのラムダボディが最高のパフォーマンスを提供していることに注意してください。

Original function:
0.483035s

Original lambda:
1.94531s


Function via template copy:
0.094748

### Lambda via template copy:
0.0264867s


Function via template reference:
0.0892594s

### Lambda via template reference:
0.0264201s


Function via std::function reference:
0.0891776s

Lambda via std::function reference:
0.09s
41
Artem Tokmakov