web-dev-qa-db-ja.com

std :: function vs template

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

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

  • ヘッダーとして提供する必要がありますが、これは、ライブラリをクローズドコードとしてリリースするときに行いたくないことではありません。
  • extern templateのようなポリシーが導入されない限り、コンパイル時間が非常に長くなる場合があります。
  • テンプレートの要件(概念、だれでも)を表す明確な方法はありません(少なくとも私には知られています)。

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


編集:

私のコンパイラはVisual Studio 2012 without CTPです。

155
Red XIII

一般的に、選択できる design 状況に直面している場合は、use templatesを使用します。 Word design に重点を置いたのは、std::functionとテンプレートの使用ケースの違いであり、これらはかなり異なるため、焦点を当てる必要があると思うからです。

一般に、テンプレートの選択は、より広い原則の単なる例です。コンパイル時に可能な限り多くの制約を指定してみてください。その理由は簡単です。プログラムを生成する前であっても、エラーや型の不一致をキャッチできる場合、バグの多いプログラムを顧客に出荷することはありません。

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

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

そのような使用例の1つは、特定の署名に準拠しているが、具体的な型がコンパイル時に不明である呼び出し可能オブジェクトを呼び出すことにより、呼び出しat-timeを解決する必要がある場合に発生します-時間。これは通常、潜在的に異なるタイプのコールバックのコレクションがあるが、一様に呼び出す必要がある場合です/;登録されたコールバックのタイプと数は、プログラムの状態とアプリケーションロジックに基づいて実行時に決定されます。それらのコールバックの一部はファンクターであり、一部は単純な関数であり、一部は他の関数を特定の引数にバインドした結果です。

std::functionおよびstd::bindは、C++で機能プログラミングを有効にするための自然なイディオムも提供します。 。この種の組み合わせはテンプレートでも実現できますが、通常、同様の設計状況には、実行時に結合された呼び出し可能オブジェクトのタイプを決定する必要があるユースケースが伴います。

最後に、std::functionが避けられない他の状況があります。 recursive lambdas ;を書きたい場合しかし、これらの制限は、私が信じる概念的な違いよりも技術的な制限により決定されます。

要約すると、デザインに焦点を当てる、これら2つの構成要素の概念的なユースケースを理解しようとします。あなたがしたようにそれらを比較に入れた場合、あなたは彼らがおそらく属していないアリーナにそれらを強制しています。

165
Andy Prowl

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

まず、測定手法について簡単に説明します。calc1で得られた11ミリ秒はまったく意味がありません。実際、生成されたAssemblyを見る(またはAssemblyコードをデバッグする)と、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および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

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

86
Cassio Neri

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になります! Visual Studioの場合、差は2倍に近く、clangの場合、差はありません(以下を参照)。

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

Clang:

Clang(3.2を使用)が実際に生成されたidenticalバイナリをサンプルコードの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の結論は変わりません(どちらもはるかに高速ですが、比率はほぼ同じです)。

37
Johan Lundberg

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

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

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つのdifferent関数を保持します。

そうする必要がない場合は、not use std::functionを使用する必要があります。

13
Pete Becker

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

8
TheAgitator

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

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

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

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

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

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

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

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

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

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

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

同じマシンでjohan-lundbergのコードを実行すると、ループごとに約39ナノ秒が表示されますが、実際のコンストラクターとstd :: destructorを含むループにはさらに多くのものがあります。それは新規および削除を伴うためです。

-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)
6
greggo

私はあなたの結果が非常に興味深いと感じたので、何が起こっているのかを理解するために少し掘り下げました。まず、他の多くの人が、計算の結果がプログラムの状態に影響を与えずに、コンパイラがこれを最適化するだけだと言っています。第二に、コールバックに対する武器として定数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とテンプレートファンクタの生の速度はほぼ同じです。ただし、オプティマイザーの仕事ははるかに困難になります。

4