std :: function vsテンプレート


161

C ++ 11のおかげでstd::function、ファンクターラッパーのファミリーを受け取りました。残念ながら、私はこれらの新しい追加について悪いことだけを聞き続けます。最も人気があるのは、ひどく遅いということです。私はそれをテストしました、そして彼らはテンプレートと比較して本当に吸います。

#include <iostream>
#include <functional>
#include <string>
#include <chrono>

template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }

float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }

int main() {
    using namespace std::chrono;

    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        calc1([](float arg){ return arg * 0.5f; });
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    return 0;
}

111 ms vs 1241 ms。これは、テンプレートを適切にインライン化できる一方で、function仮想呼び出しを介して内部をカバーできるためだと思います。

私が目にするように、明らかにテンプレートには問題があります:

  • それらは、ライブラリをクローズドコードとしてリリースするときに、望まないことではないヘッダーとして提供する必要があります。
  • のようextern templateなポリシーを導入しない限り、コンパイル時間が大幅に長くなる可能性があります。
  • テンプレートの要件(概念、誰か?)を表現する(少なくとも私には既知の)明確な方法はなく、期待されるファンクタの種類を説明するコメントを禁止します。

したがって、functions は通過ファンクタの事実上の標準として使用でき、高いパフォーマンスが期待される場所ではテンプレートを使用する必要があると想定できますか?


編集:

私のコンパイラは、CTPのない Visual Studio 2012 です。


16
呼び出し可能なオブジェクトの異種混合コレクションがstd::function実際に必要な場合にのみ使用してください(つまり、実行時に識別情報がそれ以上利用できません)。
Kerrek SB、2013

30
あなたは間違ったものを比較しています。テンプレートはどちらの場合でも使用されます-「std::functionまたはテンプレート」ではありません。ここでの問題は、ラムダをラップするのではなく、ラムダをstd::functionラップすることstd::functionです。現時点での質問は、「リンゴとボウルのどちらがいいですか?」
オービットの

7
1nsでも10nsでも、どちらも意味がありません。
ipc 2013

23
@ipc:1000%は何もではありません。OPが特定するように、実際的な目的のためにスケーラビリティーがOPに入ってくると、気を使い始めます。
オービットの

18
@ipc 10倍遅く、これは巨大です。速度はベースラインと比較する必要があります。ナノ秒だからといって、それが問題ではないと考えるのはだまされています。
ポールマンタ

回答:


170

一般に、選択できる設計状況に直面している場合は、テンプレートを使用します。焦点を当てる必要があるのはとテンプレートのユースケースの違いであり、かなり異なるので、デザインという言葉を強調しましたstd::function

一般に、テンプレートの選択は、より広い原則の単なる例です。コンパイル時にできるだけ多くの制約を指定するようにしてください。理論的根拠は単純です。プログラムが生成される前であっても、エラーや型の不一致を検出できる場合は、バグのあるプログラムを顧客に出荷することはありません。

さらに、正しく指摘したように、テンプレート関数の呼び出しは静的に(つまり、コンパイル時に)解決されるため、コンパイラーはコードを最適化し、場合によってはコードをインライン化するために必要なすべての情報を持っています(呼び出しがvtable)。

はい、テンプレートのサポートは完全ではなく、C ++ 11にはまだ概念のサポートが欠けています。しかし、私はstd::functionその点であなたをどのように救うのかわかりません。std::functionはテンプレートの代わりではなく、テンプレートを使用できない設計状況のためのツールです。

このようなユースケースの1つは、特定のシグネチャに準拠しているが、具体的な型がコンパイル時に不明である呼び出し可能なオブジェクトを呼び出すことにより、実行時に呼び出しを解決する必要がある場合です。これは通常、潜在的に異なるタイプのコールバックのコレクションがある場合に当てはまりますが、均一呼び出す必要があります。登録されたコールバックのタイプと数は、プログラムの状態とアプリケーションロジックに基づいて、実行時に決定されます。これらのコールバックの一部はファンクタであり、一部はプレーン関数であり、一部は他の関数を特定の引数にバインドした結果である可能性があります。

