Variable möglichst "lokal" im speicher platzieren?
-
Ich bastle gerade mit expression templates und automatischer vektorisierung herum (ähnlich wie Eigen). Das Vektorisieren funktioniert ganz gut (es werden sofern möglich operationen durch xmmintrin.h intrinsics ersetzt). Nur was immer wieder erratischer performanceprobleme erzeugt ist die "Nichtlokalität" von Daten.
Ich habe also etwa solchen code in einem ansonsten fast leeren programm:
Vector<float> a(n);
Vector<float> b(n);
b = 0.5*b + a;Dreht man im Compiler (gcc) die optimierung schön hoch (-O2 -funroll-loops) bekommt man erstmal sehr schöne performance. Dann fügt man weiteren code in das programm ein und wenn man es das nächste mal laufen lässt ist genau dieselbe operation auf einmal bis 20-50% langsamer. Das ganze konnte ich jetzt darauf zurückführen, dass der compiler die 0.5 als konstante ins executable einfügt und in der schleife immer wieder von einer position "im code" lädt. Wenn nun dieser abstand zwischen konstante und code zu gross wird werden cachemisses immer wahrscheinlicher. Das führt dann zu so effekten wie dass etwa die version da oben mit der konstanten 0.5 "längsam" läuft, aber wenn ich sowas wie:
{
float f;
std::cin >> f;
Vector<float> a(n);
Vector<float> b(n);
b = f*b + a;
}mache ist die 0.5 eingebe ist es schneller, weil die Variable f "näher" am Code ist oder sich auf dem stack befindet der auch eine sehr hohe chance hat sowieso im cache zu sein. Am liebsten hätte ich f natürlich direkt vor einem der arrays, am besten noch auf derselben page stehen oder würde sie nur einmal in ein register laden lassen, nur da sehe ich es noch weniger wie man den Compiler rein durch intrinsics und templates dazu bewegen könnte.
Jetzt reicht es im ersten fall nicht, wenn ich aus den 0.5 einfach eine variable mache, weil der compiler das merkt wenn die faktisch konstant ist und mir daraus wieder eine Compilerzeit Konstante erzeugt.
Hat da wer ne Idee?
-
Gibt es da nicht was von volatile?
-
Volatile ist ja normal dazu da um den Compiler zu sagen, dass die Variable auch von außerhalb eventuell verändert werden könnte und dass der Compiler daher keine Optimierungen mit dieser Variable durchführen soll.
Lg freeG
-
Das ist eher ein Compileroptimierungsproblem. Die Sprache C++ bietet dafür nicht wirklich etwas an. Was helfen könnte, sind entweder spezielle Compilerschalter/pragmas oder clevere Optimierungsoptionen. Ersteres musst du im Handbuch deines Compilers nachlesen. Optimierungsoptionen würde ich mal eine "profile-guided" Optimierung probieren, die der GCC und viele andere Compiler bieten. Dabei müssten solche Problematiken wie Nichtlokalität gelöst werden (sofern möglich - manchmal geht es eben einfach nicht ohne den Agorithmus an sich zu verbessern). Außerdem schraub ruhig mal auf O3 hoch, das bringt meiner Erfahrung nach auch oft noch etwas.
edit: Und deine Ausführungen zu den Ursachen klingen nach Unfug. Computerprogramme funktionieren nicht so. Wie kommst du zu deiner Analyse?
-
SeppJ schrieb:
Außerdem schraub ruhig mal auf O3 hoch, das bringt meiner Erfahrung nach auch oft noch etwas.
edit: Und deine Ausführungen zu den Ursachen klingen nach Unfug. Computerprogramme funktionieren nicht so. Wie kommst du zu deiner Analyse?
O3 führt gerne mal dazu dass der Compiler sich für schlauer hält als die "vorgegebene" Vektorisierung und das ganze verschlimmbessert. Peakwerte erreiche ich eigentlich konsistent mit O2 (compiler output ohne intrinsics hingegen profitiert schon davon, aber das ist hier nicht relevant).
Was meinst du mit "programme funktionieren nicht so"?
Ich zeihe meine Schlüsse vorallem aus dem Assembler output des Compilers. Konstanten werden am ende in das executable eingefügt und entsprechend von dort geladen. Dass weit über den speicher verstreute daten zu mehr Cache misses führen ist jetzt auch nicht gerade ein Geheimnis. Dass das die ursache für die performance schwankungen ist, ist einfach das naheliegendste nach meinem bisherigen geteste. Einmal weil der Durchsatz dann hoch ist wenn der Faktor gemäss assembler vom stack geladen wird. Andererseits weil sogar identischer code der durch inlining an verschiedenen stellen im executable steht "gegen hinten" im programm immer schneller arbeiten (Unterschied im Gflop bereich).
-
japro schrieb:
O3 führt gerne mal dazu dass der Compiler sich für schlauer hält als die "vorgegebene" Vektorisierung und das ganze verschlimmbessert. Peakwerte erreiche ich eigentlich konsistent mit O2.
Konnt ich noch nicht feststellen.
Haste auch brav -march=native an?
-
Ja, wobei das für das "verpfuschen" der intrinsics auch nicht dermassen relevant ist. Es geht da um so kram wie dass er etwa einen ganzen stapel von "addpd" operationen aufeinanderstapelt weil er sich einbildet dass repetierte instruktion schneller ausgeführt werden, nur wenn alle adds auf dasselbe register wirken zerschiesst er einfach die pipeline...
Jedenfalls ist das eigentlich nicht relvant für dieses Thema. Ich erhalte für O2 wie O3 im konkreten fall identische werte. Hingegen werte zwischen 5 und 8 Gflops abhängig davon wo im speicher sich die "konstante" befindet.
-
japro schrieb:
Was meinst du mit "programme funktionieren nicht so"?
Das heißt, dass die Lokalität von Konstanten keine Rolle spielen sollte. Die liegen im Codesegment des Programms und dieses sollte lokal sein, schließlich wird der Code gerade ausgeführt. Und so ein Zugriff auf eine feste Adresse ist außerdem ganz super vorhersehbar.
O3 führt gerne mal dazu dass der Compiler sich für schlauer hält als die "vorgegebene" Vektorisierung und das ganze verschlimmbessert. Peakwerte erreiche ich eigentlich konsistent mit O2 (compiler output ohne intrinsics hingegen profitiert schon davon, aber das ist hier nicht relevant).
Interessant, dass unsere Erfahrungen sich so extrem unterscheiden. O3 ist meiner Erfahrung nach ein kleiner Götterhammer was Optimierung angeht. Bei den Compilern die ich nutze (gcc, intel) wird nämlich erst auf dieser Stufe das Inlining aktiviert und das bringt doch ganz gewaltig viel.
-
Lokalität meine ich in diesem sinne eher im bezug auf cache/cachelines und pages. Es befindet sich ja nicht das ganze Programm gleichzeitig im T0 Cache, dort sollten sich aber gefälligst die operanden der SSE instruktionen befinden. Nur schon ein wert der sich aus versehen im T1 befindet kann die ausführungszeit des befehls verdoppeln und ein vollständer Ladevorgang aus dem Speicher vernichtet gerne mal mehrere hundert Zyklen.
Was O3 angeht muss man in diesem fall zwischen "normalem" code und intrinsics unterscheiden. Einerseits hält sich mindestens gcc 4.4 auch schon bei O1 an das von mir vorgegebene inlining, ausserdem erzeugen die intrinsics prinzipiell vektorisierten output während die automatische vektorisierung von gcc erst bei O3 anspringt.
-
Ein kleines update:
Ich konnte das ganze tatsächlich etwas verbessern in dem ich variablen in den expression templates via volatiles zuweise. Ich erzeuge in der funktion die den "knoten" erzeugt eine lokale volatile variable und lade über die. Auf diese weise ist der Compiler dann nicht mehr so eifrig was das propagieren von Konstanten angeht. Was sich als weiterer Grund für die Leistungsschwankungen zeigte war die Codegrösse. Ich habe das ganze gemessen indem ich die operationen ein par tausend mal in einer schleife ausgeführt habe. Nun braucht man für hohe leistung das loop unrolling der SSE schleifen, also benutzt man -funroll-loops. Nur der Compiler ist dann besonders eifrig, unrollt auch noch die äussere Benchmark-schleife UND inlined zusätzlich. Das resultat ist dann ellenlanger code und gelegentlich kommt es dann vor, dass eine der inneren schleifen über eine pagegrenze läuft oder dergleichen und auf einmal hat man sehr teure sprungbefehle.
Ich bekomme jetzt konsistentere resultate in dem ich das loop unrolling dadurch unterdrücke, dass ich die obergrenzen der loops volatile mache (gibts da was besseres?).