Schließlich schrieb ich einen Beitrag über [[trivial_abi]]!
Dies ist eine neue proprietäre Funktion im Clang-Trunk, die ab Februar 2018 neu ist. Dies ist eine Herstellererweiterung der C ++ - Sprache, es handelt sich nicht um Standard-C ++, es wird vom GCC-Trunk nicht unterstützt, und es gibt meines Wissens keine aktiven Vorschläge von WG21, sie in den C ++ - Standard aufzunehmen.

Ich habe an der Implementierung dieser Funktion nicht teilgenommen. Ich habe mir nur die Patches auf der Mailingliste von cfe-commit angesehen und mir schweigend applaudiert. Aber das ist so eine coole Funktion, dass ich denke, jeder sollte davon wissen.
Als erstes beginnen wir also: Dies ist kein Standardattribut, und der Clang-Trunk unterstützt nicht die Standardschreibweise des Attributs [[trivial_abi]]. Stattdessen sollten Sie es im alten Stil schreiben, wie unten gezeigt:
__attribute__((trivial_abi)) __attribute__((__trivial_abi__)) [[clang::trivial_abi]]
Und da dies ein Attribut ist, ist der Compiler sehr wählerisch, wo Sie es einfügen, und stillschweigend aggressiv, wenn Sie es an der falschen Stelle einfügen (da nicht erkannte Attribute einfach ohne Nachrichten ignoriert werden). Dies ist kein Fehler, dies ist eine Funktion. Die richtige Syntax lautet:
#define TRIVIAL_ABI __attribute__((trivial_abi)) class TRIVIAL_ABI Widget {
Welches Problem löst das?
Erinnerst du dich an meinen Beitrag vom 17.04.2008, in dem ich zwei Versionen der Klasse gezeigt habe?
Hinweis perev: Da der Beitrag vom 17.04.2008 ein kleines Volumen hat, habe ich ihn nicht separat veröffentlicht, sondern direkt hier unter dem Spoiler eingefügt.
Beitrag vom 17.04.2008Nachteile eines fehlenden Trivial Destructor Call
Siehe die C ++ Standard-Angebots-Mailingliste. Welche der beiden Funktionen, foo oder bar, hat den besten vom Compiler generierten Code?
struct Integer { int value; ~Integer() {}
Kompilieren mit GCC und libstdc ++. Ratet mal, richtig?
foo: movq 8(%rdi), %rax imull $-559038737, -4(%rax), %edx subq $4, %rax movl %edx, (%rax) movq %rax, 8(%rdi) ret bar: subq $4, 8(%rdi) ret
Folgendes passiert hier: GCC ist intelligent genug, um zu verstehen, dass beim Starten eines Destruktors für einen Speicherbereich seine Lebensdauer endet und alle vorherigen Einträge in diesen Speicherbereich "tot" sind. GCC ist aber auch klug genug zu verstehen, dass ein trivialer Destruktor (wie der Pseudo-Destruktor ~ int ()) nichts tut und keine Effekte erzeugt.
Die Balkenfunktion ruft also pop_back auf, das ~ Integer () ausführt, wodurch vec.back () tot ist, und GCC entfernt die Multiplikation mit 0xDEADBEEF vollständig.
Auf der anderen Seite ruft foo pop_back auf, wodurch der Pseudo-Destruktor ~ int () gestartet wird (der Aufruf kann vollständig übersprungen werden, aber nicht). GCC erkennt, dass er leer ist, und vergisst ihn. Daher sieht GCC nicht, dass vec.back () tot ist, und entfernt die Multiplikation mit 0xDEADBEEF nicht.
Dies geschieht für einen trivialen Destruktor, jedoch nicht für einen Pseudo-Destruktor wie ~ int (). Ersetzen Sie unsere ~ Integer () {} durch ~ Integer () = default; und sehen Sie, wie die imull Anweisung wieder erschien!
struct Foo { int value; ~Foo() = default;
In diesem Beitrag wird der Code angegeben, in dem der Compiler Code für Foo schlechter als für Bar generiert hat. Es lohnt sich zu diskutieren, warum dies unerwartet war. Programmierer erwarten intuitiv, dass "trivialer" Code besser ist als "nichttrivialer" Code. Dies ist in den meisten Situationen der Fall. Dies ist insbesondere dann der Fall, wenn wir einen Funktionsaufruf durchführen oder zurückkehren:
template<class T> T incr(T obj) { obj.value += 1; return obj; }
incr wird mit folgendem Code
kompiliert :
leal 1(%rdi), %eax retq
(leal ist der x86-
Befehl , der "add" bedeutet.) Wir sehen, dass unser 4-Byte-Objekt im% edi-Register an incr übergeben wird, und wir addieren 1 zu seinem Wert und geben ihn an% eax zurück. Vier Bytes am Eingang, vier Bytes am Ausgang, einfach und unkompliziert.
Schauen wir uns nun incr an (der Fall mit einem nicht trivialen Destruktor).
movl (%rsi), %eax addl $1, %eax movl %eax, (%rsi) movl %eax, (%rdi) movq %rdi, %rax retq
Hier wird obj nicht im Register übergeben, obwohl hier die gleichen 4 Bytes mit der gleichen Semantik sind. Hier wird obj übergeben und an die Adresse zurückgegeben. Hier reserviert der Aufrufer etwas Platz für den Rückgabewert und übergibt uns einen Zeiger auf diesen Platz in rdi, und der Aufrufer gibt uns einen Zeiger für den Rückgabewert obj im nächsten Argumentregister% rsi. Wir extrahieren den Wert aus (% rsi), addieren 1, speichern ihn zurück in (% rsi), um den Wert von obj selbst zu aktualisieren, und kopieren dann (trivial) 4 Bytes von obj in den Steckplatz für den Rückgabewert, auf den% rdi zeigt. Schließlich kopieren wir den vom Aufrufer übergebenen ursprünglichen Zeiger von% rdi nach% rax, da das
x86-64-ABI- Dokument (S. 22) uns dazu auffordert.
Der Grund, warum Bar so anders ist als Foo, ist, dass Bar einen nicht trivialen Destruktor hat und der
x86-64 ABI (S. 19) speziell angibt:
Wenn ein C ++ - Objekt einen nichttrivialen Kopierkonstruktor oder einen nichttrivialen Destruktor hat, wird es über einen unsichtbaren Link übergeben (das Objekt wird in der Parameterliste durch einen Zeiger [...] ersetzt).
Ein späteres
Itanium C ++ ABI- Dokument definiert Folgendes:
Wenn der Parametertyp für den Zweck des Aufrufs nicht trivial ist, muss der Anrufer einen temporären Ort zuweisen und einen Link zu diesem temporären Ort übergeben:
[...]
Ein Typ wird für den Zweck des Aufrufs als nicht trivial betrachtet, wenn:
Es verfügt über einen nichttrivialen Kopierkonstruktor, einen sich bewegenden Konstruktor, einen Destruktor oder alle seine Verschiebungs- und Kopierkonstruktoren werden gelöscht.
Das erklärt also alles: Bar hat eine schlechtere Codegenerierung, weil sie über einen unsichtbaren Link geleitet wird. Es wird über eine unsichtbare Verbindung übertragen, da eine unglückliche Kombination zweier unabhängiger Umstände eingetreten ist:
- Laut ABI-Dokument werden Objekte mit nicht trivialem Destruktor über unsichtbare Links weitergeleitet
- Bar hat einen nicht trivialen Destruktor.
Dies ist ein klassischer
Syllogismus : Der erste Punkt ist die Hauptprämisse, der zweite ist privat. Infolgedessen wird Bar über eine unsichtbare Verbindung übertragen.
Lassen Sie uns einen Syllogismus geben:
- Alle Menschen sind sterblich
- Sokrates ist ein Mann.
- Folglich ist Sokrates sterblich.
Wenn wir die Schlussfolgerung „Sokrates ist sterblich“ widerlegen wollen, müssen wir eine der Prämissen widerlegen: entweder um die Hauptsache zu widerlegen (vielleicht sind einige Menschen nicht sterblich) oder um das Private zu widerlegen (vielleicht ist Sokrates keine Person).
Damit Bar in ein Register (wie Foo) aufgenommen werden kann, müssen wir eine von zwei Prämissen widerlegen. Der Standard-C ++ - Pfad besteht darin, Bar einen trivialen Destruktor zu geben, der die private Prämisse zerstört. Aber es gibt noch einen anderen Weg!
Wie [[trivial_abi]] das Problem löst
Das neue Clang-Attribut zerstört die Hauptprämisse. Clang erweitert das ABI-Dokument wie folgt:
Wenn der Parametertyp für den Zweck des Aufrufs nicht trivial ist, muss der Anrufer einen temporären Ort zuweisen und einen Link zu diesem temporären Ort übergeben:
[...]
Ein Typ wird für den Zweck des Aufrufs als nicht trivial betrachtet, wenn er als [[trivial_abi]] markiert ist und:
Es verfügt über einen nichttrivialen Kopierkonstruktor, einen sich bewegenden Konstruktor, einen Destruktor oder alle seine Verschiebungs- und Kopierkonstruktoren werden gelöscht.
Selbst wenn eine Klasse mit einem nichttrivial bewegten Konstruktor oder Destruktor für den Zweck des Aufrufs als trivial betrachtet werden kann, wenn sie als [[trivial_abi]] markiert ist.
Mit Clang können wir jetzt folgendermaßen schreiben:
#define TRIVIAL_ABI __attribute__((trivial_abi)) struct TRIVIAL_ABI Baz { int value; ~Baz() {}
Kompilieren Sie incr <Baz> und erhalten Sie den gleichen Code wie incr <Foo>!
Warnung Nr. 1: [[trivial_abi]] macht manchmal nichts
Ich würde hoffen, dass wir Wrapper für Standardbibliothekstypen wie folgt "trivial für Aufrufzwecke" machen können:
template<class T, class D> struct TRIVIAL_ABI trivial_unique_ptr : std::unique_ptr<T, D> { using std::unique_ptr<T, D>::unique_ptr; };
Leider funktioniert das nicht. Wenn Ihre Klasse über Basisklassen oder nicht statische Felder verfügt, die für den Zweck des Aufrufs "nicht trivial" sind, macht die Clang-Erweiterung in der Form, in der sie jetzt geschrieben ist, Ihre Klasse "irreversibel nicht trivial", und das Attribut hat keine Auswirkung. (Es werden keine Diagnosemeldungen ausgegeben. Dies bedeutet, dass Sie [[trivial_abi]] in der Klassenvorlage als optionales Attribut verwenden können und die Klasse "bedingt trivial" ist, was manchmal nützlich ist. Der Nachteil ist natürlich, dass Sie dies können Markieren Sie die Klasse als trivial und stellen Sie dann fest, dass der Compiler sie leise behoben hat.)
Das Attribut wird ohne Nachrichten ignoriert, wenn Ihre Klasse über eine virtuelle Basisklasse oder virtuelle Funktionen verfügt. In diesen Fällen passt es möglicherweise nicht in die Register, und ich weiß nicht, was Sie erhalten möchten, indem Sie es als Wert übergeben, aber Sie wissen es wahrscheinlich.
Soweit ich weiß, ist die einzige Möglichkeit, TRIVIAL_ABI für „Standard-Utility-Typen“ wie optional <T>, unique_ptr <T> und shared_ptr <T> zu verwenden, die
- Implementieren Sie sie selbst von Grund auf neu und wenden Sie das Attribut an, oder
- Brechen Sie in Ihre lokale Kopie von libc ++ ein und fügen Sie das Attribut dort mit Ihren Händen ein
(In der Open Source-Welt sind beide Methoden im Wesentlichen gleich.)
Warnung Nr. 2: Verantwortung des Zerstörers
Im Beispiel mit Foo / Bar hat die Klasse einen leeren Destruktor. Lassen Sie unsere Klasse tatsächlich einen nichttrivialen Destruktor haben.
struct Up1 { int value; Up1(Up1&& u) : value(u.value) { u.value = 0; } ~Up1() { puts("destroyed"); } };
Dies sollte Ihnen bekannt sein. Dies ist unique_ptr <int>, bis zum Limit vereinfacht, wobei die Nachricht beim Löschen gedruckt wird.
Ohne TRIVIAL_ABI sieht Inkr <Up1> wie Inkr <Bar> aus:
movl (%rsi), %eax addl $1, %eax movl %eax, (%rdi) movl $0, (%rsi) movq %rdi, %rax retq
Mit TRIVIAL_ABI sieht
Inkr größer und beängstigender aus !
pushq %rbx leal 1(%rdi), %ebx movl $.L.str, %edi callq puts movl %ebx, %eax popq %rbx retq
In der traditionellen Aufrufkonvention werden Typen mit einem nicht trivialen Destruktor immer von einer unsichtbaren Verknüpfung übergeben, was bedeutet, dass die empfangende Seite (in diesem Fall inkr) immer einen Zeiger auf ein Parameterobjekt akzeptiert, ohne dieses Objekt zu besitzen. Das Objekt gehört dem Anrufer. Dadurch funktioniert die Elisionsarbeit!
Wenn ein Typ mit [[trivial_abi]] in Registern übergeben wird, erstellen wir im Wesentlichen eine Kopie des Parameterobjekts.
Da x86-64 nur ein Register zurückgeben kann (Applaus), kann die aufgerufene Funktion das Objekt am Ende nicht zurückgeben. Die aufgerufene Funktion sollte das Eigentum an dem Objekt übernehmen, das wir an sie übergeben haben! Dies bedeutet, dass die aufgerufene Funktion den Destruktor des Parameterobjekts aufrufen muss, wenn es beendet ist.
In unserem vorherigen Beispiel, Foo / Bar / Baz, heißt der Destruktor, aber er war leer und wir haben es nicht bemerkt. In Inkr <Up2> sehen wir nun zusätzlichen Code, der vom Destruktor auf der Seite der aufgerufenen Funktion generiert wird.
Es kann davon ausgegangen werden, dass dieser zusätzliche Code in einigen Benutzerfällen generiert wird. Im Gegenteil, der Ruf des Zerstörers erscheint nirgendwo! Es wird in incr aufgerufen, weil es in der aufrufenden Funktion
nicht aufgerufen wird. Im Allgemeinen werden Preis und Nutzen ausgewogen sein.
Warnung Nr. 3: Zerstörerreihenfolge
Der Destruktor für einen Parameter mit einem trivialen ABI wird von der aufgerufenen Funktion aufgerufen und nicht von der aufrufenden (Warnung Nr. 2). Richard Smith weist darauf hin, dass dies bedeutet, dass er nicht in der Reihenfolge aufgerufen wird, in der sich die Destruktoren der anderen Parameter befinden.
struct TRIVIAL_ABI alpha { alpha() { puts("alpha constructed"); } ~alpha() { puts("alpha destroyed"); } }; struct beta { beta() { puts("beta constructed"); } ~beta() { puts("beta destroyed"); } }; void foo(alpha, beta) {} int main() { foo(alpha{}, beta{}); }
Dieser Code druckt:
alpha constructed beta constructed alpha destroyed beta destroyed
Wenn TRIVIAL_ABI als [[clang :: trivial_abi]] definiert ist, wird Folgendes ausgegeben:
alpha constructed beta constructed beta destroyed alpha destroyed
Beziehung zu einem "trivial verschiebbaren" / "verschiebbaren" Objekt
Keine Beziehung ..., was?
Wie Sie sehen, gibt es für die Klasse [[trivial_abi]] keine Anforderungen an eine bestimmte Semantik für einen sich bewegenden Konstruktor, Destruktor oder Standardkonstruktor. Eine bestimmte Klasse ist wahrscheinlich trivial verlagerbar, einfach weil die meisten Klassen trivial verlagerbar sind.
Wir können die Offset_ptr-Klasse einfach so machen, dass sie nicht trivial verschiebbar ist:
template<class T> class TRIVIAL_ABI offset_ptr { intptr_t value_; public: offset_ptr(T *p) : value_((const char*)p - (const char*)this) {} offset_ptr(const offset_ptr& rhs) : value_((const char*)rhs.get() - (const char*)this) {} T *get() const { return (T *)((const char *)this + value_); } offset_ptr& operator=(const offset_ptr& rhs) { value_ = ((const char*)rhs.get() - (const char*)this); return *this; } offset_ptr& operator+=(int diff) { value_ += (diff * sizeof (T)); return *this; } }; int main() { offset_ptr<int> top = &a[4]; top = incr(top); assert(top.get() == &a[5]); }
Hier ist der vollständige Code.Wenn TRIVIAL_ABI definiert ist, besteht der Clang-Trunk diesen Test bei -O0 und -O1, aber bei -O2 (d. H. Sobald er versucht, Aufrufe an trivial_offset_ptr :: operator + = und den Kopierkonstruktor zu inline), stürzt er beim Assert ab.
Also noch eine Warnung. Wenn Ihr Typ mit diesem Zeiger etwas so Verrücktes macht, möchten Sie ihn wahrscheinlich nicht in Registern übergeben.
Bug 37319 , in der Tat eine Anfrage nach Dokumentation. In diesem Fall stellt sich heraus, dass es keine Möglichkeit gibt, den Code so zu gestalten, wie es der Programmierer wünscht. Wir sagen, dass der Wert von value_ vom Wert dieses Zeigers abhängen sollte, aber von der Grenze zwischen der aufrufenden und der aufgerufenen Funktion befindet sich das Objekt in Registern und der Zeiger darauf existiert nicht! Daher schreibt die aufrufende Funktion es in den Speicher und übergibt diesen Zeiger erneut. Wie sollte die aufgerufene Funktion den richtigen Wert berechnen, um ihn in value_ zu schreiben? Vielleicht ist es besser zu fragen, wie es überhaupt bei -O0 funktioniert?
Dieser Code sollte überhaupt nicht funktionieren.Wenn Sie also [[trivial_abi]] verwenden möchten, sollten Sie Mitgliedsfunktionen (nicht nur spezielle, sondern generell) vermeiden, die stark von der eigenen Adresse des Objekts abhängen (mit einer undefinierten Bedeutung des Wortes "wesentlich").
Wenn eine Klasse als [[trivial_abi]] markiert ist und Sie eine Kopie erwarten, können Sie intuitiv copy plus memcpy erhalten. Und in ähnlicher Weise können Sie, wenn Sie einen Umzug erwarten, den Umzug plus memcpy erhalten.
Wenn ein Typ "trivial verschiebbar" ist (wie von mir in
C ++ Now definiert ), können Sie immer dann, wenn Sie mit Kopieren und Zerstören rechnen, tatsächlich memcpy erhalten. Und wenn Sie Vertreibung und Zerstörung erwarten, können Sie tatsächlich Memcpy bekommen. Tatsächlich gehen Aufrufe von Sonderfunktionen verloren, wenn wir von „trivialer Verlagerung“ sprechen, aber wenn die Klasse das Attribut [[trivial_abi]] von Clang hat, gehen Aufrufe nicht verloren. Sie erhalten nur (sozusagen) memcpy zusätzlich zu den erwarteten Anrufen. Diese Art von Memcpy ist der Preis, den Sie für eine schnellere Anrufregisterkonvention zahlen.
Links zur weiteren Lektüre:
Akira Hatanakas cfe-dev Thread vom November 2017Offizielle Clang-DokumentationDie Einheit testet auf trivial_abiFehler 37319: trivial_offset_ptr kann unmöglich funktionieren