Neue Antwort schreiben 
 
Themabewertung:
  • 1 Bewertung(en) - 5 im Durchschnitt
  • 1
  • 2
  • 3
  • 4
  • 5
Welche Art von Optimierungen nehmen moderne Compiler vor?
windowsneptune Offline
Windows Profi

Beiträge: 3.276
Registriert seit: Jun 2010
Beitrag #1
Welche Art von Optimierungen nehmen moderne Compiler vor?
Nabend WHF :>


Und zwar habe ich mich (mit kaum Coding-Erfahrung) gefragt, welche Art von Optimierungen im Codeverlauf von modernen Hochsprachen die entsprechenden Compiler vornehmen, welche Optimierungen selbstverständlich sind und welche sich schwierig umsetzen lassen.
Kennt sich damit jemand aus? Wäre super, einige Beispiele oder Links zu erhalten - bspw. darauf eingehend, welche Teile/Reihenfolge eines Codeschnipsels im Assembler optimiert wird (logischer Ablauf würde reichen bzw. wäre noch besser.)

mfG,
Neptune b1


Xeon E3 1231v3 | R9 290X @ Accelero Hybrid | 16GB DDR3-1866 CL9 | 840 EVO 250G | M550 256G
Dell Precision M4800 | i7-4910MQ | 8GB DDR3-1600 SC Quadro K2100M | UHD IPS-Igzo
Dell XPS 15 9570 | i7-8750H | GTX 1050 Ti | 16 GB DDR4 | UHD Touch

Google Pixel 6 Pro
(Dieser Beitrag wurde zuletzt bearbeitet: 19.02.2014 12:04 von windowsneptune.)
19.02.2014 11:58
Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
gandro Offline
Quälgeist

Beiträge: 8.951
Registriert seit: Jul 2008
Beitrag #2
RE: Welche Art von Optimierungen nehmen moderne Compiler vor?
Was tatsächlich gemacht wird, kann sich von Compiler zu Compiler Version ändern. Es gibt wenig "generelle", grosse Optimierungen, sondern viele kleine Dinge die getan werden. Es hängt auch ein bisschen von der Programmiersprache ab, je "höher" die Programmiersprache, desto mehr Optimierungspotential einsteht.

Relativ simple Optimierungen betreffen die Optimierung von mathematischen Operationen. Beispielsweise kann eine Division (oder Multiplikation) durch 2, 4, 8, 16, usw bei Ganzzahlen durch Bitverschiebung schneller gemacht werden:

x = y / 16 macht ein Compiler zu x = y >> 4.

Oder manche Rechnungen können bereits zur Kompilizierzeit vorgenommen werden: pi = 3.14159; u = 2 * pi * r wird zu u = 6.28318 * r. Sollte Pi in dem Programm später nicht mehr verwendet werden, dann wird der Compiler die Variable eliminieren.



Dann gibt es relativ viele Optimierungen die Schleifen betreffen:
Code:
for (i=0; i < 10; ++i) {
     a[i] = 17 * i;
}

Kann der Compiler die (langsame) Multiplikation durch eine schnellere Addition ersetzen:
Code:
tmp = -17;
for (i = 0; i < 10; ++i) {
     tmp = tmp + 17;
     a[i] = tmp;
}



Gerade bei Schleifen mit fixer Grösser kann der Compiler dann auch die Schleife durch "Copy&Paste" ersetzen ("Loop unwinding"):

Code:
for( i=0 ; i<8 ; i=i+1 ) {
    dest[i] = src[i];
}

wird zu

Code:
dest[0] = src[0];
dest[1] = src[1];
dest[2] = src[2];
dest[3] = src[3];
dest[4] = src[4];
dest[5] = src[5];
dest[6] = src[6];
dest[7] = src[7];



Je nach dem kann der Compiler auch Dinge etwas umordnen. Beispielsweise:
Code:
u = (b + c) * x;
v = (b + c) * y;

wird der gemeinsame Teil rausgenommen und nur 1x berechnet, anstatt zweimal:

Code:
tmp = b + c;
u = tmp * x;
v = tmp * y;



Oder eine If-Abfrage in der Schleife die jedes mal das gleich funktioniert kann rausgenommen werden, so dass die Überprüfung nur einmal stattfindet:

Code:
int result = 0;
boolean do_plus = ...;
for( i=0 ; i<100 ; i=i+1 ) {
  if(do_plus == true) {
    result = result + i;
  } else {
    result = result - i;
  }
}

wird zu

Code:
int result = 0;
boolean do_plus = ...;
if(do_plus == true) {
  for( i=0 ; i<100 ; i=i+1 ) {
    result = result + i;
  }
} else {
  for( i=0 ; i<100 ; i=i+1 ) {
    result = result - i;
  }
}
}