std::functionそして、std::bindも可能にするための天然のイディオム提供関数型プログラミングを関数はオブジェクトとして扱われ、自然にカレーや他の機能を生成するために組み合わされ得るC ++、です。この種類の組み合わせはテンプレートでも実現できますが、通常、実行時に結合された呼び出し可能オブジェクトのタイプを判別する必要があるユースケースと同様の設計状況が一緒に発生します。

最後に、他にstd::functionも避けられない状況があります。たとえば、再帰的なラムダを記述したい場合などです。ただし、これらの制限は、私が信じている概念上の違いよりも、技術的な制限によってより多く規定されています。

要約すると、設計焦点を当て、これら2つの構成の概念的な使用例を理解するようにしてください。あなたがそうした方法でそれらを比較に入れると、それらがおそらく属していないアリーナにそれらを強制しています。


23
「これは通常、潜在的に異なるタイプのコールバックのコレクションがあるが、均一に呼び出す必要がある場合に当てはまります。」重要なビットです。私の経験則は、「std::functionストレージ側とFunインターフェース上のテンプレートを優先する」です。
R.マルティーニョフェルナンデス

2
注:具象型を非表示にする手法は、型消去と呼ばれます(マネージ言語の型消去と混同しないでください)。多くの場合、動的なポリモーフィズムの観点から実装されますが、より強力です(たとえば、unique_ptr<void>仮想デストラクタのない型でも適切なデストラクタを呼び出す)。
ecatmur 2013

2
@ecatmur:私はこの用語に同意しますが、用語については少しずれています。動的ポリモーフィズムとは、私が「コンパイル時に異なるフォームを想定する」と解釈する静的ポリモーフィズムとは対照的に、「実行時に異なるフォームを想定する」ことを意味します。後者はテンプレートでは実現できません。私にとって、型消去は、設計的には、動的ポリモーフィズムを実現するための一種の前提条件です。さまざまな型のオブジェクトとやり取りするための一定のインターフェースが必要であり、型消去は、型を抽象化する方法です。特定の情報。
Andy Prowl 2013

2
@ecatmur:つまり、動的ポリモーフィズムは概念的なパターンであり、型消去はそれを実現するための手法です。
Andy Prowl 2013

2
@Downvoter:私はあなたがこの答えで間違っていたものを聞きたいと思います。
Andy Prowl 2013年

89

Andy Prowlはデザインの問題をうまくカバーしています。これはもちろん非常に重要ですが、元の質問はに関連するより多くのパフォーマンスの問題に関係していると思いますstd::function

まず、測定手法について簡単に説明calc1します。得られた11ms はまったく意味がありません。実際、生成されたアセンブリを見る(またはアセンブリコードをデバッグする)と、VS2012のオプティマイザは、呼び出しの結果がcalc1反復から独立していて、呼び出しをループの外に移動することを理解するのに十分賢いことがわかります。

for (int i = 0; i < 1e8; ++i) {
}
calc1([](float arg){ return arg * 0.5f; });

さらに、呼び出しcalc1は目に見える効果がないことを認識し、呼び出しを完全にドロップします。したがって、111msは、空のループの実行にかかる時間です。(オプティマイザーがループを保持していることに驚いています。)したがって、ループ内の時間測定に注意してください。これは見かけほど簡単ではありません。

指摘されているように、オプティマイザは理解すべき問題が多くstd::function、呼び出しをループの外に移動しません。したがって、1241msはの公正な測定値ですcalc2

は、std::functionさまざまなタイプの呼び出し可能オブジェクトを格納できることに注意してください。したがって、ストレージに対してタイプ消去マジックを実行する必要があります。通常、これは動的なメモリ割り当てを意味します(デフォルトではへの呼び出しによるnew)。これは非常にコストのかかる操作であることはよく知られています。

