2010-05-21 3 views
6

(Ja, ich weiß, dass ein Maschinenbefehl normalerweise keine Rolle spielt. Ich stelle diese Frage, weil ich das Pimpl-Idiom verstehen und es auf die bestmögliche Weise verwenden möchte, und weil mir manchmal etwas wichtig ist Maschinenbefehl.)C++ pimpl idiom verschwendet eine Anweisung vs. C-Stil?

In dem folgenden Beispielcode gibt es zwei Klassen, Thing und OtherThing. Benutzer würden "thing.hh" einschließen. Thing verwendet das Pimpl-Idiom, um seine Implementierung zu verbergen. OtherThing verwendet einen C-Stil – Nicht-Member-Funktionen, die zurückgeben und Zeiger nehmen. Dieser Stil erzeugt einen etwas besseren Maschinencode. Ich bin Fragen: Gibt es eine Möglichkeit, C++ - Stil verwenden – dh, machen Sie die Funktionen in Elementfunktionen – und noch immer die Maschinenanweisung speichern. Ich mag diesen Stil, weil er den Namensraum außerhalb der Klasse nicht verschmutzt.

Hinweis: Ich sehe nur auf Aufruf von Mitgliedsfunktionen (in diesem Fall calc). Ich betrachte die Objektzuordnung nicht.

Unten sind die Dateien, Befehle und der Maschinencode auf meinem Mac.

thing.hh:

class ThingImpl; 
class Thing 
{ 
    ThingImpl *impl; 
public: 
    Thing(); 
    int calc(); 
}; 

class OtherThing;  
OtherThing *make_other(); 
int calc(OtherThing *); 

thing.cc:

#include "thing.hh" 

struct ThingImpl 
{ 
    int x; 
}; 

Thing::Thing() 
{ 
    impl = new ThingImpl; 
    impl->x = 5; 
} 

int Thing::calc() 
{ 
    return impl->x + 1; 
} 

struct OtherThing 
{ 
    int x; 
}; 

OtherThing *make_other() 
{ 
    OtherThing *t = new OtherThing; 
    t->x = 5; 
} 

int calc(OtherThing *t) 
{ 
    return t->x + 1; 
} 

main.cc (nur den Code zu testen, tatsächlich funktioniert ...)

#include "thing.hh" 
#include <cstdio> 

int main() 
{ 
    Thing *t = new Thing; 
    printf("calc: %d\n", t->calc()); 

    OtherThing *t2 = make_other(); 
    printf("calc: %d\n", calc(t2)); 
} 

Makefile:

all: main 

thing.o : thing.cc thing.hh 
    g++ -fomit-frame-pointer -O2 -c thing.cc 

main.o : main.cc thing.hh 
    g++ -fomit-frame-pointer -O2 -c main.cc 

main: main.o thing.o 
    g++ -O2 -o [email protected] $^ 

clean: 
    rm *.o 
    rm main 

Führen Sie make aus und sehen Sie sich den Maschinencode an. Auf dem Mac verwende ich otool -tv thing.o | c++filt. Auf Linux denke ich, es ist objdump -d thing.o. Hier ist der relevante Ausgang:

Thing :: Calc():
0000000000000000 movq (% rdi)% rax
0000000000000003 movl (% rax),% eax
0000000000000005 inkl% eax
0000000000000007 ret
calc (OtherThing *):
0000000000000010 movl (% RDI),% eax
0000000000000012 inkl% eax
0000000000000014 ret

Beachten Sie die zusätzliche Anweisung wegen der Zeigerindirektion. Die erste Funktion sucht zwei Felder (impl, dann x), während die zweite nur x erhalten muss. Was kann getan werden?

+0

Laufen Sie mit voller Optimierung? –

+1

@the_drow: Schauen Sie sich das Makefile an. Und nein, ist er nicht. @Rob: Versuche mit '-O3' zu kompilieren ... irgendeinen Grund, warum du * nicht * die volle Optimierung benutzt? –

+1

Sie * müssen * den Zeiger irgendwann dereferenzieren. Es kommt nicht herum. –

Antwort

5

Nicht zu schwer, verwenden Sie einfach die gleiche Technik in Ihrer Klasse. Jeder halbwegs ordentliche Optimierer wird den trivialen Wrapper inline .

class ThingImpl; 
class Thing 
{ 
    ThingImpl *impl; 
    static int calc(ThingImpl*); 
public: 
    Thing(); 
    int calc() { calc(impl); } 
}; 
+0

Das vermeidet die doppelte Indirektion nicht: 'this-> impl-> x'. –

+0

@Marcelo: Es vermeidet es nicht, aber es sollte dazu führen, dass die generierte Assembly mit seiner 'calc (OtherThing *)' Routine übereinstimmt, also ist die Indirektion "versteckt". Es gibt natürlich keinen Grund, die Klassendefinition mit einer Reihe von statischen Wrapperfunktionen zu überladen, da diese nur in der Implementierungsdatei versteckt werden können. –

+0

+1, 'this-> impl 'indirect sollte grundsätzlich in Ihrem Code verschwinden. Sie müssen dies jedoch nicht wirklich tun, aktivieren Sie nur die Generierung des Link-Time-Codes. – avakar

6