Das sind ein paar (imho) anschauliche Beispiele, hab die meisten aus der Wikipedia geklaut: https://en.wikipedia.org/wiki/Optimizing_compiler

Die Liste von möglichen Optimierungen ist aber riesig, insbesondere auch wenn es um das Generieren von Maschinen- oder Bytecode geht.
  • Auf Intel-Plattformen (x86) gibt es z.B. einen Maschinenbefehl der Addition und Multiplikation um 2,4,8 kombiniert: x = a * 4 + c kann in einem einzigen Assemblerbefehl gemacht werden, anstatt zwei (zuerst Multiplikation, dann Addition).
  • Spezielle SSE-Instruktionen können verwendet werden, wenn der Compiler merkt dass die gleiche Opteration mehrmals ausgeführt wird:

    Code:
    for (i=0; i<8; i++){
        a[i] = b[i] + c[i];
      }

    Anstatt da eine Schleife zu verwenden kann der Compiler da etwa dem Prozessor dank SSE-Instruktionen sagen, er solle alle 8 Rechnungen gleichzeitig ausführen, anstatt eine nach der anderen (nennt sich Schleifen Vektorisierung).
  • In Programmiersprachen wie Java kann manchmal die Überprüfung wegfallen, ob ein Zugriff im Array ist, wenn der Compiler garantieren kann, dass dies nicht der Fall ist, so dass weniger Überprüfungscode ausgeführt werden muss.
  • In Programmiersprachen mit bekannten Funktionen wie C kann der Compiler auch Funktionen strlen (Länge eines Strings berechnen) bereits zur Kompilierzeit ausführen, strlen("hallo") kann der Compiler ausrechnen dass das 5 ist, weil der String keine Variablen ist.
  • Weitere Optimierungen wären etwa das Rauslöschen von Funktionen die niemals aufgerufen werden, oder Schleifen die niemals ausgeführt werden usw.
  • In anderen Sprachen wo Rekursion wichtig kann der Compiler auch rekursive Funktionsaufrufe durch "Tail-Call-Optimization" wegoptimieren, damit die so schnell sind wie Schleifen.

Grundsätzlich gilt dass der Compiler häufig auch Dinge optimiert, wo der Mensch auch drauf gekommen wäre, also lieber mal eine Variable mehr verwenden wenn es den Code lesbarer macht.

Nachtrag: Eine leider etwas gar technische, aber kurze Übersicht findet auch in den LLVM-Dokus: http://llvm.org/docs/Passes.html
(Dieser Beitrag wurde zuletzt bearbeitet: 19.02.2014 14:02 von gandro.)
19.02.2014 13:19
Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
oreissig Offline
Maître Modérateur

Beiträge: 12.021
Registriert seit: Jul 2008
Beitrag #3
RE: Welche Art von Optimierungen nehmen moderne Compiler vor?
+1 für gandro


etwas OT: lohnt sich Loop Unrolling denn überhaupt in nennenswertem Umfang? also für zwei iterationen leuchtet mir das ja noch ein, aber den gesamten schleifenrumpf irgendwie 8x oder so zu replizieren bläßt den code doch ungemein auf und verschlechtert damit die cache-lokalität deutlich. bei moderenen sprungvorhersagen ist es doch bestimmt einfacher die schleife schleife sein zu lassen, sodass sie komplett im Cache abläuft und anderen Krams nicht verdrängt
19.02.2014 13:25
Webseite des Benutzers besuchen Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
gandro Offline
Quälgeist

Beiträge: 8.951
Registriert seit: Jul 2008
Beitrag #4
RE: Welche Art von Optimierungen nehmen moderne Compiler vor?
(19.02.2014 13:25)oreissig schrieb:  etwas OT: lohnt sich Loop Unrolling denn überhaupt in nennenswertem Umfang? also für zwei iterationen leuchtet mir das ja noch ein, aber den gesamten schleifenrumpf irgendwie 8x oder so zu replizieren bläßt den code doch ungemein auf und verschlechtert damit die cache-lokalität deutlich. bei moderenen sprungvorhersagen ist es doch bestimmt einfacher die schleife schleife sein zu lassen, sodass sie komplett im Cache abläuft und anderen Krams nicht verdrängt
Glaube wo es der Compiler es darf, wird teilweise Loop-Unwinding auch gemacht um den Instruction Level Parallelism zu erhöhen:

Code:
for(i=0; i < 1000; i++) {
  sum += a[i]
}

könnte ja mit Partial Loop Unwinding umgeschrieben werden als

