79 Stimmen

Wie viel Overhead gibt es beim Aufrufen einer Funktion in C++?

In der Literatur wird viel über die Verwendung von Inline-Funktionen gesprochen, um "den Overhead eines Funktionsaufrufs zu vermeiden". Allerdings habe ich keine konkreten Daten gesehen. Was ist tatsächlich der Overhead eines Funktionsaufrufs, d.h. welchen Leistungszuwachs erzielen wir durch die Inline-Funktionen?

0 Stimmen

Voting to close as "zu breit". Dies kann nicht wirklich sinnvoll beantwortet werden. Es hängt vom Compiler, der Hotspot-Erkennung, der CPU (Caching, spekulativer Ausführung, Branch-Ziel-Pufferung) und vielen weiteren Faktoren ab.

52voto

Eclipse Punkte 43775

Auf den meisten Architekturen besteht der Aufwand darin, alle (oder einige oder keine) Register auf den Stapel zu legen, die Funktionargumente auf den Stapel zu schieben (oder sie in Register zu setzen), den Stapelzeiger zu erhöhen und zum Anfang des neuen Codes zu springen. Dann, wenn die Funktion beendet ist, müssen die Register vom Stapel wiederhergestellt werden. Diese Webseite enthält eine Beschreibung dessen, was bei den verschiedenen Aufrufkonventionen beteiligt ist.

Die meisten C++-Compiler sind inzwischen klug genug, um Funktionen für Sie zu inline. Das Schlüsselwort inline ist nur ein Hinweis an den Compiler. Einige werden sogar Inlining über Übersetzungseinheiten hinweg machen, wenn sie der Meinung sind, dass es hilfreich ist.

10 Stimmen

Auf x86 (und vielen anderen Architekturen) müssen nicht ALLE Register gesichert werden, da erwartet wird, dass sie durch Funktionsaufrufe geändert werden. Die C-Aufrufkonvention auf x86 erhält in der Regel nicht eax, ecx und edx.

1 Stimmen

Das Durchschieben aller Funktionsparameter auf den Stack ist das C ABI. C++ spezifiziert kein spezifisches ABI als Teil des Standards (im Gegensatz zu C). Somit kann jeder Compiler optimieren, wie erforderlich. Daher schieben die meisten C++-Compiler nicht alle Parameter auf den Stack.

9 Stimmen

@Martin York: Das C-ABI ist nicht Teil des Standards - es kann nicht sein, da der Standard architekturunabhängig ist, während das ABI von der Architektur abhängt. Die standardisierten ABIs für C, die es als Basis-Austausch- und Klebesprache verwendet werden lassen, werden vom Betriebssystem oder Chip-Hersteller erstellt. BeOS hat ein C++ ABI.

41voto

PSkocik Punkte 54603

Ich habe einen einfachen Benchmark gegen eine einfache Inkrement-Funktion gemacht:

inc.c:

typedef unsigned long ulong;
ulong inc(ulong x){
    return x+1;
}

main.c

#include 
#include 

typedef unsigned long ulong;

#ifdef EXTERN 
ulong inc(ulong);
#else
static inline ulong inc(ulong x){
    return x+1;
}
#endif