Eine Anweisung ist selten etwas, worüber man sich viel Zeit nimmt.Erstens kann der Compiler den pImpl in einem komplexeren Anwendungsfall zwischenspeichern, wodurch die Kosten in einem realen Szenario amortisiert werden. Zweitens machen Pipeline-Architekturen es nahezu unmöglich, die tatsächlichen Kosten in Taktzyklen vorherzusagen. Sie erhalten eine viel realistischere Vorstellung von den Kosten, wenn Sie diese Operationen in einer Schleife ausführen und den Unterschied zeitlich festlegen.

+1

Ich bin mir nicht sicher, was die beste Funktion für das Profiling ist. Aber ich # und genannt Uhr() und es scheint keinen Unterschied in den Laufzeiten. –

+2

Einige sorgfältige Codierung und sehr lange Schleifen können einige Diskrepanz zeigen, aber Ihr Ergebnis überrascht mich nicht. –

2

Es gibt die böse Art, die den Zeiger auf ThingImpl mit einem groß genug Array von unsignierten Zeichen ersetzen und dann Platzierung/Neu interpretieren/explizit Zerstören der ThingImpl Objekt.

Oder Sie könnten nur die Thing um nach Wert übergeben, da er nicht größer als der Zeiger auf die ThingImpl sein soll, wenn auch ein wenig mehr erfordern als die (Referenzzählung der ThingImpl würde die Optimierung besiegen, so müssen Sie eine Möglichkeit, das "Owning".zu kennzeichnen, was auf einigen Architekturen zusätzlichen Platz erfordern könnte.

+0

Ich hatte von dieser hässlichen, reinterpret_cast-Technik gehört. Ich habe es gerade ausprobiert und es entfernt die zusätzliche Anweisung. Aber die zweite Sache, die Sie gesagt haben - an Wert vorbei - scheint nicht zu helfen. Das Aufrufen von t.calc() auf einem Stack zugeordneten Ding ruft die gleiche Funktion mit dem zusätzlichen Befehl auf. –

+0

Sie können die intrusive Referenzzählung auf ThingImpl verwenden, da sie ohnehin privat ist. Das wird wahrscheinlich die Optimierung nicht zunichtemachen, da Sie den Referenzzähler in eine (öffentliche Basisklasse) "ThingImpl", nicht "Thing" – MSalters

+0

@MSalters setzen, wie ich es vermutete, da Sie die Anzahl jedes Mal erhöhen müssten, wenn Sie bestanden haben Die Sache mit einer Funktion und das Inkrementieren der Zählung wird wahrscheinlich mehr Operationen erfordern, als Sie speichern, indem Sie keine zusätzliche Zeigerumleitung haben. –

2

Ich stimme nicht zu Ihrer Verwendung: Sie vergleichen nicht die 2 gleichen Dinge.

#include "thing.hh" 
#include <cstdio> 

int main() 
{ 
    Thing *t = new Thing;    // 1 
    printf("calc: %d\n", t->calc()); 

    OtherThing *t2 = make_other();  // 2 
    printf("calc: %d\n", calc(t2)); 
} 
  1. Sie haben in der Tat zwei Anrufe neu hier, ist eine ausdrückliche und der andere ist implizit (done durch den Konstruktor Thing.
  2. Sie haben 1 neu hier, implizite (innen 2)

Sie sollen Thing auf dem Stapel zuweisen, obwohl es nicht wahrscheinlich die doppelte Dereferenzierung Anweisung ändern würde ... aber könnten seine Kosten ändern (eine Cache-Miss entfernen).

Aber der Hauptpunkt ist, dass Thing seinen Speicher verwaltet, so dass Sie nicht vergessen können, den tatsächlichen Speicher zu löschen, während Sie definitiv mit der C-Stil-Methode können.

Ich würde argumentieren, dass die automatische Speicherbehandlung eine extra Speicheranweisung wert ist, speziell weil, wie gesagt wurde, der dereferenzierte Wert wahrscheinlich zwischengespeichert wird, wenn Sie mehr als einmal darauf zugreifen, also fast nichts.

Korrektheit ist wichtiger als die Leistung.

+0

Sie haben die Zeile in meiner Frage verfehlt, die besagt: "Anmerkung: Ich suche nur nach Memberfunktionen (in diesem Fall calc). Ich betrachte die Objektzuordnung nicht." –

+1

Und ich forderte die Notwendigkeit Ihrer Frage heraus. Pimpl bringt sowohl RAII als auch Isolierung, während C-Style nur Isolierung bringt. Daher meine Schlussfolgerung, dass ich bereit bin, bei jedem Funktionsaufruf einen Zyklus in meiner CPU loszulassen (für einen Wert, den jeder gute Compiler cachen würde, wenn er in einer engen Schleife aufgerufen würde) im Austausch für RAII (also gewährleistete Korrektheit). –

1

Lassen Sie den Compiler sich darum sorgen. Es weiß viel mehr darüber, was tatsächlich schneller oder langsamer ist als wir. Besonders in solch einem winzigen Maßstab.

Objekte in Klassen haben weit, viel mehr Vorteile als nur Kapselung. PIMPL ist eine großartige Idee, wenn Sie vergessen haben, das private Schlüsselwort zu verwenden.