Ein effizienter Weg als Funktionsreferenz?

8

Ich habe eine Klasse, die einen Verweis auf eine Funktion verwendet:

%Vor%

Diese Funktion würde während eines typischen Laufs etwa 10 8 mal heißen.

Die Klasse geht in eine Bibliothek und die Funktion u wird vom Benutzer der Bibliothek definiert. Also kann ich die Funktionsdefinition nicht innerhalb der Klasse haben.

Ich habe dies gelesen:

  

( std::function ) ... hat den Nachteil, einige (sehr   klein) Overhead beim Anrufen (also in einer sehr performance-kritischen   Situation könnte es ein Problem sein, aber in den meisten sollte es nicht)

Gibt es effizientere Möglichkeiten, die Funktion u an die Klasse equation zu übergeben? Und würde das als "sehr leistungskritische Situation" gelten?

BEARBEITEN

Es scheint ein bisschen Verwirrung zu geben. Um es klar zu stellen, die Funktion u ist zur Kompilierzeit der ausführbaren Dateien bekannt, aber nicht in der Bibliothek. Das Abrufen der Funktion zur Laufzeit ist ein Feature, das ich in späteren Versionen der Bibliothek berücksichtigen werde, aber nicht jetzt.

    
Eliad 24.04.2015, 19:40
quelle

5 Antworten

4

Ein Funktionszeiger (oder ein Verweis, der auf der Implementierungsebene fast identisch ist) funktioniert gut.

Moderne CPUs sind sehr gut bei der Verzweigungsvorhersage, nach den ersten paar Aufrufen wird die CPU erkennen, dass dieser "indirekte" Aufruf immer an dieselbe Stelle geht, und spekulative Ausführung verwenden, um die Pipeline voll zu halten .

Es wird jedoch immer noch keine Optimierung über die Funktionsgrenze hinweg geben. Kein Inlining, keine Auto-Vektorisierung.

Wenn diese Funktion 10 8 mal aufgerufen wird, ist es wahrscheinlich, dass eine große Anzahl von ihnen sich in einer sehr engen Schleife mit variierenden Parametern befindet. In diesem Fall empfehle ich, den Funktionsprototyp zu ändern, um ein Array von Parameterwerten zu akzeptieren und ein Array von Ergebnissen auszugeben. Dann haben Sie eine Schleife innerhalb der Funktion, wo der Compiler Optimierungen wie Abrollen und Auto-Vektorisierung durchführen kann.

(Dies ist ein spezieller Fall des allgemeinen Prinzips, mit Interop-Kosten umzugehen, indem die Anzahl der Anrufe über die Grenze hinweg reduziert wird)

Wenn das nicht möglich ist, übergeben Sie die Parameter als Wert. Wie andere gesagt haben, ist dies am effizientesten als const Referenz für Gleitkommavariablen. Wahrscheinlich eine Menge effizienter, da die meisten Aufrufkonventionen Floating-Point-Register (typischerweise SSE-Register, auf modernen Intel-Architekturen, bevor sie den x87-Stack verwendeten) verwenden, wo sie bereit sind, Berechnungen sofort durchzuführen. Das Verschütten von Werten zum / vom RAM, um durch Referenz zu gehen, ist ziemlich teuer, wenn die Funktion inline wird, dann wird Pass-by-Reference weg optimiert, aber das wird hier nicht passieren. Dies ist immer noch nicht so gut, wie ein ganzes Array zu übergeben.

    
Ben Voigt 24.04.2015, 21:48
quelle
4

Da die Funktion zur Kompilierzeit nicht bekannt ist, werden Sie nicht schneller als ein Funktionszeiger / Verweis.

Der Vorteil von std::function ist, dass Sie beispielsweise einen Funktor, einen Mitgliedsfunktionszeiger oder Lambda-Ausdrücke verwenden können. Aber da ist etwas Overhead.

Wie ein Kommentar erwähnt, würde ich die const double & args durch double ersetzen. Die Größe ist heutzutage auf den meisten Plattformen gleich und entfernt eine Dereferenzierung.

