it-swarm.com.de

Macht der Funktionszeiger das Programm langsam?

Ich habe in C über Funktionszeiger gelesen. Und alle sagten, dass mein Programm dadurch langsamer wird. Ist es wahr?

Ich habe ein Programm erstellt, um es zu überprüfen. Und ich habe in beiden Fällen die gleichen Ergebnisse erzielt. (Messen Sie die Zeit.)

Ist es also schlecht, Funktionszeiger zu verwenden? Danke im Voraus.

Um für einige Leute zu antworten. Ich sagte 'lauf langsam' für die Zeit, die ich in einer Schleife verglichen habe. so was:

int end = 1000;
int i = 0;

while (i < end) {
 fp = func;
 fp ();
}

Wenn Sie dies ausführen, habe ich die gleiche Zeit, wenn ich dies ausführen.

while (i < end) {
 func ();
}

Ich denke also, dass Funktionszeiger keinen Zeitunterschied haben und ein Programm nicht langsam laufen lassen, wie viele Leute sagten.

49
drigoSkalWalker

In Situationen, die aus Performance-Sicht tatsächlich wichtig sind, z. B. das wiederholte Aufrufen der Funktion in einem Zyklus, ist die Leistung möglicherweise überhaupt nicht unterschiedlich.

Dies mag seltsam für Menschen klingen, die daran gewöhnt sind, über C-Code als etwas nachzudenken, das von einer abstrakten C-Maschine ausgeführt wird, deren "Maschinensprache" der C-Sprache selbst sehr nahe kommt. In einem solchen Kontext ist "standardmäßig" ein indirekter Aufruf einer Funktion in der Tat langsamer als ein direkter, da formal ein zusätzlicher Speicherzugriff erforderlich ist, um das Ziel des Anrufs zu bestimmen. 

In der Praxis wird der Code jedoch von einer realen Maschine ausgeführt und von einem optimierenden Compiler kompiliert, der über ziemlich gute Kenntnisse der zugrunde liegenden Maschinenarchitektur verfügt, wodurch er den optimalsten Code für diese bestimmte Maschine generiert. Auf vielen Plattformen könnte sich herausstellen, dass der effizienteste Weg zum Ausführen eines Funktionsaufrufs aus einem Zyklus tatsächlich zu einem identisch - Code sowohl für den direkten als auch den indirekten Aufruf führt, was zu einer identischen Leistung der beiden führt.

Betrachten Sie zum Beispiel die x86-Plattform. Wenn wir einen direkten und indirekten Aufruf "wörtlich" in Maschinencode übersetzen, könnte dies zu einem Ergebnis führen

// Direct call
do-it-many-times
  call 0x12345678

// Indirect call
do-it-many-times
  call dword ptr [0x67890ABC]

Ersteres verwendet einen unmittelbaren Operanden in der Maschinenanweisung und ist normalerweise normalerweise schneller als letzterer, der die Daten von einem unabhängigen Speicherplatz lesen muss.

An dieser Stelle sei daran erinnert, dass die x86-Architektur tatsächlich eine weitere Möglichkeit hat, einen Operanden für die call-Anweisung bereitzustellen. Es liefert die Zieladresse in einem register . Und sehr wichtig bei diesem Format ist, dass es normalerweise schneller ist als beide der beiden oben genannten . Was heißt das für uns? Dies bedeutet, dass ein gut optimierender Compiler diese Tatsache nutzen muss und wird. Um den obigen Zyklus zu implementieren, versucht der Compiler, einen Aufruf durch ein Register in beide Fällen zu verwenden. Wenn dies gelingt, könnte der endgültige Code folgendermaßen aussehen

// Direct call

mov eax, 0x12345678

do-it-many-times
  call eax

// Indirect call

mov eax, dword ptr [0x67890ABC]

do-it-many-times
  call eax

Beachten Sie, dass jetzt der wichtige Teil - der tatsächliche Aufruf im Zykluskörper - in beiden Fällen exakt und genau derselbe ist. Es ist unnötig zu erwähnen, dass die Leistung nahezu identisch ist. 

Man könnte sogar sagen, wie seltsam es auch klingen mag, dass auf dieser Plattform ein direkter Aufruf (ein Aufruf mit einem unmittelbaren Operanden in call) langsamer ist als ein indirekter Aufruf, solange der Operand des indirekten Anrufs ist geliefert in einem register (anstatt gespeichert zu werden). 

Natürlich ist das Ganze im Allgemeinen nicht so einfach. Der Compiler muss sich mit der begrenzten Verfügbarkeit von Registern, Aliasing-Problemen usw. auseinandersetzen. Ist dies jedoch so einfach wie in Ihrem Beispiel (und sogar in viel komplizierteren), wird die obige Optimierung von einem guten Compiler ausgeführt und vollständig eliminiert jeder Leistungsunterschied zwischen einem zyklischen Direktanruf und einem zyklischen indirekten Anruf. Diese Optimierung funktioniert besonders gut in C++, wenn eine virtuelle Funktion aufgerufen wird, da die beteiligten Zeiger in einer typischen Implementierung vollständig vom Compiler gesteuert werden, wodurch sie vollständige Kenntnisse des Aliasing-Bildes und anderer relevanter Elemente erhalten.

