Eines der Probleme von pimpl ist die Leistungsbeeinträchtigung bei der Verwendung (zusätzliche Speicherzuweisung, nicht zusammenhängende Datenelemente, zusätzliche Indirektionen usw.). Ich möchte eine Variation der Pimpl-Sprache vorschlagen, mit der diese Leistungseinbußen vermieden werden, wenn nicht alle Vorteile von Pimpl genutzt werden. Die Idee ist, alle privaten Datenelemente in der Klasse selbst zu belassen und nur die privaten Methoden in die pimpl-Klasse zu verschieben. Der Vorteil gegenüber Pimpl besteht darin, dass der Speicher zusammenhängend bleibt (keine zusätzliche Indirektion). Die Vorteile gegenüber der Nichtverwendung von Pimpl sind:
- Es verbirgt die privaten Funktionen.
- Sie können es so strukturieren, dass alle diese Funktionen eine interne Verknüpfung haben und der Compiler es aggressiver optimieren kann.
Meine Idee ist es also, den Pickel von der Klasse selbst erben zu lassen (klingt ein bisschen verrückt, ich weiß, aber ertrage es mit mir). Es würde ungefähr so aussehen:
In der Ah-Datei:
class A
{
A();
void DoSomething();
protected: //All private stuff have to be protected now
int mData1;
int mData2;
//Not even a mention of a PImpl in the header file :)
};
In der A.cpp-Datei:
#define PCALL (static_cast<PImpl*>(this))
namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
static_assert(sizeof(PImpl) == sizeof(A),
"Adding data members to PImpl - not allowed!");
void DoSomething1();
void DoSomething2();
//No data members, just functions!
};
void PImpl::DoSomething1()
{
mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
DoSomething2();
}
void PImpl::DoSomething2()
{
mData2 = baz();
}
}
A::A(){}
void A::DoSomething()
{
mData2 = foo();
PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}
Soweit ich sehe, gibt es absolut keine Leistungseinbußen bei der Verwendung von vs no pimpl und einige mögliche Leistungssteigerungen und eine sauberere Header-Dateischnittstelle. Ein Nachteil gegenüber Standard-Pimpl ist, dass Sie die Datenelemente nicht ausblenden können, sodass Änderungen an diesen Datenelementen immer noch eine Neukompilierung von allem auslösen, was von der Header-Datei abhängt. Aber so wie ich es sehe, ist es entweder der Vorteil oder der Leistungsvorteil, wenn die Mitglieder zusammenhängend im Speicher sind (oder machen Sie diesen Hack- "Warum Versuch Nr. 3 bedauerlich ist"). Eine weitere Einschränkung ist, dass wenn A eine Vorlagenklasse ist, die Syntax ärgerlich wird (Sie wissen, Sie können mData1 nicht direkt verwenden, Sie müssen dies tun-> mData1, und Sie müssen den Typnamen und möglicherweise Vorlagenschlüsselwörter für abhängige Typen verwenden und Schablonentypen usw.). Eine weitere Einschränkung ist, dass Sie in der ursprünglichen Klasse nicht mehr private, sondern nur geschützte Mitglieder verwenden können, sodass Sie den Zugriff von keiner ererbenden Klasse, nicht nur von der Zuhälterin, einschränken können. Ich habe es versucht, konnte dieses Problem aber nicht umgehen. Ich habe beispielsweise versucht, den Pimpl zu einer Friend-Template-Klasse zu machen, in der Hoffnung, die Friend-Deklaration so breit zu gestalten, dass ich die eigentliche Pimpl-Klasse in einem anonymen Namespace definieren kann, aber das funktioniert einfach nicht. Wenn jemand eine Idee hat, wie er die Datenelemente privat halten und dennoch einer in einem anonymen Namensraum definierten ererbenden Pimpl-Klasse den Zugriff auf diese erlauben kann, würde ich es wirklich gerne sehen! Das würde meine Hauptreservierung davon abhalten, dies zu verwenden.
Ich bin jedoch der Meinung, dass diese Vorbehalte für die Vorteile meines Vorschlags akzeptabel sind.
Ich habe versucht, online nach Hinweisen auf diese "Nur-Funktion-Zuhälter" -Sprache zu suchen, konnte aber nichts finden. Ich bin wirklich interessiert daran, was die Leute darüber denken. Gibt es andere Probleme oder Gründe, warum ich das nicht verwenden sollte?
AKTUALISIEREN:
Ich habe diesen Vorschlag gefunden , der mehr oder weniger versucht, genau das zu erreichen, was ich bin, aber dies durch Ändern des Standards. Ich stimme diesem Vorschlag voll und ganz zu und hoffe, dass er in den Standard aufgenommen wird (ich weiß nichts über diesen Prozess, daher habe ich keine Ahnung, wie wahrscheinlich dies sein wird). Ich hätte es viel lieber, wenn dies über einen eingebauten Sprachmechanismus möglich wäre. Der Vorschlag erklärt auch die Vorteile dessen, was ich viel besser erreichen möchte als ich. Es hat auch nicht das Problem, die Kapselung zu brechen, wie es mein Vorschlag getan hat (privat -> geschützt). Bis dieser Vorschlag es in den Standard schafft (falls dies jemals passiert), denke ich, dass mein Vorschlag es ermöglicht, diese Vorteile mit den von mir aufgeführten Vorbehalten zu erzielen.
UPDATE2:
In einer der Antworten wird LTO als mögliche Alternative erwähnt, um einige der Vorteile zu nutzen (ich vermute, aggressivere Optimierungen). Ich bin mir nicht sicher, was genau in verschiedenen Compiler-Optimierungsdurchläufen vor sich geht, aber ich habe ein wenig Erfahrung mit dem resultierenden Code (ich verwende gcc). Durch einfaches Einfügen der privaten Methoden in die ursprüngliche Klasse werden diese zu einer externen Verknüpfung gezwungen.
Ich könnte mich hier irren, aber die Art und Weise, wie ich das interpretiere, ist, dass der Optimierer zur Kompilierungszeit die Funktion nicht eliminieren kann, selbst wenn alle seine Aufrufinstanzen vollständig in dieser TU eingebettet sind. Aus irgendeinem Grund weigert sich sogar LTO, die Funktionsdefinition loszuwerden, selbst wenn es den Anschein hat, dass alle Aufrufinstanzen in der gesamten verknüpften Binärdatei inline sind. Ich habe einige Referenzen gefunden, die besagen, dass der Linker nicht weiß, ob Sie die Funktion irgendwie immer noch mithilfe von Funktionszeigern aufrufen (obwohl ich nicht verstehe, warum der Linker nicht herausfinden kann, dass die Adresse dieser Methode niemals verwendet wird ).
Dies ist nicht der Fall, wenn Sie meinen Vorschlag verwenden und diese privaten Methoden in einem Pickel in einem anonymen Namespace ablegen. Wenn diese inline werden, werden die Funktionen NICHT in der Objektdatei angezeigt (mit -O3, einschließlich -finline-Funktionen).
So wie ich es verstehe, berücksichtigt der Optimierer bei der Entscheidung, ob eine Funktion eingebunden werden soll oder nicht, ihre Auswirkungen auf die Codegröße. Mit meinem Vorschlag mache ich es für den Optimierer etwas "billiger", diese privaten Methoden zu integrieren.
PCALL
ist undefiniertes Verhalten. Sie können einA
in aPImpl
umwandeln und verwenden, es sei denn, das zugrunde liegende Objekt ist tatsächlich vom TypPImpl
. Sofern ich mich nicht irre, erstellen Benutzer nur Objekte vom TypA
.Antworten:
Die Verkaufsargumente des Pimpl-Musters sind:
Zu diesem Zweck besteht der klassische Pimpl aus drei Teilen:
Eine Schnittstelle für das Implementierungsobjekt, die öffentlich sein muss und virtuelle Methoden für die Schnittstelle verwendet:
Diese Schnittstelle muss stabil sein.
Ein Schnittstellenobjekt, das die private Implementierung vertritt. Es müssen keine virtuellen Methoden verwendet werden. Das einzige zulässige Mitglied ist ein Zeiger auf die Implementierung:
Die Header-Datei dieser Klasse muss stabil sein.
Mindestens eine Implementierung
Der Pimpl verschafft uns dann viel Stabilität für eine Bibliotheksklasse auf Kosten einer Heap-Zuweisung und eines zusätzlichen virtuellen Versands.
Wie misst sich Ihre Lösung?
Für jedes Ziel des Pimpl-Musters erfüllen Sie dieses Ziel nicht. Es ist daher nicht sinnvoll, Ihr Muster als Variation des Pimpl zu bezeichnen, es ist viel mehr eine gewöhnliche Klasse. Eigentlich ist es schlimmer als eine gewöhnliche Klasse, weil Ihre Mitgliedsvariablen privat sind. Und wegen dieser Besetzung, die ein eklatanter Punkt der Zerbrechlichkeit ist.
Beachten Sie, dass das Pimpl-Muster nicht immer optimal ist - es gibt einen Kompromiss zwischen Stabilität und Polymorphismus einerseits und Speicherkompaktheit andererseits. Es ist semantisch unmöglich, dass eine Sprache beides hat (ohne JIT-Kompilierung). Wenn Sie also die Speicherkompaktheit mikrooptimieren, ist der Pimpl eindeutig keine geeignete Lösung für Ihren Anwendungsfall. Sie werden wahrscheinlich auch die Hälfte der Standardbibliothek nicht mehr verwenden, da diese schrecklichen Zeichenfolgen- und Vektorklassen dynamische Speicherzuweisungen beinhalten ;-)
quelle
Für mich überwiegen die Vorteile nicht die Nachteile.
Vorteile :
Dies kann die Kompilierung beschleunigen, da eine Neuerstellung gespeichert wird, wenn nur die Signaturen der privaten Methode geändert wurden. Eine Neuerstellung ist jedoch erforderlich, wenn sich öffentliche oder geschützte Methodensignaturen oder private Datenelemente geändert haben, und es ist selten, dass ich private Methodensignaturen ändern muss, ohne eine dieser anderen Optionen zu berühren.
Es kann aggressivere Compiler-Optimierungen zulassen, aber LTO sollte viele der gleichen Optimierungen zulassen (zumindest denke ich, dass dies möglich ist - ich bin kein Guru für Compiler-Optimierung), sowie einige weitere und kann standardmäßig und automatisch erstellt werden.
Nachteile:
Sie haben einige Nachteile erwähnt: die Unfähigkeit, private Vorlagen zu verwenden, und die Komplexität mit Vorlagen. Für mich ist der größte Nachteil jedoch, dass es einfach umständlich ist: ein unkonventioneller Programmierstil mit nicht ganz standardmäßigen Pimpl-Stil-Sprüngen zwischen Schnittstelle und Implementierung, der zukünftigen Betreuern oder neuen Teammitgliedern unbekannt sein wird und möglicherweise auch nicht von Werkzeugen schlecht unterstützt werden (siehe z. B. diesen GDB-Fehler ).
Hier gelten die Standardbedenken hinsichtlich der Optimierung: Haben Sie gemessen, dass die Optimierungen Ihre Leistung erheblich verbessern? Wäre Ihre Leistung dadurch besser oder würden Sie sich die Zeit nehmen, um dies aufrechtzuerhalten und in die Profilerstellung von Hotspots, die Verbesserung von Algorithmen usw. zu investieren? Persönlich würde ich lieber einen klaren und unkomplizierten Programmierstil wählen, unter der Annahme, dass dadurch Zeit für gezielte Optimierungen frei wird. Aber das ist meine Perspektive für die Arten von Code, an denen ich arbeite - für Ihre Problemdomäne können die Kompromisse unterschiedlich sein.
Randnotiz: Zulassen von privat mit nur Methode pimpl
Sie haben gefragt, wie Sie private Mitglieder mit Ihrem Pimpl-Vorschlag nur für Methoden zulassen können. Leider halte ich Pimpl nur für Methoden für eine Art Hack, aber wenn Sie entschieden haben, dass die Vorteile die Nachteile überwiegen, können Sie den Hack genauso gut annehmen.
Ah:
A.cpp:
quelle
Sie können
std::aligned_storage
den Speicher für Ihren Pimpl in der Schnittstellenklasse deklarieren.In der Implementierung können Sie Ihre Pimpl-Klasse direkt unter folgender Adresse erstellen
_storage
:quelle
Nein, Sie können es nicht ohne Leistungseinbußen implementieren. PIMPL ist von Natur aus eine Leistungsbeeinträchtigung, da Sie eine Laufzeit-Indirektion anwenden.
Dies hängt natürlich genau davon ab, was Sie indirekt wollten. Einige Informationen werden vom Verbraucher einfach nicht verwendet - wie genau das, was Sie in Ihre 64 4-Byte-ausgerichteten Bytes einfügen möchten. Andere Informationen sind jedoch wie die Tatsache, dass Sie 64 4-Byte-ausgerichtete Bytes für Ihr Objekt benötigen.
Generische PIMPLs ohne Leistungseinbußen existieren nicht und werden niemals existieren. Es sind dieselben Informationen, die Sie Ihrem Benutzer verweigern, die er zur Optimierung verwenden möchte. Wenn Sie es ihnen geben, wird Ihre IMPL nicht abstrahiert. Wenn Sie es ihnen verweigern, können sie nicht optimieren. Sie können es nicht in beide Richtungen haben.
quelle
Bei allem Respekt und nicht mit der Absicht, diese Aufregung zu töten, sehe ich keinen praktischen Nutzen, den dies aus Sicht der Kompilierungszeit bringt. Viele der Vorteile von
pimpls
werden sich aus dem Ausblenden benutzerdefinierter Typdetails ergeben. Zum Beispiel:... in einem solchen Fall ergeben sich die höchsten Kosten für die Kompilierung aus der Tatsache, dass
Foo
wir zur Definition die Größen- / Ausrichtungsanforderungen von kennen müssenBar
(was bedeutet, dass wir die Definition von rekursiv benötigen müssenBar
).Wenn Sie die Datenelemente nicht ausblenden, geht einer der wichtigsten Vorteile aus Sicht der Kompilierungszeit verloren. Außerdem gibt es dort möglicherweise gefährlich aussehenden Code, aber der Header wird nicht leichter und die Quelldatei wird mit mehr Weiterleitungsfunktionen schwerer, sodass die Kompilierungszeiten insgesamt wahrscheinlich eher erhöht als verkürzt werden.
Leichtere Header sind der Schlüssel
Um die Erstellungszeiten zu verkürzen, möchten Sie eine Technik zeigen können, die zu einem dramatisch leichteren Header führt (normalerweise, indem Sie
#include
einige andere Header nicht mehr rekursiv ausführen können, weil Sie Details ausgeblendet haben, für die bestimmtestruct/class
Definitionen nicht mehr erforderlich sind ). Hierpimpls
kann Original einen signifikanten Effekt haben, indem es kaskadierende Ketten von Header-Einschlüssen abbricht und weitaus unabhängigere Header ergibt, bei denen alle privaten Details verborgen sind.Sicherere Wege
Wenn Sie sowieso so etwas tun möchten, ist es auch viel einfacher, eine
friend
in Ihrer Quelldatei definierte Klasse zu verwenden, als eine, die dieselbe Klasse erbt, die Sie nicht mit Zeiger-Casting-Tricks zum Aufrufen von Methoden instanziieren ein nicht instanziiertes Objekt oder verwenden Sie einfach freistehende Funktionen mit interner Verknüpfung innerhalb der Quelldatei, die die entsprechenden Parameter erhalten, um die erforderliche Arbeit zu erledigen (beides könnte es Ihnen zumindest ermöglichen, einige private Methoden aus dem Header auszublenden, um sehr geringfügige Einsparungen zu erzielen in Kompilierungszeiten und ein wenig Spielraum, um eine kaskadierende Neukompilierung zu vermeiden).Feste Allokator
Wenn Sie die billigste Art von Zuhälter wollen, besteht der Haupttrick darin, einen festen Allokator zu verwenden. Insbesondere beim Aggregieren von Zuhältern in großen Mengen ist der Verlust der räumlichen Lokalität und die zusätzlichen obligatorischen Seitenfehler beim erstmaligen Zugriff auf die Zuhälter der größte Killer. Durch die Vorbelegung von Speicherpools, die Speicher für zugewiesene Pimpls bündeln und den Speicher an den Pool zurückgeben, anstatt ihn bei der Freigabe freizugeben, werden die Kosten für eine Schiffsladung von Pimpl-Instanzen drastisch gesenkt. Unter Leistungsgesichtspunkten ist es zwar immer noch nicht kostenlos, aber viel billiger und weitaus cache- / seitenfreundlicher.
quelle