webentwicklung-frage-antwort-db.com.de

Virtuelle Funktionen und Leistung - C ++

In meinem Klassendesign verwende ich häufig abstrakte Klassen und virtuelle Funktionen. Ich hatte das Gefühl, dass virtuelle Funktionen die Performance beeinflussen. Ist das wahr? Aber ich denke, dieser Leistungsunterschied ist nicht bemerkbar und es sieht so aus, als würde ich vorzeitig optimieren. Recht?

114
Navaneeth K N

Eine gute Faustregel ist:

Es ist kein Leistungsproblem, bis Sie es beweisen können.

Die Verwendung virtueller Funktionen wirkt sich nur geringfügig auf die Leistung aus, es ist jedoch unwahrscheinlich, dass sich dies auf die Gesamtleistung Ihrer Anwendung auswirkt. Bessere Orte, um nach Leistungsverbesserungen zu suchen, sind Algorithmen und E/A.

Ein ausgezeichneter Artikel, der sich mit virtuellen Funktionen (und mehr) befasst, ist Member Function Pointers und die schnellstmöglichen C++ - Delegaten .

86
Greg Hewgill

Ihre Frage hat mich neugierig gemacht, also habe ich einige Timings für die PowerPC-CPU mit 3 GHz durchgeführt, mit der wir arbeiten. Der von mir durchgeführte Test bestand darin, eine einfache 4D-Vektorklasse mit get/set-Funktionen zu erstellen

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

Dann habe ich drei Arrays eingerichtet, die jeweils 1024 dieser Vektoren enthalten (klein genug, um in L1 zu passen), und eine Schleife ausgeführt, in der sie 1000 Mal miteinander addiert wurden (A.x = B.x + C.x). Ich habe dies mit den als inline, virtual definierten Funktionen und regulären Funktionsaufrufen ausgeführt. Hier sind die Ergebnisse:

  • inline: 8 ms (0,65 ns pro Anruf)
  • direkt: 68 ms (5,53 ns pro Anruf)
  • virtuell: 160 ms (13 ns pro Anruf)

In diesem Fall (wo alles in den Cache passt) waren die virtuellen Funktionsaufrufe also etwa 20x langsamer als die Inline-Aufrufe. Aber was heißt das eigentlich? Jede Fahrt durch die Schleife verursachte genau 3 * 4 * 1024 = 12,288 Funktionsaufrufe (1024 Vektoren mal vier Komponenten mal drei Aufrufe pro Addition), diese Zeiten repräsentieren 1000 * 12,288 = 12,288,000 Funktionsaufrufe. Die virtuelle Schleife dauerte 92 ms länger als die direkte Schleife, sodass der zusätzliche Overhead pro Aufruf 7 Nanosekunden pro Funktion betrug.

Daraus schließe ich: ja , virtuelle Funktionen sind viel langsamer als direkte Funktionen, und nein , es sei denn, Sie planen, sie zehn Millionen Mal pro Sekunde anzurufen, spielt es keine Rolle.

Siehe auch: Vergleich der generierten Assembly

162
Crashworks

Wenn Objective-C (bei dem alle Methoden virtuell sind) die Hauptsprache für das iPhone und freakin 'Java die Hauptsprache für Android ist, ist es meiner Meinung nach ziemlich sicher, virtuelle C++ - Funktionen auf unseren 3 GHz zu verwenden Dual-Core-Türme.

42
Chuck

In sehr leistungskritischen Anwendungen (wie Videospielen) kann ein virtueller Funktionsaufruf zu langsam sein. Bei moderner Hardware ist der Cache-Miss das größte Leistungsproblem. Wenn sich keine Daten im Cache befinden, kann es Hunderte von Zyklen dauern, bis sie verfügbar sind.

Ein normaler Funktionsaufruf kann einen Befehls-Cache-Fehler erzeugen, wenn die CPU den ersten Befehl der neuen Funktion abruft und er nicht im Cache ist.

Ein virtueller Funktionsaufruf muss zuerst den vtable-Zeiger aus dem Objekt laden. Dies kann zu einem Daten-Cache-Fehler führen. Anschließend wird der Funktionszeiger aus der V-Tabelle geladen, was zu einem weiteren Daten-Cache-Fehler führen kann. Dann ruft es die Funktion auf, die wie eine nicht-virtuelle Funktion zu einem Fehlschlagen des Befehls-Cache führen kann.