Natürlich stellt sich immer die Frage, ob Ihr Compiler intelligent genug ist, um solche Dinge zu optimieren ...

80
AnT

Ich denke, wenn die Leute dies sagen, beziehen sie sich auf die Tatsache, dass die Verwendung von Funktionszeigern Compiler-Optimierungen (Inlining) und Prozessor-Optimierungen (Zweigvorhersagen) verhindern kann. Wenn Funktionszeiger jedoch ein effektiver Weg sind, um etwas zu erreichen, das Sie zu erreichen versuchen, besteht die Möglichkeit, dass jede andere Methode dieselben Nachteile hat.

Und wenn Ihre Funktionszeiger nicht in engen Schleifen in einer leistungskritischen Anwendung oder in einem sehr langsamen eingebetteten System verwendet werden, besteht die Gefahr, dass der Unterschied sowieso vernachlässigbar ist.

23
Tyler McHenry

Viele Leute haben gute Antworten gegeben, aber ich denke immer noch, dass ein Punkt übersehen wird. Funktionszeiger fügen eine zusätzliche Dereferenzierung hinzu, wodurch sie um einige Zyklen langsamer werden. Diese Zahl kann aufgrund schlechter Verzweigungsvorhersagen (was übrigens fast nichts mit dem Funktionszeiger selbst zu tun hat) zunehmen. Darüber hinaus können über einen Zeiger aufgerufene Funktionen nicht eingebettet werden. Was jedoch fehlt, ist, dass die meisten Funktionszeiger als Optimierung verwenden.

Die häufigste Stelle, an der Sie Funktionszeiger in c/c ++ - APIs finden, sind Callback-Funktionen. Der Grund für viele APIs besteht darin, dass das Schreiben eines Systems, das bei jedem Auftreten eines Ereignisses einen Funktionszeiger aufruft, wesentlich effizienter ist als andere Methoden wie das Weiterleiten von Nachrichten. Persönlich habe ich auch Funktionszeiger als Teil eines komplexeren Eingabeverarbeitungssystems verwendet, bei dem jeder Taste auf der Tastatur ein Funktionszeiger über eine Sprungtabelle zugeordnet ist. Dies erlaubte mir, jegliche Verzweigungen oder Logik aus dem Eingabesystem zu entfernen und lediglich den eingegangenen Tastendruck zu handhaben.

9
Beanz

Und alle sagten, dass mein Programm langsam laufen wird. Ist es wahr?

Diese Behauptung ist höchstwahrscheinlich falsch. Zum einen, wenn die Alternative zur Verwendung von Funktionszeigern so ähnlich ist

if (condition1) {
        func1();
} else if (condition2)
        func2();
} else if (condition3)
        func3();
} else {
        func4();
}

dies ist höchstwahrscheinlich relativ viel langsamer als die Verwendung eines einzelnen Funktionszeigers. Während der Aufruf einer Funktion über einen Zeiger einen gewissen (normalerweise vernachlässigbaren) Overhead hat, ist es normalerweise nicht der Direktfunktionsaufruf im Vergleich zu der Durchgangszeigeraufrufdifferenz, die für den Vergleich relevant ist.

Und zweitens: Optimieren Sie niemals die Leistung ohne Messungen. Zu wissen, wo die Engpässe liegen, ist sehr schwer (lesen unmöglich ) zu kennen, und manchmal kann dies nicht intuitiv sein (beispielsweise haben die Linux-Kernel-Entwickler angefangen, das inline-Schlüsselwort aus den Funktionen zu entfernen, da dies die Performance beeinträchtigt).

9
hlovdal

Das Aufrufen einer Funktion über einen Funktionszeiger ist etwas langsamer als ein statischer Funktionsaufruf, da der frühere Aufruf eine zusätzliche Dereferenzierung für Zeiger enthält. Aber AFAIK ist dieser Unterschied auf den meisten modernen Maschinen vernachlässigbar (außer vielleicht einigen speziellen Plattformen mit sehr begrenzten Ressourcen).

Funktionszeiger werden verwendet, weil sie das Programm wesentlich einfacher, sauberer und einfacher zu warten machen (wenn es richtig verwendet wird). Dies macht den möglichen sehr geringen Geschwindigkeitsunterschied mehr als wett.

8
Péter Török

Die Verwendung eines Funktionszeigers ist langsamer als das Aufrufen einer Funktion, da dies eine weitere Ebene der Umleitung ist. (Der Zeiger muss dereferenziert sein, um die Speicheradresse der Funktion zu erhalten). Es ist zwar langsamer, aber im Vergleich zu allem anderen, was Ihr Programm tun kann (Lesen einer Datei, Schreiben auf die Konsole), ist es vernachlässigbar.

Wenn Sie Funktionszeiger verwenden müssen, verwenden Sie sie, da alles, was dasselbe versucht, aber nicht verwendet wird, langsamer und weniger wartbar ist als Funktionszeiger.

6
Yacoby

Viele gute Punkte in früheren Antworten. 

