この回答は、既存の回答のセットに、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と同様に、以下のテストを行いました。ただし、主な変更点は次のとおりです。
- 各ケースは10億回ループしますが、std::functionオブジェクトは1回だけ作成されます。出力コードを見ると、実際のstd :: function呼び出しを作成するときに「operatornew」が呼び出されることがわかりました(最適化されている場合はそうではないかもしれません)。
- 望ましくない最適化を防ぐために、テストは2つのファイルに分割されます
- 私の場合は次のとおりです。(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)