Hier ist ein Beispiel mit std::function :

%Vor%

Aber wiederum hat dieser Flexibilitätsgewinn geringe Laufzeitkosten. Ob es sich lohnt, hängt von der Anwendung ab. Ich würde wahrscheinlich zuerst auf Allgemeingültigkeit und eine flexible Schnittstelle zurückgreifen und dann später ein Profil erstellen, um zu sehen, ob es ein echtes Problem für die Arten von Funktionen ist, die in der Praxis verwendet werden.

Wenn Sie Ihren Solver zu einer Nur-Header-Bibliothek machen können, können Sie möglicherweise eine bessere Leistung erzielen, wenn der Benutzer inline-fähige Funktionen in seinem Code bereitstellt. Zum Beispiel:

%Vor%

Ihre Instanziierung der Klasse Equation kann nun die Ausführung der Funktion vollständig inline machen. Das Aufrufen ist sehr ähnlich, wenn man die Funktion make_equation annimmt (die obige Implementierung geht von C ++ 14 aus, aber die C ++ 11-Version unterscheidet sich nicht wesentlich):

%Vor%

Bei der vollständigen Optimierung werden Sie wahrscheinlich nur den Aufruf von printf mit den Ergebnissen der Berechnung im kompilierten Code finden.

    
sfjac 24.04.2015 19:57
quelle
3

Verwenden von Vorlagenargumenten:

%Vor%

Wenn Sie die Parameter für die Funktion nicht ändern müssen, ist es in der Funktion besser, den Wert zu übergeben (im Fall von Einbautypen sind dies höchstwahrscheinlich Werte, die in CPU-Registern geladen werden oder sind bereits geladen).

%Vor%

Dies wäre höchstwahrscheinlich inline u in using_the_function method. In Ihrem Fall konnte der Compiler das nicht, weil der Funktionszeiger auf irgendeine Funktion zeigen konnte.

Das mögliche Problem dieses Ansatzes, wenn Code aufgebläht ist, wenn Sie viele verschiedene Funktionen und / oder Klassen unterstützen müssen, ist groß.

    
NetVipeC 24.04.2015 19:51
quelle
1

Mit 10 ^ 8 Aufrufen und keiner Möglichkeit, die Funktionsdefinition zur Kompilierzeit den Aufrufern zur Verfügung zu stellen, würde ich eine Entwurfsänderung vorschlagen, wenn möglich, etwa so:

%Vor%

Die Idee, equation und andere ähnliche Funktionen zu erlauben, mehrere Ergebnisse in einem einzigen Aufruf zu erhalten.

Nun wird dir das nicht automatisch einen Geschwindigkeitsschub geben. Sie können leicht einen Overhead für einen anderen austauschen, wenn Sie sagen, dass Sie eine Arbeitswarteschlange mit variabler Größe für u erstellen und Ihr Algorithmus sehr seriell ist. Es hängt stark von der Art der Algorithmen ab, die u verwenden.

Wenn jedoch die Algorithmen z. B. schnell ein Array von N x und y erstellen können, um auf dem Hardwarestapel zu berechnen, sagen wir mal 8, kann das helfen.

Es macht auch u geeignet für Multithreading mit parallelen fors und so, da die Arbeit in der Regel ausreichend umfangreich für u sein soll, um den Thread-Overhead zu trivialisieren, der mit den Planungsaufgaben für u verbunden ist jeder Thread. Wenn Sie also u so berechnen, dass eine variable Anzahl von Ergebnissen gleichzeitig berechnet wird, können Sie wirklich eine stabilere Schnittstelle entwerfen, die verhindern kann, dass sie als Reaktion auf Optimierungen unterbrochen wird.

    
Team Upvote 05.05.2015 16:29
quelle
-2

Da Sie c ++ 11 verwenden können, können Sie std::bind verwenden. Es bindet den Funktionszeiger mit seinen Argumenten an eine Variable. Die Argumente können Platzhalter sein und dynamisch zur Laufzeit geändert werden.

Wie so:

%Vor%     
KuramaYoko 24.04.2015 19:55
quelle