Schauen Sie sich jedoch die C qsort-Vergleichsfunktion an. Da die Vergleichsfunktion nicht eingebettet werden kann und standardmäßigen stapelbasierten Aufrufkonventionen folgen muss, kann die Gesamtlaufzeit für die Sortierung um Größenordnung (genauer 3-10x) für Ganzzahlschlüssel langsamer sein als ansonsten derselbe Code mit einem direkten, nichtlinearen Anruf.

Ein typischer Inline-Vergleich wäre eine Folge von einfachen CMP- und möglicherweise CMOV/SET-Befehlen. Ein Funktionsaufruf verursacht auch den Aufwand eines CALL, das Einrichten eines Stapelrahmens, einen Vergleich, das Abreißen des Stapelrahmens und die Rückgabe des Ergebnisses. Beachten Sie, dass die Stack-Operationen aufgrund der Länge der CPU-Pipeline und der virtuellen Register zum Stoppen der Pipeline führen können. Zum Beispiel, wenn der Wert eax benötigt wird, bevor der Befehl, der zuletzt geändert wurde, die Ausführung abgeschlossen hat (was bei den neuesten Prozessoren normalerweise etwa 12 Taktzyklen dauert). Wenn die CPU nicht in der Lage ist, andere Anweisungen auszuführen, um darauf zu warten, wird die Pipeline angehalten.

6
user267027

Möglicherweise.

Die Antwort hängt davon ab, wofür der Funktionszeiger verwendet wird und welche Alternativen es gibt. Das Vergleichen von Funktionszeigeraufrufen mit direkten Funktionsaufrufen ist irreführend, wenn ein Funktionszeiger verwendet wird, um eine Auswahl zu implementieren, die Teil unserer Programmlogik ist und nicht einfach entfernt werden kann. Ich werde weitermachen und trotzdem diesen Vergleich zeigen und später auf diesen Gedanken zurückkommen.

Funktionszeigeraufrufe haben die beste Möglichkeit, die Leistung im Vergleich zu direkten Funktionsaufrufen zu beeinträchtigen, wenn sie das Inlining verhindern. Da das Inlining eine Gateway-Optimierung ist, können wir wilde pathologische Fälle herstellen, in denen Funktionszeiger willkürlich langsamer gemacht werden als der entsprechende direkte Funktionsaufruf:

void foo(int* x) {
    *x = 0;
}

void (*foo_ptr)(int*) = foo;

int call_foo(int *p, int size) {
    int r = 0;
    for (int i = 0; i != size; ++i)
        r += p[i];
    foo(&r);
    return r;
}

int call_foo_ptr(int *p, int size) {
    int r = 0;
    for (int i = 0; i != size; ++i)
        r += p[i];
    foo_ptr(&r);
    return r;
}

Code generiert für call_foo():

call_foo(int*, int):
  xor eax, eax
  ret

Nett. foo() wurde nicht nur inline dargestellt, sondern hat es dem Compiler ermöglicht, die gesamte vorangehende Schleife zu löschen! Der erzeugte Code löscht das Rückgaberegister einfach durch XOR-Verknüpfung des Registers mit sich selbst und kehrt dann zurück. Auf der anderen Seite müssen Compiler Code für die Schleife in call_foo_ptr() (100 Zeilen mit gcc 7.3) generieren, und der größte Teil dieses Codes bewirkt praktisch nichts (solange foo_ptr noch auf foo() zeigt). (In eher typischen Szenarien können Sie davon ausgehen, dass das Inlining einer kleinen Funktion in eine heiße innere Schleife die Ausführungszeit um etwa eine Größenordnung reduzieren kann.)

Im schlimmsten Fall ist ein Funktionszeigeraufruf willkürlich langsamer als ein direkter Funktionsaufruf, was jedoch irreführend ist. Es stellte sich heraus, dass, wenn foo_ptrconst gewesen wäre, call_foo() und call_foo_ptr() denselben Code generiert hätten. Dies würde jedoch erfordern, dass wir die von foo_ptr bereitgestellte Indirektionsmöglichkeit aufgeben. Ist es "fair" für foo_ptr, const zu sein? Wenn wir an der von foo_ptr bereitgestellten Indirektion interessiert sind, nein, aber wenn dies der Fall ist, ist ein direkter Funktionsaufruf ebenfalls keine gültige Option.

Wenn ein Funktionszeiger verwendet wird, um eine nützliche Indirektion bereitzustellen, können wir die Indirektion verschieben oder in einigen Fällen Funktionszeiger für Bedingungen oder sogar Makros austauschen, aber wir können sie nicht einfach entfernen. Wenn wir entschieden haben, dass Funktionszeiger ein guter Ansatz sind, aber die Leistung ein Problem ist, möchten wir normalerweise die Indirektion in den Aufrufstapel ziehen, sodass wir die Kosten für die Indirektion in einer äußeren Schleife bezahlen. In dem üblichen Fall, in dem eine Funktion einen Rückruf annimmt und in einer Schleife aufruft, versuchen wir beispielsweise, die innerste Schleife in den Rückruf zu verschieben (und die Verantwortlichkeit jedes Aufrufs des Rückrufs entsprechend zu ändern).

0
Praxeolitic