Frage

Ich habe die folgende Situation vor:


class A
{
public:
    A(int whichFoo);
    int foo1();
    int foo2();
    int foo3();
    int callFoo(); // cals one of the foo's depending on the value of whichFoo
};

In meiner aktuellen Implementierung speichere ich den Wert von whichFoo in einem Datum Mitglied im Konstruktor und verwenden Sie einen switch in callFoo() zu entscheiden, welche von den foo zu nennen ist. Alternativ kann ich einen switch im Konstruktor verwenden, um einen Zeiger auf der rechten Seite fooN() speichern in callFoo() aufgerufen werden.

Meine Frage ist, welche Art und Weise effizienter ist, wenn ein Objekt der Klasse A nur einmal aufgebaut ist, während callFoo() eine sehr große Anzahl, wie oft aufgerufen wird. So im ersten Fall haben wir mehrere Ausführungen einer switch-Anweisung, während in der zweiten nur ein Schalter, und mehrere Anrufe eines Mitglieds Funktion mit dem Zeiger auf sie ist. Ich weiß, dass eine Memberfunktion aufrufen einen Zeiger mit langsamer ist als nur direkt aufrufen. Weiß jemand, ob dieser Aufwand mehr oder weniger als die Kosten eines switch ist?

Zur Verdeutlichung: Ich weiß, dass Sie nie wirklich wissen, welcher Ansatz eine bessere Leistung gibt, bis Sie es und die Zeit es versuchen. Doch in diesem Fall habe ich bereits Ansatz 1 umgesetzt, und ich wollte herausfinden, ob Ansatz 2 effiziente zumindest im Prinzip sein kann. Es scheint, dass es sein kann, und jetzt macht es Sinn für mich, es zu implementieren, um die Mühe und versuchen Sie es.

Oh, und Ich mag Ansatz auch 2 besser aus ästhetischen Gründen. Ich glaube, ich für eine Rechtfertigung suchen, sie umzusetzen. :)

War es hilfreich?

Lösung

Wie sicher sind, dass Sie über einen Zeiger eine Memberfunktion aufrufen langsamer ist als nur direkt anrufen? Können Sie den Unterschied messen?

In der Regel sollten Sie nicht auf Ihre Intuition verlassen, wenn Performance-Auswertungen machen. Setzen Sie sich mit Ihrem Compiler und eine Timing-Funktion, und tatsächlich Maß die verschiedenen Möglichkeiten. Sie werden überrascht sein!

Weitere Informationen: Es gibt einen ausgezeichneten Artikel Mitglied Funktionszeiger und die schnellstmöglichen C ++ Delegierten , die geht in den sehr tiefen Detail über die Umsetzung der Mitgliedsfunktionszeiger.

Andere Tipps

Sie können schreiben:

class Foo {
public:
  Foo() {
    calls[0] = &Foo::call0;
    calls[1] = &Foo::call1;
    calls[2] = &Foo::call2;
    calls[3] = &Foo::call3;
  }
  void call(int number, int arg) {
    assert(number < 4);
    (this->*(calls[number]))(arg);
  }
  void call0(int arg) {
    cout<<"call0("<<arg<<")\n";
  }
  void call1(int arg) {
    cout<<"call1("<<arg<<")\n";
  }
  void call2(int arg) {
    cout<<"call2("<<arg<<")\n";
  }
  void call3(int arg) {
    cout<<"call3("<<arg<<")\n";
  }
private:
  FooCall calls[4];
};

Die Berechnung der tatsächlichen Funktionszeiger ist linear und schnell:

  (this->*(calls[number]))(arg);
004142E7  mov         esi,esp 
004142E9  mov         eax,dword ptr [arg] 
004142EC  push        eax  
004142ED  mov         edx,dword ptr [number] 
004142F0  mov         eax,dword ptr [this] 
004142F3  mov         ecx,dword ptr [this] 
004142F6  mov         edx,dword ptr [eax+edx*4] 
004142F9  call        edx 

