Diese Antwort soll zu den vorhandenen Antworten einen meiner Meinung nach aussagekräftigeren Maßstab für die Laufzeitkosten von std :: function-Aufrufen beitragen.
Der std :: -Funktionsmechanismus sollte für das erkannt werden, was er bietet: Jede aufrufbare Entität kann in eine std :: -Funktion mit entsprechender Signatur konvertiert werden. Angenommen, Sie haben eine Bibliothek, die eine Oberfläche an eine durch z = f (x, y) definierte Funktion anpasst. Sie können sie schreiben, um a zu akzeptieren std::function<double(double,double)>
, und der Benutzer der Bibliothek kann jede aufrufbare Entität problemlos in diese konvertieren. Sei es eine gewöhnliche Funktion, eine Methode einer Klasseninstanz oder ein Lambda oder alles, was von std :: bind unterstützt wird.
Im Gegensatz zu Vorlagenansätzen funktioniert dies, ohne dass die Bibliotheksfunktion für verschiedene Fälle neu kompiliert werden muss. Dementsprechend wird für jeden weiteren Fall wenig zusätzlicher kompilierter Code benötigt. Es war immer möglich, dies zu erreichen, aber früher waren einige umständliche Mechanismen erforderlich, und der Benutzer der Bibliothek musste wahrscheinlich einen Adapter um seine Funktion herum erstellen, damit es funktioniert. Die Funktion std :: erstellt automatisch den Adapter, der benötigt wird, um eine gemeinsame Laufzeitaufrufschnittstelle für alle Fälle zu erhalten. Dies ist eine neue und sehr leistungsstarke Funktion.
Meiner Ansicht nach ist dies der wichtigste Anwendungsfall für die std :: -Funktion in Bezug auf die Leistung: Ich bin an den Kosten interessiert, die entstehen, wenn eine std :: -Funktion viele Male aufgerufen wird, nachdem sie einmal erstellt wurde, und das muss Es kann vorkommen, dass der Compiler den Aufruf nicht optimieren kann, indem er die tatsächlich aufgerufene Funktion kennt (dh Sie müssen die Implementierung in einer anderen Quelldatei ausblenden, um einen geeigneten Benchmark zu erhalten).
Ich habe den folgenden Test durchgeführt, ähnlich wie bei den OPs. aber die wichtigsten Änderungen sind:
- Jeder Fall wird 1 Milliarde Mal wiederholt, aber die std :: function-Objekte werden nur einmal erstellt. Ich habe anhand des Ausgabecodes festgestellt, dass beim Erstellen tatsächlicher std :: -Funktionsaufrufe 'operator new' aufgerufen wird (möglicherweise nicht, wenn sie optimiert sind).
- Der Test ist in zwei Dateien aufgeteilt, um unerwünschte Optimierungen zu vermeiden
- Meine Fälle sind: (a) Funktion ist inline (b) Funktion wird von einem gewöhnlichen Funktionszeiger übergeben (c) Funktion ist eine kompatible Funktion, die als std :: Funktion umschlossen ist (d) Funktion ist eine inkompatible Funktion, die mit einem std :: kompatibel gemacht wurde binden, als std :: function verpackt
Die Ergebnisse, die ich bekomme, sind:
Fall (d) ist tendenziell etwas langsamer, aber die Differenz (etwa 0,05 ns) wird im Rauschen absorbiert.
Die Schlussfolgerung ist, dass die std :: -Funktion (zum Zeitpunkt des Aufrufs) mit der Verwendung eines Funktionszeigers vergleichbar ist, selbst wenn eine einfache Anpassung an die eigentliche Funktion erfolgt. Die Inline ist 2 ns schneller als die anderen, aber das ist ein erwarteter Kompromiss, da die Inline der einzige Fall ist, der zur Laufzeit fest verdrahtet ist.
Wenn ich den Code von johan-lundberg auf demselben Computer ausführe, werden ungefähr 39 ns pro Schleife angezeigt, aber in der Schleife befindet sich noch viel mehr, einschließlich des tatsächlichen Konstruktors und Destruktors der Funktion std ::, der wahrscheinlich ziemlich hoch ist da es sich um eine neue und löschen handelt.
-O2 gcc 4.8.1 zum x86_64-Ziel (Kern i5).
Beachten Sie, dass der Code in zwei Dateien aufgeteilt ist, um zu verhindern, dass der Compiler die Funktionen dort erweitert, wo sie aufgerufen werden (außer in dem einen Fall, in dem dies beabsichtigt ist).
----- erste Quelldatei --------------
#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;
}
----- zweite Quelldatei -------------
#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;
}
Für Interessenten ist hier der Adapter, den der Compiler erstellt hat, damit 'mul_by' wie ein float (float) aussieht - dies wird 'aufgerufen', wenn die als bind (mul_by, _1,0.5) erstellte Funktion aufgerufen wird:
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
(Es wäre also vielleicht etwas schneller gewesen, wenn ich 0,5 f in die Bindung geschrieben hätte ...) Beachten Sie, dass der Parameter 'x' in% xmm0 ankommt und einfach dort bleibt.
Hier ist der Code in dem Bereich, in dem die Funktion erstellt wurde, bevor test_stdfunc aufgerufen wird - führen Sie c ++ filt aus:
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)
std::function
Option nur dann, wenn Sie tatsächlich eine heterogene Sammlung aufrufbarer Objekte benötigen (dh zur Laufzeit sind keine weiteren Unterscheidungsinformationen verfügbar).