In vielen Fällen sind zwei zusätzliche Cache-Ausfälle kein Problem, aber in einer engen Schleife mit leistungskritischem Code kann dies die Leistung drastisch verringern.

34
Mark James

Ab Seite 44 von Handbuch "Optimizing Software in C++" von Agner Fog :

Der Zeitaufwand für den Aufruf einer virtuellen Member-Funktion beträgt einige Taktzyklen mehr als für den Aufruf einer nicht virtuellen Member-Funktion, vorausgesetzt, die Funktionsaufruf-Anweisung ruft immer dieselbe Version der virtuellen Funktion auf. Wenn sich die Version ändert, erhalten Sie eine Zeitstrafe von 10 - 30 Taktzyklen. Die Regeln für Vorhersage und falsche Vorhersage von virtuellen Funktionsaufrufen sind die gleichen wie für switch-Anweisungen ...

27
Boojum

absolut. Es war vor langer Zeit ein Problem, als Computer mit 100 Mhz liefen, da jeder Methodenaufruf eine Suche in der vtable erforderte, bevor sie aufgerufen wurde. Aber heute ... auf einer 3 GHz-CPU, die einen Cache der ersten Ebene mit mehr Speicher als mein erster Computer hat? Keineswegs. Das Zuweisen von Hauptspeicher RAM kostet Sie mehr Zeit als wenn alle Ihre Funktionen virtuell wären.

Es ist wie in alten Zeiten, in denen die Leute sagten, die strukturierte Programmierung sei langsam, weil der gesamte Code in Funktionen aufgeteilt war, jede Funktion Stapelzuweisungen und einen Funktionsaufruf erforderte!

Das einzige Mal, dass ich überhaupt daran denken würde, die Auswirkungen einer virtuellen Funktion auf die Leistung in Betracht zu ziehen, wäre, wenn sie sehr häufig in Vorlagencode verwendet und instanziiert würde, der in allem auftauchte. Selbst dann würde ich nicht allzu viel Mühe darauf verwenden!

PS: Denken Sie an andere benutzerfreundliche Sprachen. Alle Methoden sind virtuell und kriechen heutzutage nicht mehr.

7
gbjbaanb

Neben der Ausführungszeit gibt es noch andere Leistungskriterien. Eine Vtable belegt ebenfalls Speicherplatz und kann in einigen Fällen vermieden werden: ATL verwendet die Kompilierungszeit " simulierte dynamische Bindung " mit Vorlagen , um den Effekt "statisch" zu erhalten Polymorphismus ", was schwer zu erklären ist; Sie übergeben die abgeleitete Klasse im Grunde genommen als Parameter an eine Basisklassenvorlage, sodass die Basisklasse beim Kompilieren "weiß", was ihre abgeleitete Klasse jeweils ist. Sie können nicht mehrere verschiedene abgeleitete Klassen in einer Sammlung von Basistypen speichern (das ist Laufzeit-Polymorphismus), aber aus statischer Sicht, wenn Sie eine Klasse Y erstellen möchten, die mit einer bereits vorhandenen Schablonenklasse X mit der übereinstimmt Haken für diese Art des Überschreibens, müssen Sie nur die Methoden überschreiben, die Sie interessieren, und dann erhalten Sie die Basismethoden der Klasse X, ohne eine vtable haben zu müssen.

In Klassen mit großem Speicherbedarf sind die Kosten für einen einzelnen vtable-Zeiger nicht viel, aber einige der ATL-Klassen in COM sind sehr klein, und es lohnt sich die Einsparung von vtable, wenn der Laufzeit-Polymorphismus-Fall niemals auftreten wird.

Siehe auch diese andere SO Frage .

Übrigens ist hier ein Beitrag, den ich gefunden habe , der die Aspekte der CPU-Zeit-Leistung behandelt.

6
Jason S

Ja, Sie haben Recht und wenn Sie neugierig auf die Kosten eines virtuellen Funktionsaufrufs sind, finden Sie diesen Beitrag interessant.

4
Serge

Die einzige Möglichkeit, mit der ich feststellen kann, dass eine virtuelle Funktion zu einem Leistungsproblem wird, besteht darin, dass viele virtuelle Funktionen in einer engen Schleife aufgerufen werden und wenn und nur wenn einen Seitenfehler oder einen anderen "schweren" Fehler verursachen msgstr "Speicheroperation soll stattfinden.