Code:
for(i=0; i < 500 ; i+=2) {
  sum1 += a[i];
  sum2 += a[i+1];
}
sum = sum1 + sum2;

Erlaubt es dem Prozessor jetzt explizit zumindest zwei Additionen zu parallelisieren. Ich hab ehrlich gesagt aber keine Ahnung inwiefern sowas wirklich von Compilern gemacht wird (korrekt wäre es ja nur für Ints, nicht for Floats), oder ob Prozessoren evtl. inzwischen sogar in der Lage sind sowas selber zu erkennen.

Wobei ich mir auch vorstellen kann dass gerade solche Trade-Off Optimierungen (Cache vs. Branchprediction) auch schlicht sehr prozessorspezifisch sind.
Erinnere mich an ein Beispiel im GCC wo die Schleifenbedingung in while-Loops zweimal generiert wurde, einmal für die initiale Überprüfung, einmal für die Überprüfung während der Iteration, weil das dem Branch-Predictor vom P6 (Pentium Pro und aufwärts) besser gefallen hat. Wurde dann irgendwann aber geändert, weil spätere Intels sich da anders verhalten haben.
(Dieser Beitrag wurde zuletzt bearbeitet: 19.02.2014 14:00 von gandro.)
19.02.2014 13:53
Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
friedrichheinze Offline
...und Kondensatoren.

Beiträge: 2.840
Registriert seit: Jul 2008
Beitrag #5
RE: Welche Art von Optimierungen nehmen moderne Compiler vor?
(19.02.2014 13:25)oreissig schrieb:  etwas OT: lohnt sich Loop Unrolling denn überhaupt in nennenswertem Umfang? also für zwei iterationen leuchtet mir das ja noch ein, aber den gesamten schleifenrumpf irgendwie 8x oder so zu replizieren bläßt den code doch ungemein auf und verschlechtert damit die cache-lokalität deutlich. bei moderenen sprungvorhersagen ist es doch bestimmt einfacher die schleife schleife sein zu lassen, sodass sie komplett im Cache abläuft und anderen Krams nicht verdrängt

In meiner Erfahrung: Wenn du MMX/SSE zum Vektorisieren kriegst haste halt je nach wordsize nen deutlich höheren Durchsatz (bei 128 bit halt 4 Instruktionen auf einmal, also fast Faktor 4 Speedup, das kann sich in engen inneren Loops durchaus lohnen). Loop unrolling bringt normalerweise nicht sooo arg viel, aber ich hab hier und da auch schon 10% damit rausgeholt.

Cache-Lokalität ist hier vllt auch der falsche Begriff, gerade wenn du den Code 8x replizierst gehst du da doch linear durch, also wenn das den Cache nicht freut :)
19.02.2014 14:10
Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
oreissig Offline
Maître Modérateur

Beiträge: 12.021
Registriert seit: Jul 2008
Beitrag #6
RE: Welche Art von Optimierungen nehmen moderne Compiler vor?
(19.02.2014 14:10)friedrichheinze schrieb:  Cache-Lokalität ist hier vllt auch der falsche Begriff, gerade wenn du den Code 8x replizierst gehst du da doch linear durch, also wenn das den Cache nicht freut :)
es gibt ja auch noch ein Leben nach der Schleife, und wenn der Cache leergeputzt ist, weil er komplett mit entrollter Schleife belegt ist, haste danach ordentlich Mrs.
19.02.2014 14:34
Webseite des Benutzers besuchen Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
niwax Offline
Hardcore-Coder

Beiträge: 3.829
Registriert seit: Dec 2009
Beitrag #7
RE: Welche Art von Optimierungen nehmen moderne Compiler vor?
(19.02.2014 13:25)oreissig schrieb:  etwas OT: lohnt sich Loop Unrolling denn überhaupt in nennenswertem Umfang? also für zwei iterationen leuchtet mir das ja noch ein, aber den gesamten schleifenrumpf irgendwie 8x oder so zu replizieren bläßt den code doch ungemein auf und verschlechtert damit die cache-lokalität deutlich. bei moderenen sprungvorhersagen ist es doch bestimmt einfacher die schleife schleife sein zu lassen, sodass sie komplett im Cache abläuft und anderen Krams nicht verdrängt

