Hi,
Ich arbeite mit VS 2010 und habe, um einen Überblick über die Optimierungsmöglichkeiten zu kriegen, mal meine Kompilate mit den Quelltexten verglichen …
Zuerst muss ich sagen: Der Optimizer ist gut. Er leistet stellenweise so gute Arbeit, dass ich mit meinen Assembler-Basiskenntnissen nicht mehr nachvollziehen kann, was in bestimmten Codeabschnitten vorgeht (was auch der Grund ist, warum ich mich jetzt hier erkundigen möchte).
Dann gibt es aber auch Stellen, an denen – meiner Meinung nach – offensichtliche Optimierungsmöglichkeiten schlicht ignoriert werden. Das sind vor allem: Aussagenlogik mit integralen Typen, Exception-Overhead und vftables in abstrakten Klassen.
Aussagenlogik mit integralen Typen:
Mir stieß schon in dem Thread über den möglichst schnellen Point-in-Rect-Test bitter auf, dass der Compiler alle Ausdrücke behandelt, als ob bei ihrer Auswertung Side-Effects aufträten – auch, wenn es sich nur um Ints handelt. Ein Beispiel: Testen, ob einer von drei Zeigern nicht nullptr ist.
Code: Alles auswählen
// 1)
if((nullptr != p1) || (nullptr != p2) || (nullptr != p3))
// Kompiliert zu sechs Befehlen, davon drei bedingte Sprünge
// 2)
if((nullptr != p1) | (nullptr != p2) | (nullptr != p3))
// Kompiliert zu zwölf Befehlen, davon ein bedingter Sprung
// 3)
if(uintptr_t(p1) | uintptr_t(p2) | uintptr_t(p3))
// Kompiliert zu vier Befehlen, davon ein bedingter Sprung
Exception-Overhead:
Der Compiler behandelt jedes throw mit inline. Wenn man mal darüber nachdenkt, ist das doch wahnwitzig: throw impliziert doch, dass der Codeabschnitt bestenfalls nie benutzt werden wird und dass die Performance des umliegenden Codes wichtiger ist, sonst würde man return verwenden. Außerdem ist das Schmeißen einer Exception von sich aus teuer wie nichts, aber trotzdem optimiert es der Compiler, als ginge es um Leben und Tod. Beispiel:
Code: Alles auswählen
// Üblich:
class CException : public ::std::exception {
public:
CException() throw() { }
virtual char const * what() const throw() { return "dummy"; }
};
if(!SomeCrucialCondition)
throw CException; // Kompiliert zu sechs Befehlen, bei komplexeren Exception-Klassen auch gern zu elf oder >20.
// Dem gegenüber:
class CException : public ::std::exception {
protected:
CException() throw() { }
public:
virtual char const * what() const throw() { return "dummy"; }
__declspec(noinline) __declspec(noreturn) static void Throw() { throw CException(); }
};
if(!SomeCrucialCondition)
CException::Throw(); // Kompiliert zu *einem* Befehl
Wenn der Compiler schon sonst nicht weiß, welcher Pfad bevorzugt behandelt werden muss: throw ist doch der Wink mit dem Zaunpfahl schlechthin, dass dem Pfad Geschwindigkeit egal ist. Der Compiler müsste mit Leichtigkeit erkennen können, dass derselbe throw-Code hunderte Male an unkritischen Stellen im Code vorkommt, es mit Leichtigkeit auslagern können und COMDAT-Folding würde dann den Rest erledigen. Auch hier wieder: Ist das Absicht? (Unter x64 sieht die Situation glücklicherweise besser aus, wenn auch bloß architekturbedingt.)
vftables in abstrakten Klassen:
__declspec(novtable) erlaubt dem Compiler, den vftable einer abstrakten Klasse wegzulassen – da abstrakte Klassen ja sowieso nicht instanziiert werden können. Nette kleine Optimierung, die wieder einen unwesentlichen bis beachtlichen Overhead aus dem Kompilat treibt – je nachdem, wie viele oder wie wenige abstrakte Klassen man benutzt. Warum macht der Compiler das nicht automatisch? Gibt es überhaupt einen Fall, in dem der vftable einer abstrakten Klasse zur Laufzeit erreichbar sein muss?
Ich hoffe, ihr könnt mich erleuchten …
Gruß, Ky