Die Art, wie std::variant
an verschiedene Besuchermethoden versendet, wenn std::visit
aufgerufen wird, ist ziemlich vernünftig, wenn die Variantenalternativen völlig unterschiedliche Typen sind. Im Wesentlichen wird eine Besucher-spezifische vtable
zur Kompilierungszeit aufgebaut und nach einer Fehlerüberprüfung 1 wird die entsprechende Besucherfunktion durch Indizieren der Tabelle basierend auf der aktuellen index()
, die zu etwas wie einem indirekten aufgelöst wird, betrachtet springe auf den meisten Plattformen.
Wenn die Alternativen eine gemeinsame Basisklasse verwenden, ist das Aufrufen einer (nicht virtuellen) Elementfunktion oder des Zugriffs auf den Status der Basisklasse mit einem Besucher konzeptionell viel einfacher: Sie rufen immer das selbe Methode und in der Regel mit dem gleichen Zeiger 2 auf die Basisklasse.
Dennoch endet die Implementierung genauso langsam. Zum Beispiel:
%Vor% Der generierte Code auf x86 für die neueste Version von gcc
und clang
ist ähnlich 3 (klingel):
call qword ptr [8*rcx + ...
ist der tatsächliche indirekte Aufruf einer Funktion, auf die vtable zeigt (die vtable selbst erscheint am Ende der Liste). Der Code davor prüft zuerst den "ist leer" -Zustand und richtet dann den visit
-Aufruf ein (ich bin mir nicht sicher, was die Seltsamkeit mit rdi
ist, ich schätze, es richtet einen Zeiger auf den Besucher als den erstes Argument oder etwas).
Die eigentlichen Methoden, auf die der vtable verweist und die von call
ausgeführt werden, sind sehr einfach, ein einzelner mov
, um das Member zu lesen. Kritisch, beide sind identisch:
Wir haben also ein großes Durcheinander. Um dieses einzelne mov
auszuführen, haben wir ein Dutzend Setup-Anweisungen und, was noch wichtiger ist, einen indirekten Zweig: Wenn das Targeting auf eine Reihe von Foobar
variant
-Objekten mit verschiedenen enthaltenen Alternativen sehr schlecht vorhersagt. Schließlich scheint der indirekte Aufruf eine unüberwindbare Barriere für weitere Optimierungen zu sein: Hier wird ein einfacher Aufruf ohne umgebenden Kontext betrachtet, aber im realen Einsatz könnte dieser zu einer größeren Funktion mit erheblichen Möglichkeiten für weitere Optimierung optimiert werden - aber ich denke indirekt Anruf wird es blockieren.
Du kannst mit dem Code selbst auf godbolt spielen .
Die Langsamkeit ist nicht inhärent: Hier ist eine sehr einfache "diskriminierte Union" struct
, die die beiden Klassen in union
zusammen mit einem isFoo
Diskriminator kombiniert, der verfolgt, welche Klasse enthalten ist:
Die entsprechende getBaseMemUnion
Funktion kompiliert bis zu einer einzelnen mov
Anweisung sowohl für gcc als auch für clang:
Zugegeben, die diskriminierte Gewerkschaft muss die "ist wertlos" -Fehlerbedingung nicht überprüfen, aber das ist nicht der Hauptgrund für variant
Langsamkeit, und in jedem Fall ist eine solche Bedingung unmöglich mit Foo
und Bar
da keiner ihrer Konstruktoren 4 wirft. Selbst wenn Sie einen solchen Zustand unterstützen wollten, ist die resultierende Funktion mit dem union
immer noch sehr effizient - nur ein kleiner Haken ist hinzugefügt, aber das Verhalten beim Aufruf der Basisklasse ist das gleiche.
Gibt es etwas, was ich tun kann, um% ce_de% efficient in diesem Fall zu verwenden, wenn man eine allgemeine Basisklassenfunktion aufruft, oder ist das Versprechen einer Null-Kosten-Abstraktion einfach nicht hier herauszuspulen?
Ich bin offen für ein anderes Anrufmuster, Compiler-Optionen, was auch immer.
1 Insbesondere Überprüfung, ob die Variante variant
aufgrund einer früheren fehlgeschlagenen Zuweisung ist.
2 Der Zeiger auf die Basisklasse hat nicht immer die gleiche Beziehung zum meist abgeleiteten Zeiger für alle Alternativen, zB wenn Mehrfachvererbung beteiligt ist.
3 Well valueless_by_exception
ist ein bisschen schlechter, da es die "is wertlose" Prüfung sowohl vor dem Aufruf von gcc
als auch in jeder automatisch erzeugten Methode redundant auszuführen scheint um by visit
. Clang macht es nur im Voraus. Denken Sie daran, wenn ich "gcc" sage, ich meine wirklich "gcc mit libstdc ++", während "clang" wirklich "clang mit libc ++" bedeutet. Einige Unterschiede, wie die redundante Überprüfung von vtable
in den generierten Besucherfunktionen, sind wahrscheinlich auf Unterschiede in der Bibliothek zurückzuführen, nicht auf Unterschiede bei der Compileroptimierung.
4 Wenn der index()
-Zustand problematisch ist, könnte man auch etwas wie valueless
Für was es wert ist, macht ein total handrolled Besuch mit einem switch
ziemlich gut:
Was würden Sie gerne verwenden:
%Vor% Mit VALUELESS
wird Folgendes ausgegeben:
Was ziemlich gut ist. Ohne VALUELESS
wird Folgendes ausgegeben:
wie gewünscht.
Ich weiß nicht wirklich, welche Schlussfolgerung, wenn überhaupt, daraus zu ziehen ist. Klar, gibt es Hoffnung?Tags und Links c++ x86 performance c++17 variant