175

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ミリ秒対1241ミリ秒。これは、テンプレートを適切にインライン化できる一方で、function仮想呼び出しを介して内部をカバーできるためだと思います。

明らかに、テンプレートには問題があります。

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

したがって、 sはパスファンクタのデファクトfunctionスタンダードとして使用でき、高性能が期待される場所ではテンプレートを使用する必要があると想定できますか?


編集:

私のコンパイラは、CTPを使用しないVisualStudio2012です。

4

8 に答える 8

182

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

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

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

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

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

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

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

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

于 2013-02-03T22:50:10.990 に答える
92

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

まず、測定手法について簡単に説明します。で得られた11mscalc1は、まったく意味がありません。実際、生成されたアセンブリを見る(またはアセンブリコードをデバッグする)と、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。これにより、呼び出し可能オブジェクトが大きくなりすぎて、小さなオブジェクトの最適化を適用できなくなります。

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によって簡単に構築できるaを意味します。より正確には、以下を使用します。std::refstd::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-02-22T01:42:58.903 に答える
38

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が投稿したもの(ビジュアルスタジオを使用)よりもはるかに大きな違いです。実際に値を最後に出力することも、コンパイラがコードを削除して結果が表示されないようにすることをお勧めします(ルールのように)。カシオ・ネリはすでに彼の答えの中でこれを言っています。結果がどれほど異なるかに注意してください-異なる計算を実行するコードの速度係数を比較するときは注意が必要です。

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

ユーザー指定の値引数をcalc1と2に追加すると、calc1とcalc2の間の速度係数は40から5の係数になります。Visual Studioの場合、違いは2倍に近く、clangの場合は違いはありません(以下を参照)。

また、乗算は高速であるため、速度低下の要因について話すことはそれほど興味深いことではありません。さらに興味深い質問は、関数がどれだけ小さいかということです。これらは実際のプログラムのボトルネックと呼ばれていますか?

Clang:

サンプルコード(以下に投稿)のcalc1とcalc2を切り替えると、Clang(3.2を使用)は実際に同じバイナリを生成しました。質問に投稿された元の例では、両方とも同じですが、まったく時間がかかりません(上記のようにループが完全に削除されています)。私の修正した例では、-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の結論は変わりません(どちらもはるかに高速ですが、比率はほぼ同じです)。

于 2013-02-23T20:17:07.117 に答える
14

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

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

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でください。

于 2013-02-03T22:50:01.050 に答える
8

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

于 2013-02-21T19:46:31.890 に答える
6

この回答は、既存の回答のセットに、std::function呼び出しの実行時コストのより意味のあるベンチマークであると私が信じているものに貢献することを目的としています。

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

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

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

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

  1. 各ケースは10億回ループしますが、std::functionオブジェクトは1回だけ作成されます。出力コードを見ると、実際のstd :: function呼び出しを作成するときに「operatornew」が呼び出されることがわかりました(最適化されている場合はそうではないかもしれません)。
  2. 望ましくない最適化を防ぐために、テストは2つのファイルに分割されます
  3. 私の場合は次のとおりです。(a)関数がインライン化されている(b)関数が通常の関数ポインターによって渡されている(c)関数がstd :: functionとしてラップされた互換性のある関数である(d)関数がstd::と互換性のある互換性のない関数であるバインド、std::functionとしてラップ

私が得た結果は次のとおりです。

  • ケース(a)(インライン)1.3 nsec

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

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

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

同じマシンでjohan-lundbergのコードを実行すると、ループごとに約39 nsecが表示されますが、実際のコンストラクタやstd :: functionのデストラクタなど、ループにはさらに多くのものがあります。これはおそらくかなり高いです。新規および削除が含まれるためです。

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

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

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

#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)
于 2014-03-05T01:42:09.703 に答える
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では2702msの時間を取得しました。したがって、テンプレートの使用は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
  • グローバル標準::関数:330ms

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

于 2014-08-18T03:59:48.653 に答える
1

C ++ 20の代わりにテンプレート使用する場合は、実際に可変個引数テンプレートを使用して独自の概念を作成できます( C ++ 20の概念に関するHendrikNiemeyerの講演に触発されています)。std::function

template<class Func, typename Ret, typename... Args>
concept functor = std::regular_invocable<Func, Args...> && 
                  std::same_as<std::invoke_result_t<Func, Args...>, Ret>;

次に、が戻り値であり、可変個引数の入力引数functor<Ret, Args...> F>であるとして使用できます。例:RetArgs...functor<double,int> F

template <functor<double,int> F>
auto CalculateSomething(F&& f, int const arg) {
  return f(arg)*f(arg);
}

テンプレート引数としてファンクターが必要です。これは、()演算子をオーバーロードする必要があり、double戻り値と型の単一の入力引数を持ちintます。同様に、入力引数を受け取らない戻り型functor<double>のファンクターになります。double

ここで試してみてください!

次のような可変個引数関数でも使用できます

template <typename... Args, functor<double, Args...> F>
auto CalculateSomething(F&& f, Args... args) {
  return f(args...)*f(args...);
}

ここで試してみてください!

于 2021-06-01T23:30:56.560 に答える