Gerade bei kleinen Loops bringt das extrem viel. Wenn du zB Videos oder Bilder dekodierst, sind die üblicherweise in Makroblocks unterteilt, was bedeutet dass du zwei verschachtelte Loops hast, die letztendlich in jeder Runder nur eine Handvoll Instruktionen ausführen. Ohne Unrolling hättest du enormen Overhead weil nach jeder Ausführung von zwei oder drei Instruktionen zuerst noch eine Manipulation an der Schleifenvariablen kommt dann ein GOTO, das dazu auch noch konditional ist. Im schlimmsten Fall schlägt also bei Blocks von 8x8 Pixeln die Branching Prediction des Prozessors mindestens alle acht Runden ins leere und die gesamte Pipeline muss ausgeräumt und neu aufgebaut werden. Von dem verbrauchten Cache ergibt sich kaum ein Unterschied, da Code bei dem sich das Unrolling lohnt meistens nur ein paar Instruktionen hat. Dazu kommt, dass der Overhead entfällt und bei diesem Beispiel je vier (oder noch mehr mit AVX/AVX2) Durchläufe in eine Instruktion gepackt werden können.


19.02.2014 14:53
Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
friedrichheinze Offline
...und Kondensatoren.

Beiträge: 2.840
Registriert seit: Jul 2008
Beitrag #8
RE: Welche Art von Optimierungen nehmen moderne Compiler vor?
(19.02.2014 14:34)oreissig schrieb:  
(19.02.2014 14:10)friedrichheinze schrieb:  Cache-Lokalität ist hier vllt auch der falsche Begriff, gerade wenn du den Code 8x replizierst gehst du da doch linear durch, also wenn das den Cache nicht freut :)
es gibt ja auch noch ein Leben nach der Schleife, und wenn der Cache leergeputzt ist, weil er komplett mit entrollter Schleife belegt ist, haste danach ordentlich Mrs.

Wenn es noch ein Leben nach der Schleife gibt das relevant ist dann optimierst du gerade am falschen Code ;)
19.02.2014 15:56
Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
oreissig Offline
Maître Modérateur

Beiträge: 12.021
Registriert seit: Jul 2008
Beitrag #9
RE: Welche Art von Optimierungen nehmen moderne Compiler vor?
(19.02.2014 14:53)niwax schrieb:  Ohne Unrolling hättest du enormen Overhead weil nach jeder Ausführung von zwei oder drei Instruktionen zuerst noch eine Manipulation an der Schleifenvariablen kommt dann ein GOTO, das dazu auch noch konditional ist.
die Schleifenvariable muss ja auch im entrollten Zustand zwischen den einzelnen Schritten noch iteriert werden

(19.02.2014 14:53)niwax schrieb:  Im schlimmsten Fall schlägt also bei Blocks von 8x8 Pixeln die Branching Prediction des Prozessors mindestens alle acht Runden ins leere und die gesamte Pipeline muss ausgeräumt und neu aufgebaut werden.
dieser worst case ist sehr hypothetisch, selbst mit ganz einfachen heuristiken (z.B. Branches nach hinten werden genommen, branches nach vorn nicht) kriegt man da nur einen einzigen Miss beim Verlassen der Schleife. Effektiv ist die Branch Prediction heute aber um dimensionen über diesem trivialen Ansatz.

(19.02.2014 14:53)niwax schrieb:  Von dem verbrauchten Cache ergibt sich kaum ein Unterschied, da Code bei dem sich das Unrolling lohnt meistens nur ein paar Instruktionen hat.
Wie viel Platz belegt wird, hängt auch von der Anzahl der Schleifendurchläufe ab.

Vektorisierung hängt damit zwar zusammen, aber das meine ich hier nicht

(19.02.2014 15:56)friedrichheinze schrieb:  Wenn es noch ein Leben nach der Schleife gibt das relevant ist dann optimierst du gerade am falschen Code ;)
na selbst in der Schleife ist das ja ein Unterschied, ob der Code im Cache liegt, oder so groß ist, dass er sich dauernd neue cache lines ausm RAM pullen muss
(Dieser Beitrag wurde zuletzt bearbeitet: 19.02.2014 16:42 von oreissig.)
19.02.2014 16:41
Webseite des Benutzers besuchen Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
friedrichheinze Offline
...und Kondensatoren.

Beiträge: 2.840
Registriert seit: Jul 2008
Beitrag #10
RE: Welche Art von Optimierungen nehmen moderne Compiler vor?
(19.02.2014 16:41)oreissig schrieb:  na selbst in der Schleife ist das ja ein Unterschied, ob der Code im Cache liegt, oder so groß ist, dass er sich dauernd neue cache lines ausm RAM pullen muss

1. Instruction prefetch
2. kannst ja ausrechnen wie lange du Instruktionen unrollen kannst bis du 64KB L1 ICache voll hast...
19.02.2014 17:09
Alle Beiträge dieses Benutzers finden Diese Nachricht in einer Antwort zitieren
Neue Antwort schreiben 


Gehe zu:


Benutzer, die gerade dieses Thema anschauen: 1 Gast/Gäste