標準(20.8.11.2.1 / 5)は、VS2012が(特に、元のコードに対して)ありがたいことに、小さなオブジェクトに対する動的メモリ割り当てを回避するための実装を規定しています。

メモリの割り当てが関係しているときにどれだけ遅くなるかを理解するために、3つfloatのs をキャプチャするようにラムダ式を変更しました。これにより、呼び出し可能オブジェクトが大きすぎて、小さなオブジェクトの最適化を適用できなくなります。

float a, b, c; // never mind the values
// ...
calc2([a,b,c](float arg){ return arg * 0.5f; });

このバージョンの場合、時間は約16000msです(元のコードの1241msと比較)。

最後に、ラムダの寿命がの寿命を囲んでいることに注意してくださいstd::function。この場合、ラムダのコピーを保存するのではなく、ラムダstd::functionへの「参照」を保存できます。「参照」とは、std::reference_wrapper関数std::refとによって簡単に作成できるを意味しますstd::cref。より正確には、以下を使用します。

auto func = [a,b,c](float arg){ return arg * 0.5f; };
calc2(std::cref(func));

時間は約1860msに減少します。

私はしばらく前にそれについて書いた:

http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

記事で述べたように、C ++ 11へのサポートが不十分なため、VS2010には引数がまったく適用されません。執筆の時点では、VS2012のベータ版しか利用できませんでしたが、C ++ 11のサポートは、この問題に十分対応できています。


これは確かに面白いと思います。副作用がないためコンパイラーによって最適化されるおもちゃの例を使用して、コード速度の証明を作成したいと思っています。実際のコードやプロダクションコードがなければ、これらの種類の測定に賭けることはめったにありません。
ジータ2013

@ Ghita:この例では、コードが最適化されないようにするために、前の反復の結果でcalc1あるfloat引数を取ることができます。のようなものx = calc1(x, [](float arg){ return arg * 0.5f; });。さらに、がをcalc1使用してxいることを確認する必要があります。しかし、これではまだ十分ではありません。副作用を作成する必要があります。たとえば、測定後x、画面に印刷します。とはいえ、timimgの測定におもちゃのコードを使用しても、実際のコードや製品コードで何が起こるかを常に完全に示すことはできないことに同意します。
Cassio Neri 2013

ベンチマークがループ内でstd :: functionオブジェクトを構築し、ループ内でcalc2を呼び出すようにも思えます。コンパイラーがこれを最適化する場合としない場合(およびコンストラクターがvptrを格納するのと同じくらい簡単な場合もあります)に関係なく、関数が一度構築され、別の関数に渡されて、ループで。つまり、構成時間(およびcalc2ではなく 'f'の呼び出し)ではなく、呼び出しのオーバーヘッドです。また、fを(calc2で)ループ内で呼び出すのではなく、ループ内で呼び出すと、どのような巻き上げからも利益が得られるかどうかに関心があります。
greggo 2014

すばらしい答えです。2つのこと:のための有効な利用の良い例std::reference_wrapper(強制テンプレートに、それは一般的なストレージのためだけではありません)、そして私は気づいたとして、それは... VSのオプティマイザは、空のループを破棄に失敗見て面白いです、このGCCのバグの再volatile
underscore_d

37

Clangでは、2つの間にパフォーマンスの違いはありません

clang(3.2、trunk 166872)(Linuxでは-O2)を使用すると、2つのケースのバイナリは実際には同じです

-ポストの最後でclangに戻ります。しかし、最初に、gcc 4.7.2:

すでに多くの洞察が進んでいますが、インライン化などにより、calc1とcalc2の計算結果は同じではないことを指摘しておきます。たとえば、すべての結果の合計を比較します。

float result=0;
for (int i = 0; i < 1e8; ++i) {
  result+=calc2([](float arg){ return arg * 0.5f; });
}

となるcalc2で

1.71799e+10, time spent 0.14 sec

calc1ではそれは

6.6435e+10, time spent 5.772 sec

