Dank C ++ 11 haben wir die std::function
Familie der Funktor-Wrapper erhalten. Leider höre ich immer wieder nur schlechte Dinge über diese Neuzugänge. Am beliebtesten ist, dass sie schrecklich langsam sind. Ich habe es getestet und sie saugen wirklich im Vergleich zu Vorlagen.
#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. Ich gehe davon aus, dass dies daran liegt, dass Vorlagen gut eingefügt werden können, während function
s die Interna über virtuelle Anrufe abdecken.
Offensichtlich haben Vorlagen ihre Probleme, wie ich sie sehe:
- Sie müssen als Header bereitgestellt werden, was Sie möglicherweise nicht möchten, wenn Sie Ihre Bibliothek als geschlossenen Code freigeben.
- Sie können die Kompilierungszeit erheblich verlängern, sofern keine
extern template
ähnlichen Richtlinien eingeführt werden. - Es gibt keine (zumindest mir bekannte) saubere Möglichkeit, Anforderungen (Konzepte, irgendjemand?) einer Vorlage darzustellen, abgesehen von einem Kommentar, der beschreibt, welche Art von Funktor erwartet wird.
Kann ich daher davon ausgehen, dass function
s als De-facto- Standard für vorbeifahrende Funktoren verwendet werden kann und an Orten, an denen eine hohe Leistung erwartet wird, Vorlagen verwendet werden sollten?
Bearbeiten:
Mein Compiler ist Visual Studio 2012 ohne CTP.
quelle
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).std::function
oder Vorlagen". Ich denke, hier geht es einfach darum, ein Lambdastd::function
einzupacken, anstatt ein Lambda nicht einzuwickelnstd::function
. Im Moment ist Ihre Frage wie die Frage: "Soll ich einen Apfel oder eine Schüssel bevorzugen?"Antworten:
Verwenden Sie im Allgemeinen Vorlagen , wenn Sie mit einer Entwurfssituation konfrontiert sind , in der Sie die Wahl haben . Ich habe das Wort Design betont, weil ich denke, dass Sie sich auf die Unterscheidung zwischen Anwendungsfällen und Vorlagen konzentrieren müssen, die ziemlich unterschiedlich sind.
std::function
Im Allgemeinen ist die Auswahl von Vorlagen nur ein Beispiel für ein umfassenderes Prinzip: Versuchen Sie, beim Kompilieren so viele Einschränkungen wie möglich anzugeben . Das Grundprinzip ist einfach: Wenn Sie einen Fehler oder eine Typinkongruenz feststellen können, bevor Ihr Programm generiert wird, senden Sie Ihrem Kunden kein fehlerhaftes Programm.
Darüber hinaus werden Aufrufe von Vorlagenfunktionen, wie Sie richtig ausgeführt haben, statisch aufgelöst (dh zur Kompilierungszeit), sodass der Compiler über alle erforderlichen Informationen verfügt, um den Code zu optimieren und möglicherweise zu integrieren (was nicht möglich wäre, wenn der Aufruf über a ausgeführt würde vtable).
Ja, es stimmt, dass die Vorlagenunterstützung nicht perfekt ist und C ++ 11 immer noch keine Unterstützung für Konzepte bietet. Ich sehe jedoch nicht ein, wie
std::function
Sie diesbezüglich gerettet werden könnten.std::function
ist keine Alternative zu Vorlagen, sondern ein Werkzeug für Entwurfssituationen, in denen Vorlagen nicht verwendet werden können.Ein solcher Anwendungsfall tritt auf, wenn Sie einen Aufruf zur Laufzeit auflösen müssen, indem Sie ein aufrufbares Objekt aufrufen, das einer bestimmten Signatur entspricht, dessen konkreter Typ jedoch zur Kompilierungszeit unbekannt ist. Dies ist normalerweise der Fall, wenn Sie eine Sammlung von Rückrufen potenziell unterschiedlicher Typen haben , die Sie jedoch einheitlich aufrufen müssen . Die Art und Anzahl der registrierten Rückrufe wird zur Laufzeit basierend auf dem Status Ihres Programms und der Anwendungslogik festgelegt. Einige dieser Rückrufe könnten Funktoren sein, andere einfache Funktionen, andere das Ergebnis der Bindung anderer Funktionen an bestimmte Argumente.
std::function
undstd::bind
bieten auch eine natürliche Redewendung für die Aktivierung der funktionalen Programmierung in C ++, bei der Funktionen als Objekte behandelt werden und auf natürliche Weise gewürfelt und kombiniert werden, um andere Funktionen zu generieren. Obwohl diese Art der Kombination auch mit Vorlagen erreicht werden kann, geht eine ähnliche Entwurfssituation normalerweise mit Anwendungsfällen einher, bei denen der Typ der kombinierten aufrufbaren Objekte zur Laufzeit bestimmt werden muss.Schließlich gibt es andere Situationen, in denen dies
std::function
unvermeidlich ist, z. B. wenn Sie rekursive Lambdas schreiben möchten . Diese Einschränkungen werden jedoch eher von technologischen Einschränkungen als von konzeptionellen Unterscheidungen bestimmt, die ich glaube.Zusammenfassend lässt sich sagen, dass Sie sich auf das Design konzentrieren und versuchen, die konzeptionellen Anwendungsfälle für diese beiden Konstrukte zu verstehen. Wenn Sie sie so vergleichen, wie Sie es getan haben, zwingen Sie sie in eine Arena, zu der sie wahrscheinlich nicht gehören.
quelle
std::function
auf der Speicherseite und VorlageFun
auf der Schnittstelle".unique_ptr<void>
Aufrufen geeigneter Destruktoren, selbst für Typen ohne virtuelle Destruktoren).Andy Prowl hat Designprobleme gut behandelt. Dies ist natürlich sehr wichtig, aber ich glaube, dass die ursprüngliche Frage mehr Leistungsprobleme betrifft
std::function
.Zunächst eine kurze Bemerkung zur Messtechnik: Die für erhaltenen 11ms haben
calc1
überhaupt keine Bedeutung. Wenn man sich die generierte Assembly ansieht (oder den Assemblycode debuggt), kann man feststellen, dass der Optimierer von VS2012 klug genug ist, um zu erkennen, dass das Ergebnis des Aufrufscalc1
unabhängig von der Iteration ist und den Aufruf aus der Schleife verschiebt:Darüber hinaus wird erkannt, dass Anrufe
calc1
keinen sichtbaren Effekt haben, und der Anruf wird insgesamt abgebrochen. Daher sind die 111 ms die Zeit, die die leere Schleife benötigt, um ausgeführt zu werden. (Ich bin überrascht, dass der Optimierer die Schleife beibehalten hat.) Seien Sie also vorsichtig mit Zeitmessungen in Schleifen. Dies ist nicht so einfach, wie es scheint.Wie bereits erwähnt, hat der Optimierer mehr Probleme zu verstehen
std::function
und verschiebt den Aufruf nicht aus der Schleife. 1241ms ist also ein faires Maß fürcalc2
.Beachten Sie, dass
std::function
verschiedene Arten von aufrufbaren Objekten gespeichert werden können. Daher muss es eine Art Löschmagie für die Speicherung ausführen. Im Allgemeinen impliziert dies eine dynamische Speicherzuweisung (standardmäßig durch einen Aufruf vonnew
). Es ist bekannt, dass dies eine ziemlich kostspielige Operation ist.Der Standard (20.8.11.2.1 / 5) enthält Implementierungen, um die dynamische Speicherzuweisung für kleine Objekte zu vermeiden, die VS2012 zum Glück (insbesondere für den Originalcode) tut.
Um eine Vorstellung davon zu bekommen, wie viel langsamer es werden kann, wenn es um die Speicherzuweisung geht, habe ich den Lambda-Ausdruck so geändert, dass er drei
float
Sekunden erfasst . Dies macht das aufrufbare Objekt zu groß, um die Optimierung für kleine Objekte anzuwenden:Für diese Version beträgt die Zeit ungefähr 16000 ms (im Vergleich zu 1241 ms für den Originalcode).
Beachten Sie schließlich, dass die Lebensdauer des Lambda die des
std::function
. In diesem Fall könnte, anstatt eine Kopie des Lambda zustd::function
speichern, ein "Verweis" darauf gespeichert werden. Mit "Referenz" meine ich eine,std::reference_wrapper
die leicht durch Funktionenstd::ref
und aufgebaut werden kannstd::cref
. Genauer gesagt, indem Sie Folgendes verwenden:Die Zeit verringert sich auf ungefähr 1860 ms.
Ich habe vor einiger Zeit darüber geschrieben:
http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059
Wie ich in dem Artikel sagte, gelten die Argumente für VS2010 aufgrund der schlechten Unterstützung von C ++ 11 nicht ganz. Zum Zeitpunkt des Schreibens war nur eine Beta-Version von VS2012 verfügbar, aber die Unterstützung für C ++ 11 war für diese Angelegenheit bereits gut genug.
quelle
calc1
könnte einfloat
Argument verwendet werden, das das Ergebnis der vorherigen Iteration wäre , um zu verhindern, dass der Code wegoptimiert wird. So etwas wiex = calc1(x, [](float arg){ return arg * 0.5f; });
. Darüber hinaus müssen wir sicherstellen, dasscalc1
Verwendungenx
. Das reicht aber noch nicht. Wir müssen einen Nebeneffekt erzeugen. Zum Beispiel nach der Messungx
auf dem Bildschirm drucken . Auch wenn ich damit einverstanden bin, dass die Verwendung von Spielzeugcodes für Timimg-Messungen nicht immer einen perfekten Hinweis darauf geben kann, was mit Real- / Produktionscode passieren wird.std::reference_wrapper
(zum Erzwingen von Vorlagen; nicht nur zum allgemeinen Speichern), und es ist lustig zu sehen, dass der Optimierer von VS eine leere Schleife nicht verwirft ... wie ich bei diesem GCC-Fehlervolatile
festgestellt habe .Bei Clang gibt es keinen Leistungsunterschied zwischen den beiden
Bei Verwendung von clang (3.2, Trunk 166872) (-O2 unter Linux) sind die Binärdateien aus beiden Fällen tatsächlich identisch .
-Ich komme zurück, um am Ende des Beitrags zu klirren. Aber zuerst gcc 4.7.2:
Es gibt bereits viele Erkenntnisse, aber ich möchte darauf hinweisen, dass das Ergebnis der Berechnungen von calc1 und calc2 aufgrund von In-Lining usw. nicht dasselbe ist. Vergleichen Sie zum Beispiel die Summe aller Ergebnisse:
mit calc2 wird das
während mit calc1 wird es
Das ist ein Faktor von ~ 40 in der Geschwindigkeitsdifferenz und ein Faktor von ~ 4 in den Werten. Der erste ist ein viel größerer Unterschied als das, was OP gepostet hat (mit Visual Studio). Das tatsächliche Ausdrucken des Werts a the end ist auch eine gute Idee, um zu verhindern, dass der Compiler Code ohne sichtbares Ergebnis entfernt (als ob-Regel). Cassio Neri hat dies bereits in seiner Antwort gesagt. Beachten Sie, wie unterschiedlich die Ergebnisse sind - Sie sollten vorsichtig sein, wenn Sie Geschwindigkeitsfaktoren von Codes vergleichen, die unterschiedliche Berechnungen durchführen.
Um fair zu sein, ist es vielleicht auch nicht so interessant, verschiedene Methoden zur wiederholten Berechnung von f (3.3) zu vergleichen. Wenn der Eingang konstant ist, sollte er sich nicht in einer Schleife befinden. (Für den Optimierer ist es leicht zu bemerken)
Wenn ich calc1 und 2 ein vom Benutzer angegebenes Wertargument hinzufüge, wird der Geschwindigkeitsfaktor zwischen calc1 und calc2 von 40 auf den Faktor 5 reduziert! Bei Visual Studio liegt der Unterschied nahe bei einem Faktor von 2, und bei Clang gibt es keinen Unterschied (siehe unten).
Da die Multiplikationen schnell sind, ist es oft nicht so interessant, über Verlangsamungsfaktoren zu sprechen. Eine interessantere Frage ist, wie klein Ihre Funktionen sind und ob dies den Engpass in einem echten Programm darstellt.
Clang:
Clang (ich habe 3.2 verwendet) hat tatsächlich identische Binärdateien erzeugt, als ich für den Beispielcode (siehe unten) zwischen calc1 und calc2 gewechselt habe. Mit dem ursprünglichen Beispiel in der Frage sind beide ebenfalls identisch, nehmen jedoch keine Zeit in Anspruch (die Schleifen werden wie oben beschrieben einfach vollständig entfernt). Mit meinem modifizierten Beispiel mit -O2:
Anzahl der auszuführenden Sekunden (Best of 3):
Die berechneten Ergebnisse aller Binärdateien sind gleich und alle Tests wurden auf demselben Computer ausgeführt. Es wäre interessant, wenn jemand mit tieferem Clang- oder VS-Wissen kommentieren könnte, welche Optimierungen möglicherweise vorgenommen wurden.
Mein geänderter Testcode:
Aktualisieren:
Hinzugefügt vs2015. Mir ist auch aufgefallen, dass es in calc1, calc2 Double-> Float-Konvertierungen gibt. Das Entfernen ändert nichts an der Schlussfolgerung für Visual Studio (beide sind viel schneller, aber das Verhältnis ist ungefähr gleich).
quelle
Anders ist nicht dasselbe.
Es ist langsamer, weil es Dinge tut, die eine Vorlage nicht kann. Insbesondere können Sie damit jede Funktion aufrufen , die mit den angegebenen Argumenttypen aufgerufen werden kann und deren Rückgabetyp aus demselben Code in den angegebenen Rückgabetyp konvertierbar ist .
Beachten Sie, dass dasselbe Funktionsobjekt
fun
an beide Aufrufe an übergeben wirdeval
. Es hat zwei verschiedene Funktionen.Wenn Sie das nicht tun müssen, sollten Sie nicht verwenden
std::function
.quelle
Sie haben hier bereits einige gute Antworten, daher werde ich ihnen nicht widersprechen. Kurz gesagt, das Vergleichen von std :: function mit Vorlagen ist wie das Vergleichen von virtuellen Funktionen mit Funktionen. Sie sollten virtuelle Funktionen niemals Funktionen "vorziehen", sondern virtuelle Funktionen verwenden, wenn dies für das Problem geeignet ist, und Entscheidungen von der Kompilierungszeit zur Laufzeit verschieben. Die Idee ist, dass Sie das Problem nicht mit einer maßgeschneiderten Lösung (wie einer Sprungtabelle) lösen müssen, sondern etwas verwenden, das dem Compiler eine bessere Chance bietet, für Sie zu optimieren. Es hilft auch anderen Programmierern, wenn Sie eine Standardlösung verwenden.
quelle
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:
Die Ergebnisse, die ich bekomme, sind:
Fall (a) (Inline) 1,3 ns
alle anderen Fälle: 3,3 ns.
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 --------------
----- zweite Quelldatei -------------
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:
(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:
quelle
Ich fand Ihre Ergebnisse sehr interessant, also habe ich ein bisschen gegraben, um zu verstehen, was los ist. Zunächst einmal, wie viele andere gesagt haben, ohne die Ergebnisse des Berechnungseffekts zu haben, wird der Status des Programms vom Compiler nur optimiert. Zweitens vermute ich, dass eine Konstante von 3,3 als Bewaffnung für den Rückruf gegeben wird, dass weitere Optimierungen stattfinden werden. In diesem Sinne habe ich Ihren Benchmark-Code ein wenig geändert.
Angesichts dieser Änderung des Codes habe ich mit gcc 4.8 -O3 kompiliert und eine Zeit von 330 ms für calc1 und 2702 für calc2 erhalten. Die Verwendung der Vorlage war also 8-mal schneller, diese Zahl sah für mich verdächtig aus. Eine Geschwindigkeit von 8 zeigt oft an, dass der Compiler etwas vektorisiert hat. Als ich mir den generierten Code für die Vorlagenversion ansah, war er eindeutig vektorisiert
Wobei wie die std :: function version nicht war. Dies ist für mich sinnvoll, da der Compiler mit der Vorlage sicher weiß, dass sich die Funktion während der gesamten Schleife niemals ändern wird, aber wenn die darin übergebene std :: -Funktion sich ändern könnte, kann sie daher nicht vektorisiert werden.
Dies veranlasste mich, etwas anderes zu versuchen, um zu sehen, ob ich den Compiler dazu bringen konnte, dieselbe Optimierung für die std :: function-Version durchzuführen. Anstatt eine Funktion zu übergeben, erstelle ich eine std :: -Funktion als globale Variable und lasse diese aufrufen.
Mit dieser Version sehen wir, dass der Compiler den Code jetzt auf die gleiche Weise vektorisiert hat und ich die gleichen Benchmark-Ergebnisse erhalte.
Mein Fazit ist also, dass die Geschwindigkeit einer std :: -Funktion gegenüber einem Template-Funktor ziemlich gleich ist. Dies erschwert jedoch die Arbeit des Optimierers erheblich.
quelle
calc3
Fall macht keinen Sinn; calc3 ist jetzt fest codiert, um f2 aufzurufen. Das kann natürlich optimiert werden.