Obwohl, wie andere Leute gesagt haben, wird es für Sie im wirklichen Leben so gut wie nie ein Problem sein. Führen Sie einen Profiler aus, führen Sie einige Tests durch und überprüfen Sie, ob dies wirklich ein Problem ist, bevor Sie versuchen, den Code für einen Leistungsvorteil zu "dekonstruieren".

3
Daemin

Wenn die Klassenmethode nicht virtuell ist, führt der Compiler normalerweise In-Lining durch. Wenn Sie dagegen einen Zeiger auf eine Klasse mit virtueller Funktion verwenden, ist die reale Adresse nur zur Laufzeit bekannt.

Dies wird durch Test, Zeitunterschied ~ 700% (!) Gut veranschaulicht:

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

Die Auswirkungen eines virtuellen Funktionsaufrufs hängen stark von der jeweiligen Situation ab. Wenn es nur wenige Anrufe und viel Arbeit in der Funktion gibt, kann dies vernachlässigbar sein.

Oder wenn es sich um einen virtuellen Anruf handelt, der mehrfach verwendet wird, während eine einfache Operation ausgeführt wird, kann er sehr groß sein.

3
Evgueny Sedov

Ich bin mindestens 20 Mal bei meinem Projekt hin und her gegangen. Obwohl es einige große Vorteile in Bezug auf die Wiederverwendung von Code, die Klarheit, die Wartbarkeit und die Lesbarkeit geben kann , bleibt die Leistung dennoch erhalten existieren mit virtuellen Funktionen.

Ist der Performance-Hit auf einem modernen Laptop/Desktop/Tablet spürbar ... wahrscheinlich nicht! In bestimmten Fällen mit eingebetteten Systemen kann der Leistungseinbruch jedoch der treibende Faktor für die Ineffizienz Ihres Codes sein, insbesondere wenn die virtuelle Funktion in einer Schleife immer wieder aufgerufen wird.

Hier ist ein etwas älteres Papier, das Best Practices für C/C++ im Kontext eingebetteter Systeme behandelt: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf =

Fazit: Es ist Sache des Programmierers, die Vor- und Nachteile der Verwendung eines bestimmten Konstrukts gegenüber einem anderen zu verstehen. Wenn Sie nicht besonders leistungsorientiert sind, ist Ihnen der Leistungseinbruch wahrscheinlich egal und Sie sollten alle nützlichen OO -Stoffe in C++ verwenden, um Ihren Code so benutzerfreundlich wie möglich zu gestalten.

2
It'sPete

Nach meiner Erfahrung ist das wichtigste die Fähigkeit, eine Funktion zu integrieren. Wenn Sie Leistungs-/Optimierungsanforderungen haben, die vorschreiben, dass eine Funktion eingebunden werden muss, können Sie die Funktion nicht virtuell machen, da dies dies verhindern würde. Andernfalls werden Sie den Unterschied wahrscheinlich nicht bemerken.

2
Hurkyl

Eine Sache zu beachten ist, dass dies:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

kann schneller sein als das:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

Dies liegt daran, dass die erste Methode nur eine Funktion aufruft, während die zweite möglicherweise viele verschiedene Funktionen aufruft. Dies gilt für jede virtuelle Funktion in jeder Sprache.

Ich sage "kann", weil dies vom Compiler, dem Cache usw. abhängt.

1
nikdeapen

Der Leistungsverlust durch die Verwendung virtueller Funktionen kann die Vorteile, die Sie auf Designebene erzielen, niemals übersteigen. Angeblich wäre ein Aufruf einer virtuellen Funktion 25% weniger effizient als ein direkter Aufruf einer statischen Funktion. Dies liegt daran, dass das VMT eine gewisse Indirektion aufweist. Die für den Aufruf benötigte Zeit ist jedoch normalerweise sehr kurz im Vergleich zu der Zeit, die für die tatsächliche Ausführung Ihrer Funktion benötigt wird, sodass die Gesamtleistungskosten insbesondere bei der aktuellen Leistung der Hardware vernachlässigbar sind. Außerdem kann der Compiler manchmal optimieren und feststellen, dass kein virtueller Aufruf erforderlich ist, und diesen in einen statischen Aufruf kompilieren. Machen Sie sich also keine Sorgen, verwenden Sie virtuelle Funktionen und abstrakte Klassen so oft Sie möchten.

0
Koen