これは、速度差で約40倍、値で約4倍です。1つ目は、OPが投稿したもの(ビジュアルスタジオを使用)よりもはるかに大きな違いです。実際に最後の値を出力することも、コンパイラーが目に見える結果(as-ifルール)のないコードを削除しないようにするための良いアイデアです。カシオネリはすでに彼の答えでこれを述べました。結果がどのように異なるかに注意してください-異なる計算を実行するコードの速度係数を比較するときは注意が必要です。

また、公平を期すために、f(3.3)を繰り返し計算するさまざまな方法を比較することは、おそらくそれほど興味深いことではありません。入力が定数である場合、ループ内であってはなりません。(オプティマイザが気付くのは簡単です)

ユーザー指定の値引数をcalc1と2に追加すると、calc1とcalc2の間の速度係数は40から5に低下します。ビジュアルスタジオでは、違いは2倍に近く、clangでは違いはありません(以下を参照)。

また、乗算は高速なので、スローダウンの要因について話すことは、それほど興味深いことではありません。より興味深い質問は、関数がどれほど小さいか、そしてこれらは実際のプログラムのボトルネックを呼び出しているかどうかです。

クラン:

Clang(3.2を使用)は、サンプルコード(下記に掲載)のcalc1とcalc2を切り替えたときに、実際には同一のバイナリを生成しました。質問に投稿された元の例では、どちらも同じですがまったく時間がかかりません(ループは上記のように完全に削除されます)。私の変更した例では、-O2を使用します。

実行する秒数(最高3):

clang:        calc1:           1.4 seconds
clang:        calc2:           1.4 seconds (identical binary)

gcc 4.7.2:    calc1:           1.1 seconds
gcc 4.7.2:    calc2:           6.0 seconds

VS2012 CTPNov calc1:           0.8 seconds 
VS2012 CTPNov calc2:           2.0 seconds 

VS2015 (14.0.23.107) calc1:    1.1 seconds 
VS2015 (14.0.23.107) calc2:    1.5 seconds 

MinGW (4.7.2) calc1:           0.9 seconds
MinGW (4.7.2) calc2:          20.5 seconds 

すべてのバイナリの計算結果は同じであり、すべてのテストは同じマシンで実行されました。clangやVSに関する深い知識を持つ誰かが、どのような最適化が行われたかについてコメントしていただければ興味深いでしょう。

私の変更したテストコード:

#include <functional>
#include <chrono>
#include <iostream>