Beachten Sie, dass Sie nicht einmal die eigentliche Funktion Nummer im Konstruktor zu beheben haben.

Ich habe diesen Code auf den von einem switch erzeugt asm verglichen. Die switch Version bietet keine Leistungssteigerung.

, um die gestellte Frage zu beantworten:. Am feinkörnigste Ebene der Zeiger auf die Member-Funktion wird eine bessere Leistung

die unausgesprochene Frage zu lösen: was „besser“ bedeutet hier? In den meisten Fällen würde erwarten, dass ich der Unterschied vernachlässigbar. Je nachdem, was die Klasse, es zu tun, jedoch kann der Unterschied erheblich sein. Performance-Tests vor, über den Unterschied besorgniserregend ist offensichtlich der richtige erste Schritt.

Wenn Sie einen Schalter verwendet, halten werden, die völlig in Ordnung ist, dann sollten Sie vielleicht die Logik in einem Hilfsmethode setzen und rufen, wenn aus dem Konstruktor. Alternativ ist dies ein klassischer Fall von dem Strategy-Muster . Sie könnten eine Schnittstelle (oder eine abstrakte Klasse) mit dem Namen IFoo schaffen, die eine Methode mit Foo Unterschrift hat. Sie würden den Konstruktor nehmen in einer Instanz von IFoo haben (Konstruktor Dependency Injection , die die foo Methode implementiert dass Sie wollen. Sie würden einen privaten IFoo haben, die mit diesem Konstruktor festgelegt werden würde, und jedes Mal wollten Sie Foo nennen würden Sie Ihre IFoo-Version nennen.

. Hinweis: Ich habe nicht mit C ++ seit dem College arbeitete, so dass mein Kauderwelsch off hier sein könnte, ut die allgemeinen Ideen halten für die meisten OO-Sprachen

Wenn Ihr Beispiel echter Code ist, dann denke ich, Sie Ihre Klasse Design überdenken sollten. Passing in einem Wert an den Konstruktor, und dass die Verwendung von Verhalten zu ändern ist wirklich äquivalent eine Unterklasse zu schaffen. Betrachten Sie Refactoring, um es deutlicher. Die Wirkung von so tun, ist, dass Ihr Code einen Funktionszeiger am Ende mit (alle virtuellen Methoden sind wirklich, sind Funktionszeiger in Sprungtabellen).

Wenn aber der Code war nur ein vereinfachtes Beispiel zu fragen, ob in der Regel springen Tabellen sind schneller als switch-Anweisungen, dann würde meine Intuition, dass die Sprungtabellen sagen sind schneller, aber Sie sind abhängig von dem Optimierungsschritt des Compilers. Aber wenn die Leistung ist wirklich so ein Problem ist, verlassen nie auf Intuition -. Klopfen ein Testprogramm und testen, oder schauen Sie auf den erzeugten Assembler

Eines ist sicher, eine switch-Anweisung wird nie als ein Sprungtabelle langsamer. Der Grund dafür ist, dass die besten eines Optimierers des Compilers kann wird tun, um eine Reihe von bedingten Tests drehen (das heißt ein Schalter) in eine Sprungtabelle. Also, wenn Sie wirklich sicher sein wollen, nehmen Sie die Compiler aus dem Entscheidungsprozess und eine Sprungtabelle verwendet werden.

Klingt wie Sie eine rein virtuelle Funktion machen callFoo sollten und einige Unterklassen von A erstellen.

Wenn Sie nicht wirklich brauchen, um die Geschwindigkeit, hat umfangreiche Profilierung und Instrumentierung durchgeführt und festgestellt, dass die Anrufe zu callFoo sind wirklich der Engpass. Haben Sie?

Funktionszeiger sind fast immer besser als verkettete-ifs. Sie machen saubereren Code, und sind fast immer schneller (außer vielleicht in einem Fall, wo seine nur eine Wahl zwischen zwei Funktionen und wird immer korrekt vorhergesagt).

sollte ich denken, dass die Zeiger schneller wäre.

Moderne CPUs Vorabrufbefehle; mis-vorhergesagte Verzweigungen des Cache-Räum, das heißt, sie abgewürgt, während es den Cache wieder auffüllt. Ein Zeiger doens't das tun.

Natürlich sollten Sie beide messen.

Optimieren Sie nur bei Bedarf

Erstens: Die meiste Zeit werden Sie wahrscheinlich nicht kümmern, wird der Unterschied sehr klein sein. Stellen Sie sicher, die Optimierung dieser Anruf wirklich Sinn macht zuerst. Nur wenn Sie Ihre Messungen dort zeigen wirklich viel Zeit im Call-Overhead ausgegeben ist, gehen sie (schamlose Werbung zu optimieren - Vgl Wie eine Anwendung zu optimieren, um es schneller zu machen? ) Wenn die Optimierung nicht signifikant ist, bevorzugt den lesbaren Code.

Indirekte Anrufkosten hängen von der Zielplattform

Sobald Sie festgestellt haben, lohnt es sich, Low-Level-Optimierung anzuwenden, dann ist es eine Zeit, um Ihre Zielplattform zu verstehen. Die Kosten können Sie vermeiden, ist hier die Verzweigungsfehlvorhersagebus Strafe. Auf modernen x86 / x64 CPU ist diese falsche Vorhersage wahrscheinlich sehr klein sein (sie können indirekte Anrufe ziemlich gut vorhersagen der meiste Zeit), aber wenn PowerPC oder andere RISC-Plattformen Targeting, die indirekten Anrufe / Sprünge werden oft überhaupt nicht vorhergesagt und die Vermeidung von sie können erhebliche Leistungssteigerung führen. Siehe auch Virtuelle Anrufkosten ist abhängig von Plattform .

Compiler kann implementieren Schalter Sprungtabelle mit als auch

Ein Gotcha: Switch kann manchmal als ein indirekter Aufruf implementiert werden (unter Verwendung einer Tabelle) und, vor allem, wenn sie zwischen vielen möglichen Werten wechseln. Solche Schalter zeigt die gleiche Fehlvorhersage als virtuelle Funktion. Um diese Optimierung zuverlässig zu machen, würde man wahrscheinlich für den häufigste Fall, wenn statt Schalter bevorzugen.

Verwenden Sie Timer, um zu sehen, welche schneller ist. Obwohl es sei denn, dieser Code wird sein, immer und immer wieder, dann ist es unwahrscheinlich, dass Sie einen Unterschied bemerken werden.

Seien Sie sicher, dass, wenn Sie Code ausführen vom Konstruktor, dass, wenn die Konstruktion nicht, dass Sie nicht ein Speicherleck auf.

Diese Technik ist stark mit Symbian OS verwendet: http://www.titu.jyu.fi/modpa/Patterns/ Muster-TwoPhaseConstruction.html

Wenn Sie nur anrufen callFoo () einmal, als wahrscheinlich die Funktionszeiger wird durch einen geringfügigen Betrag langsamer. Wenn Sie es oft als wahrscheinlich die Funktionszeiger wird schneller durch einen geringfügigen Betrag fordern (weil es nicht geht durch den Schalter zu halten braucht).

So oder so Blick auf dem zusammengebauten Code sicher, um herauszufinden, es zu tun, was Sie denken, es tut.

Ein häufig Vorteil übersehen zu wechseln (auch über Sortierung und Indizierung) ist, wenn Sie wissen, dass ein bestimmte Wert in der überwiegenden Mehrzahl der Fälle verwendet wird. Es ist einfach, den Schalter zu bestellen, so dass die am häufigsten zuerst geprüft werden.

ps. Gregs Antwort zu verstärken, wenn du auf Geschwindigkeit - measure. Hilfe bei der Suche Assembler nicht, wenn CPUs hat Prefetch / predictive Verzweigung und Pipeline-Blockierungen usw.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top