Membervariablen vs. lokale Variablen
-
Hallo Leute!
Ich habe heute eine interessante Feststellung gemacht.
Ich habe ein Programm geschrieben, in dem lineare Gleichungssysteme gelöst werden. Da werden in den einzelenen Iterationsschritten die Gleichungen die in einem 2d-Feld abgespeichert sind ausgerechnet. Wenn ich dabei auf Membervariablen der Matrix Klasse, die die Gleichungen enthält zurückgreife ist mein Programm um einiges langsamer im Gegensatz zur Benutzung von lokalen Variablen.
Ein Beispiel:void Iterations::Calculate(Matrix & rkEq, Matrix & rkST, double dOmega) { double dOneMinusOmega = 1.0-dOmega; for (int x=1; x<100; x++) { for (int y=1; y<100; y++) { rkEq[x][y] = dOneMinusOmega*rkEq[x][y] + ... } } return ; }ist um einiges schneller als
void Iterations::Calculate(Matrix & rkEq, Matrix & rkST, double dOmega) { // die Berechnung von dOneMinusOmega wird vorher durchgeführt for (int x=1; x<100; x++) { for (int y=1; y<100; y++) { rkEq[x][y] = m_dOneMinusOmega*rkEq[x][y] + ... } } return ; }Kann mir das einer erklären?

Gruß Mea
-
Hallo Mea,
zumindestens kann ich es vermuten.
Im ersten Fall ist der Faktor, der ja in Deiner Berechnung sehr häufig benutzt wird, eine lokale Variable. Der Compiler hat also die Möglichkeit, diese, solange die Schleife läuft, in ein Register der CPU zu packen, da sie mit legalen Mitteln nicht innerhalb der for-Schleife verändert werden kann.Im Falle der Membervariable tut er das nicht. Denn eine Membervariable könnte ja indirekt durch Aufruf einer Methode oder vielleicht in einem anderen Thread manipuliert werden, während die Schleife abgearbeitet wird. Daher bleibt die Variable im Hauptspeicher und muss vor jeder Multiplikation da rausgeholt werden. Du wirst jetzt vielleicht sagen, dass 'm_dOneMinusOmega' sicher nicht verändert wird, aber der Compiler kann das nicht wissen.
Gruß
Werner
-
Die Frage ist natürlich auch, welchen Compiler du verwendest und vorallem welche Optimierungen du eingeschaltet hast.
-
Zunächst mal ist das rein Implementationsabhängig, sprich lokale Variable können, aber müssen nicht schneller sein. Das hängt davon ab, wie der Compiler die Zugriffe auf lokale Variable und auf Member implementiert, je nach Architektur auch davon ob und wie viele Register der Compiler zum "Jonglieren" zur Verfügung hat. Ausserdem hängt es von den Optimierungseinstellungen ab. Die Frage lässt sich also nicht allgemeingültig beantworten. Wie starkt vor allem der Unterschied optimiert / nicht optimiert bezüglich nur verschiedenen Source-Varianten ausfallen kann, hab ich neulich mit Boyer-Moore-Suche gemerkt, eine "C-artige" Implementation war gegenüber einer String-Iterator basierten im unoptimierten Modus Faktor 10 schneller, im optimierten Modus waren beide gleich schnell.

Werner Salomon schrieb:
zumindestens kann ich es vermuten.
Im ersten Fall ist der Faktor, der ja in Deiner Berechnung sehr häufig benutzt wird, eine lokale Variable. Der Compiler hat also die Möglichkeit, diese, solange die Schleife läuft, in ein Register der CPU zu packen, da sie mit legalen Mitteln nicht innerhalb der for-Schleife verändert werden kann.Im Falle der Membervariable tut er das nicht. Denn eine Membervariable könnte ja indirekt durch Aufruf einer Methode oder vielleicht in einem anderen Thread manipuliert werden, während die Schleife abgearbeitet wird. Daher bleibt die Variable im Hauptspeicher und muss vor jeder Multiplikation da rausgeholt werden. Du wirst jetzt vielleicht sagen, dass 'm_dOneMinusOmega' sicher nicht verändert wird, aber der Compiler kann das nicht wissen.
Das ist wohl eher nicht der Grund, der Standard kennt keine Threads und die Regeln für das Verhalten der abstrakten Maschine auf der C++ definiert ist, geben diesbezüglich nur wenig vor. Es ist dem Compiler somit durchaus freigestellt die Variable (Member oder nicht) die ganze Zeit der Schleife in einem Register zu halten solange Funktionen oder Methoden darin nicht aufgerufen werden. Neuere Optimierungsansätze erkenne sogar bei einigen Aufrufen durchaus, ob diese einen Einfluß darauf haben könnten. Eine Ausnahme wäre nur, wenn m_dOneMinusOmega volatile deklariert wäre, das macht man aber hier wohl eher nicht.

PS: Allerdings würde durch das volatile die Membervariable nicht automatisch "threadfest", nur damit das nicht misverstanden wird.
-
@Hutzli
Unabhängig von Threads und Registern musst du hier das Thema Aliasing berücksichtigen. Ohne spezielle Optimierungen die den Kontext der Funktion berücksichtigen muss der Compiler annehmen, dass es weitere Referenzen auf die Matrizen gibt und das über eine dieser Referenzen das Objekt geändert werden könnte (das ist auch der Grund, warum const-correctness selten automatisch zu besseren Performance führt). Bei vielen Compilern gibt es aber nun die Möglichkeit Aliasing explizit auszuschließen (dem Compiler zu sagen er soll immer das Beste annehmen). Dazu kommen Sachen wie whole program optimization usw.
Deshalb ja genau auch mein erster Beitrag.
-
HumeSikkins schrieb:
@Hutzli
Unabhängig von Threads und Registern musst du hier das Thema Aliasing berücksichtigen. Ohne spezielle Optimierungen die den Kontext der Funktion berücksichtigen muss der Compiler annehmen, dass es weitere Referenzen auf die Matrizen gibt und das über eine dieser Referenzen das Objekt geändert werden könnte (das ist auch der Grund, warum const-correctness selten automatisch zu besseren Performance führt). Bei vielen Compilern gibt es aber nun die Möglichkeit Aliasing explizit auszuschließen (dem Compiler zu sagen er soll immer das Beste annehmen). Dazu kommen Sachen wie whole program optimization usw.
Deshalb ja genau auch mein erster Beitrag.Ja, aber ich wollte das Thema nicht noch komplizierter machen, weil es dem OP vermutlich immer weniger hilft. Klar, die Abläufe in einem Optimizer sind nicht umsonst so komplex. Aber es ist eben dem Compiler nicht verboten, wegen der Gefahr von Effekten durch Threads, non volatiles über Expression-Grenzen bzw. Synchronisationspunkte hinweg in Registern zu halten. In der Tat muß er noch weitere Dinge leisten um diese Optimierung zu ermöglichen und eine Aliasing-Behandlung ist eines der Themen. Aber alle noch denkbaren Details zu erörtern kann mehr als einen Thread füllen und gehört dann langsam eher in ein Compilerbau-Thema. Ein Thema das mich sicher reizt (schreibe ich doch nebenher an einem kleinen, der sich aber noch keines relevanten Optimizers rühmen darf und nichts mit C++ zu tun hat), aber das zur Beantwortung der Frage des OP wohl zu tief geht.

-
Erst mal danke für die vielen Antworten.
Dann werde ich mich wohl mal an die Compiler Optionen begeben müssen. Ich benutze übrigens den g++ mit -O3 und -funroll-all-loops als Optimierungsstufen.
Mal sehen zu welchen Ergebnissen ich noch so komme.
Gruß Mea