template <typename F>
float calc1(F f, float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

float calc2(std::function<float(float)> f,float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

int main() {
    using namespace std::chrono;

    const auto tp1 = high_resolution_clock::now();

    float result=0;
    for (int i = 0; i < 1e8; ++i) {
      result=calc1([](float arg){ 
          return arg * 0.5f; 
        },result);
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    std::cout << result<< std::endl;
    return 0;
}

更新:

vs2015を追加。また、calc1、calc2にはdouble-> float変換があることに気づきました。それらを削除しても、Visual Studioの結論は変わりません(どちらもはるかに高速ですが、比率はほぼ同じです)。


8
間違いなく、ベンチマークを示しているだけは間違いです。興味深い使用例として、呼び出し側のコードがどこかから関数オブジェクトを受け取っているため、コンパイラーは呼び出しのコンパイル時にstd :: functionの発生元を知りません。ここで、コンパイラーは、calc2をインラインでmainに展開することにより、呼び出し時にstd :: functionの構成を正確に認識します。9月にcalc2を「extern」にすることで簡単に修正できます。ソースファイル。次に、オレンジとリンゴを比較します。calc2はcalc1ができないことをしています。そして、ループはcalc内にある可能性があります(fへの多くの呼び出し)。関数オブジェクトのctorの周りではありません。
greggo 2014年

1
適切なコンパイラに到達できるとき。今のところ、(a)実際のstd :: functionのctorが 'new'を呼び出すと言えます。(b)ターゲットが一致する実際の関数である場合、呼び出し自体はかなり無駄がありません。(c)バインディングの場合、関数objのコードptrによって選択され、関数objからデータ(バインドされたパラメーター)を取得する、適応を行うコードのチャンクがあります(d)「バインドされた」関数はコンパイラーが認識できる場合は、そのアダプターにインライン化します。
greggo 2014年

説明されたセットアップで新しい回答が追加されました。
greggo 2014年

3
ところで、ベンチマークは間違っていません。質問( "std :: function vs template")は、同じコンパイル単位のスコープでのみ有効です。関数を別のユニットに移動すると、テンプレートは不可能になるため、比較するものは何もありません。
rustyx

13

違いは同じではありません。

テンプレートでは実行できない処理を行うため、処理速度は遅くなります。特に、指定された引数の型で呼び出すことができ、戻り値の型が同じコードからの指定された戻り値の型に変換可能関数を呼び出すことができます。

void eval(const std::function<int(int)>& f) {
    std::cout << f(3);
}

int f1(int i) {
    return i;
}

float f2(double d) {
    return d;
}

int main() {
    std::function<int(int)> fun(f1);
    eval(fun);
    fun = f2;
    eval(fun);
    return 0;
}

同じ関数オブジェクトfunがの両方の呼び出しに渡されていることに注意してくださいeval。これには2つの異なる機能があります。

それを行う必要がない場合は、を使用しないでくださいstd::function


2
'fun = f2'が実行されると、 'fun'オブジェクトは、intをdoubleに変換し、f2を呼び出し、doubleの結果をintに変換する隠し関数を指すことに注意してください(実際の例では、 'f2'はその関数にインライン化できます)。std :: bindをfunに割り当てた場合、 'fun'オブジェクトには、バインドされたパラメーターに使用される値が含まれる可能性があります。この柔軟性をサポートするために、 'fun'(またはinit of)への割り当ては、メモリの割り当て/割り当て解除を伴う場合があり、実際の呼び出しオーバーヘッドよりもかなり長くかかる場合があります。
greggo 2014年

8

ここにはすでにいくつかの良い答えがあります。つまり、std :: functionとテンプレートの比較は、仮想関数と関数の比較に似ています。関数よりも仮想関数を「優先する」べきではありませんが、問題が当てはまる場合は仮想関数を使用して、コンパイル時から実行時に決定を移します。アイデアは、カスタムテーブルソリューション(ジャンプテーブルなど)を使用して問題を解決するのではなく、コンパイラーに最適化の機会を与えるものを使用するというものです。また、標準的なソリューションを使用している場合は、他のプログラマーにとっても役立ちます。


6

この回答は、一連の既存の回答に、std :: function呼び出しの実行時コストのより意味のあるベンチマークであると私が考えるものを提供することを目的としています。

std :: functionメカニズムは、それが提供するものについて認識されるべきです:呼び出し可能なエンティティは、適切な署名のstd :: functionに変換できます。z = f(x、y)で定義された関数に面を適合させるライブラリーがあり、それを受け入れてを受け入れるstd::function<double(double,double)>ことができ、ライブラリーのユーザーが呼び出し可能なエンティティーをそれに簡単に変換できると想定します。通常の関数でも、クラスインスタンスのメソッドでも、ラムダでも、std :: bindでサポートされているものでもかまいません。

テンプレートアプローチとは異なり、これはさまざまなケースでライブラリ関数を再コンパイルする必要なく機能します。したがって、追加のケースごとに追加のコンパイル済みコードはほとんど必要ありません。これを実現することは常に可能でしたが、以前は厄介なメカニズムが必要でした。また、ライブラリのユーザーは、機能するように関数の周りにアダプターを構築する必要がある可能性があります。std :: functionは、すべての場合に共通のランタイム呼び出しインターフェースを取得するために必要なアダプターを自動的に構築します。これは、新しい非常に強力な機能です。

私の見解では、パフォーマンスに関する限り、これはstd :: functionの最も重要な使用例です。一度構築された後にstd :: functionを何度も呼び出すコストに興味があり、コンパイラが実際に呼び出されている関数を知ることによって呼び出しを最適化できない状況である(つまり、適切なベンチマークを取得するには、別のソースファイルで実装を非表示にする必要がある)。

OPと同様に、以下のテストを行いました。主な変更点は次のとおりです。

  1. 各ケースは10億回ループしますが、std :: functionオブジェクトは1回だけ構築されます。実際のstd :: function呼び出しを作成するときに「operator new」が呼び出されるという出力コードを調べたところ、(最適化されていない場合はそうではない可能性があります)わかりました。
  2. テストは2つのファイルに分割され、不要な最適化を防止します
  3. (a)関数がインライン化されている(b)関数が通常の関数ポインタによって渡されている(c)関数がstd :: functionとしてラップされている互換関数である(d)関数が非互換関数であり、std ::と互換性があるバインド、std :: functionとしてラップ

私が得る結果は:

  • ケース(a)(インライン)1.3ナノ秒

  • その他すべての場合:3.3ナノ秒。

ケース(d)は少し遅くなる傾向がありますが、その差(約0.05ナノ秒)はノイズに吸収されます。

結論は、std :: functionは、実際の関数への単純な「バインド」適応がある場合でも、(呼び出し時の)関数ポインターの使用に匹敵するオーバーヘッドであることです。インラインは他のものより2 ns高速ですが、インラインが実行時に「ハードワイヤード」される唯一のケースであるため、これは予想されるトレードオフです。

同じマシンでjohan-lundbergのコードを実行すると、ループごとに約39ナノ秒が表示されますが、実際には、かなり高いstd :: functionの実際のコンストラクターとデストラクターを含めて、ループ内にはさらに多くのループがあります。新しい削除を伴うため。

-O2 gcc 4.8.1、x86_64ターゲット(コアi5)。

コードが2つのファイルに分割されていることに注意してください。これは、コンパイラーがそれらを呼び出す場所で関数を展開しないようにするためです(意図した場合を除きます)。

-----最初のソースファイル--------------

#include <functional>


// simple funct
float func_half( float x ) { return x * 0.5; }

// func we can bind
float mul_by( float x, float scale ) { return x * scale; }

//
// func to call another func a zillion times.
//
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with a function pointer
float test_funcptr( float (*func)(float), int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with inline function
float test_inline(  int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func_half(x);
    }
    return y;
}

----- 2番目のソースファイル-------------

#include <iostream>
#include <functional>
#include <chrono>

extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline(  int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );

int main() {
    using namespace std::chrono;


    for(int icase = 0; icase < 4; icase ++ ){
        const auto tp1 = system_clock::now();

        float result;
        switch( icase ){
         case 0:
            result = test_inline( 1e9);
            break;
         case 1:
            result = test_funcptr( func_half, 1e9);
            break;
         case 2:
            result = test_stdfunc( func_half, 1e9);
            break;
         case 3:
            result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
            break;
        }
        const auto tp2 = high_resolution_clock::now();

        const auto d = duration_cast<milliseconds>(tp2 - tp1);  
        std::cout << d.count() << std::endl;
        std::cout << result<< std::endl;
    }
    return 0;
}

興味のある方のために、「mul_by」をfloat(float)のように見せるためにコンパイラが作成したアダプタを示します。これは、bind(mul_by、_1,0.5)として作成された関数が呼び出されると「呼び出されます」。

movq    (%rdi), %rax                ; get the std::func data
movsd   8(%rax), %xmm1              ; get the bound value (0.5)
movq    (%rax), %rdx                ; get the function to call (mul_by)
cvtpd2ps    %xmm1, %xmm1        ; convert 0.5 to 0.5f
jmp *%rdx                       ; jump to the func

(したがって、バインドで0.5fを記述した場合は少し高速だったかもしれません...) 'x'パラメータは%xmm0に到着し、そこに留まることに注意してください。

test_stdfuncを呼び出す前の、関数が作成された領域のコードを次に示します-c ++ filtを実行します。

movl    $16, %edi
movq    $0, 32(%rsp)
call    operator new(unsigned long)      ; get 16 bytes for std::function
movsd   .LC0(%rip), %xmm1                ; get 0.5
leaq    16(%rsp), %rdi                   ; (1st parm to test_stdfunc) 
movq    mul_by(float, float), (%rax)     ; store &mul_by  in std::function
movl    $1000000000, %esi                ; (2nd parm to test_stdfunc)
movsd   %xmm1, 8(%rax)                   ; store 0.5 in std::function
movq    %rax, 16(%rsp)                   ; save ptr to allocated mem

   ;; the next two ops store pointers to generated code related to the std::function.
   ;; the first one points to the adaptor I showed above.

movq    std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq    std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)


call    test_stdfunc(std::function<float (float)> const&, int)

1
clang 3.4.1 x64では、結果は(a)1.0、(b)0.95、(c)2.0、(d)5.0です。
rustyx

4

あなたの結果は非常に興味深いものだったので、何が起こっているのかを理解するために少し掘り下げました。まず、他の多くの人が言ったように、計算の結果がなければ、コンパイラーはプログラムの状態を最適化するだけです。第二に、コールバックへの武装として与えられた定数3.3があることで、他の最適化が行われると思います。そのことを念頭に置いて、ベンチマークコードを少し変更しました。

template <typename F>
float calc1(F f, float i) { return -1.0f * f(i) + 666.0f; }
float calc2(std::function<float(float)> f, float i) { return -1.0f * f(i) + 666.0f; }
int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc2([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

このコードの変更を考慮して、gcc 4.8 -O3でコンパイルし、calc1で330ms、calc2で2702の時間を取得しました。したがって、テンプレートを使用すると8倍速くなり、この数は私には疑わしいように見えました。8の累乗の速度は、コンパイラが何かをベクトル化したことを示しています。テンプレートバージョン用に生成されたコードを見ると、明らかにベクトル化されていました

.L34:
cvtsi2ss        %edx, %xmm0
addl    $1, %edx
movaps  %xmm3, %xmm5
mulss   %xmm4, %xmm0
addss   %xmm1, %xmm0
subss   %xmm0, %xmm5
movaps  %xmm5, %xmm0
addss   %xmm1, %xmm0
cvtsi2sd        %edx, %xmm1
ucomisd %xmm1, %xmm2
ja      .L37
movss   %xmm0, 16(%rsp)

std :: functionバージョンとは異なります。テンプレートを使用すると、コンパイラーは関数がループ全体で変更されないことを確実に知っているが、渡されるstd :: functionが変更される可能性があるため、これはベクトル化できないため、これは私にとっては理にかなっています。

これにより、コンパイラーにstd :: functionバージョンで同じ最適化を実行できるかどうかを確認するために、別の方法を試すようになりました。関数を渡す代わりに、std :: functionをグローバル変数として作成し、これを呼び出します。

float calc3(float i) {  return -1.0f * f2(i) + 666.0f; }
std::function<float(float)> f2 = [](float arg){ return arg * 0.5f; };

int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc3([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

このバージョンでは、コンパイラーが同じ方法でコードをベクトル化しており、同じベンチマーク結果が得られています。

  • テンプレート:330ms
  • std :: function:2702ms
  • グローバルstd :: function:330ms

したがって、私の結論は、std :: functionとテンプレート関数の生の速度はほとんど同じです。ただし、オプティマイザの作業がはるかに困難になります。


1
重要なのは、ファンクタをパラメータとして渡すことです。あなたのcalc3ケースは意味がありません。calc3がf2を呼び出すようにハードコードされました。もちろん、それは最適化することができます。
rustyx 2016年

確かに、これは私が見せようとしていたものです。そのcalc3はテンプレートと同等であり、その状況では事実上、テンプレートと同じようにコンパイル時の構造になります。
ジョシュアリッターマン2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.