int main(int argc, char** argv){
    if (argc < 1+1)
        return 1;
    ulong i, sum = 0, cnt;
    cnt = atoi(argv[1]);
    for(i=0;i

`

Bei einer Milliarde Iterationen auf meinem Intel(R) Core(TM) i5 CPU M 430 @ 2.27GHz hat sich folgendes ergeben:

  • 1,4 Sekunden für die Inline Version
  • 4,4 Sekunden für die regulär verlinkte Version

(Es scheint um bis zu 0,2 zu schwanken, aber ich bin zu faul, um vernünftige Standardabweichungen zu berechnen, noch interessieren sie mich)

Dies deutet darauf hin, dass der Overhead von Funktionsaufrufen auf diesem Computer etwa 3 Nanosekunden beträgt.

Das Schnellste, was ich gemessen habe, beträgt etwa 0,3ns, was darauf hindeutet, dass ein Funktionsaufruf etwa 9 primitive Operationen kostet, um es sehr vereinfacht auszudrücken.

Dieser Overhead erhöht sich um weitere 2ns pro Aufruf (Gesamtzeit eines Aufrufs ca. 6ns) für Funktionen, die über eine PLT (Funktionen in einer Shared Library) aufgerufen werden.

`

0 Stimmen

Interessant. Ich frage mich, ob dies aufgrund eines Cache-Fehlers geschieht, wenn Sie die Funktion verwenden.

12voto

nedruod Punkte 1094

Es gibt die technische und die praktische Antwort. Die praktische Antwort ist, dass es nie wichtig sein wird und in den sehr seltenen Fällen, in denen es wichtig ist, werden Sie es nur durch tatsächlich profilierte Tests erfahren.

Die technische Antwort, auf die sich Ihre Literatur bezieht, ist im Allgemeinen aufgrund von Compiler-Optimierungen nicht relevant. Aber wenn Sie noch interessiert sind, wird sie gut von Josh beschrieben.

Was ein "Prozentsatz" betrifft, müssten Sie wissen, wie teuer die Funktion selbst ist. Außer den Kosten der aufgerufenen Funktion gibt es keinen Prozentsatz, weil Sie dies mit einer kostenfreien Operation vergleichen. Bei Inline-Code entstehen keine Kosten, der Prozessor springt einfach zur nächsten Instruktion. Der Nachteil von Inline-Code ist eine größere Codegröße, die sich auf eine andere Weise als die Kosten für den Stack-Aufbau/-Abbau auswirkt.

9voto

Mecki Punkte 113876

Ihre Frage ist eine der Fragen, die keine Antwort hat, die man die "absolute Wahrheit" nennen könnte. Der Overhead eines normalen Funktionsaufrufs hängt von drei Faktoren ab:

  1. Die CPU. Der Overhead von x86-, PPC- und ARM-CPUs variiert stark, und selbst wenn Sie nur bei einer Architektur bleiben, variiert der Overhead auch ziemlich stark zwischen einem Intel Pentium 4, einem Intel Core 2 Duo und einem Intel Core i7. Der Overhead kann sogar deutlich zwischen einem Intel- und einem AMD-Prozessor variieren, auch wenn beide mit der gleichen Taktfrequenz laufen, da Faktoren wie Cache-Größen, Caching-Algorithmen, Speicherzugriffsmuster und die tatsächliche Hardwareimplementierung des Call-Opcodes selbst einen großen Einfluss auf den Overhead haben können.

  2. Die ABI (Application Binary Interface). Selbst bei derselben CPU existieren oft unterschiedliche ABIs, die spezifizieren, wie Parameter bei Funktionsaufrufen übergeben werden (über Register, über Stack oder über eine Kombination aus beidem) und wo und wie die Initialisierung und Aufräumung des Stackrahmens erfolgt. All dies beeinflusst den Overhead. Unterschiedliche Betriebssysteme können unterschiedliche ABIs für dieselbe CPU verwenden; z.B. können Linux, Windows und Solaris alle drei ein anderes ABI für dieselbe CPU verwenden.

  3. Der Compiler. Das strikte Befolgen der ABI ist nur wichtig, wenn Funktionen zwischen unabhängigen Codeeinheiten aufgerufen werden, z.B. wenn eine Anwendung eine Funktion einer Systembibliothek aufruft oder eine Benutzerbibliothek eine Funktion einer anderen Benutzerbibliothek aufruft. Solange Funktionen "privat" sind und außerhalb einer bestimmten Bibliothek oder eines bestimmten Binärdatenträgers nicht sichtbar sind, kann der Compiler "tricksen". Er muss sich nicht strikt an die ABI halten, sondern kann Abkürzungen verwenden, die zu schnelleren Funktionsaufrufen führen. Er kann z.B. Parameter in einem Register übergeben anstatt den Stack zu verwenden oder er kann das Einrichten und Aufräumen des Stackrahmens komplett überspringen, wenn es wirklich nicht notwendig ist.

Wenn Sie den Overhead für eine bestimmte Kombination der drei oben genannten Faktoren, z.B. für Intel Core i5 unter Linux mit GCC, wissen möchten, ist Ihr einziger Weg, diese Informationen zu erhalten, das Benchmarking des Unterschieds zwischen zwei Implementierungen, einer mit Funktionsaufrufen und einer, bei der Sie den Code direkt in den Aufrufer kopieren; auf diese Weise erzwingen Sie definitiv das Inline-Einfügen, da die Inline-Anweisung nur ein Hinweis ist und nicht immer zum Inline-Einfügen führt.

Die eigentliche Frage hier ist jedoch: Spielt der genaue Overhead wirklich eine Rolle? Eines ist sicher: Ein Funktionsaufruf hat immer einen Overhead. Er kann klein sein, er kann groß sein, aber er existiert auf jeden Fall. Und egal wie klein er ist, wenn eine Funktion in einem leistungsentscheidenden Abschnitt oft aufgerufen wird, wird der Overhead in gewissem Maße eine Rolle spielen. Das Inline-Einfügen macht Ihren Code selten langsamer, es wird ihn jedoch größer machen. Die heutigen Compiler sind ziemlich gut darin, selbst zu entscheiden, wann sie inline einfügen und wann nicht, sodass Sie sich kaum Gedanken darüber machen müssen.

Persönlich ignoriere ich das Inline-Einfügen während der Entwicklung vollständig, bis ich ein mehr oder weniger brauchbares Produkt habe, das ich profilieren kann, und nur wenn das Profiling mir sagt, dass eine bestimmte Funktion wirklich oft aufgerufen wird und auch innerhalb eines leistungsentscheidenden Abschnitts der Anwendung, werde ich das "erzwungene Inline-Einfügen" dieser Funktion in Betracht ziehen.

Bisher ist meine Antwort sehr allgemein, sie gilt sowohl für C als auch für C++ und Objective-C. Als Abschluss möchte ich noch etwas zu C++ sagen: Methoden, die virtuell sind, sind doppelte indirekte Funktionsaufrufe, das bedeutet, sie haben einen höheren Funktionsaufruf-Overhead als normale Funktionsaufrufe und sie können auch nicht eingefügt werden. Nichtvirtuelle Methoden können vom Compiler eingefügt werden oder nicht, aber auch wenn sie nicht eingefügt werden, sind sie immer noch signifikant schneller als virtuelle. Sie sollten also Methoden nicht virtuell machen, es sei denn, Sie planen wirklich, sie zu überschreiben oder haben vor, sie überschreiben zu lassen.

8voto

Mark Ransom Punkte 283960

Die Höhe des Overheads hängt vom Compiler, der CPU usw. ab. Der prozentuale Overhead hängt vom Code ab, den Sie einbinden. Der einzige Weg, um es herauszufinden, besteht darin, Ihren Code zu nehmen und ihn auf beide Arten zu profilieren - das ist der Grund, warum es keine endgültige Antwort gibt.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X