Arbeitsweise des Application-Compilers
Im Gegensatz zu bisherigen optimierenden Compilern, die Programme auf der
Ebene von Blöcken, Schleifen und höchstens Routinen analysieren,
untersucht der Application-Compiler ein vollständiges Programm. Dazu legt
er eine umfangreiche Datenbasis im Directory PDB an (''Program DataBase''),
die einen Umfang von etwa 1 - 1.5 kB pro Source-Zeile hat. Mit diesen
Informationen versehen, kann er nun zum einen Fehler finden, die anderen
Compilern verborgen bleiben. Dazu gehören Routinenaufrufe mit einer
falschen Anzahl oder falschen Typen von Parametern, mehrfach oder gar nicht
initialisierte globale Variable und in manchen Fällen auch
Index-Überschreitungen bei Array-Zugriffen. Zum anderen werden
Optimierungen automatisch möglich, die man vorher nur durch zusätzliche
''Handarbeit'' erreichen konnte. Diese wollen wir uns i.f. etwas genauer
ansehen.
Eine Optimierung, die schon ab der Stufe -O0 durchgeführt wird, ist das
Weiterreichen von Konstanten (''constant propagation''). Dabei wird eine
Variable, die auf einen konstanten Wert gesetzt und i.f. nicht verändert
wird, durch diesen Wert ersetzt. Dieser Prozeß kann beliebig iteriert
werden, allerdings nicht über die Grenzen einer Routine hinaus. Der
Application-Compiler dagegen reicht Konstanten beliebig tief durch
Routinen-Aufrufe hindurch, sowohl nach oben als auch nach unten. Dies
ermöglicht dem nachfolgenden Vektorisierer u.U. eine ganze Menge
zusätzlicher Optimierungen, z.B. im folgenden Beispiel:
PROGRAM FOO
REAL*4 A(1000, 10)
REAL*4 B(10, 1000)
CALL TWICE(A, 1000, 10)
C
C CALL TWICE(B, 10, 1000)
C
END
SUBROUTINE TWICE(A, M, N)
REAL*4 A(M, N)
INTEGER M, N
INTEGER I, J
DO J=1, M
DO I=1, N
A(I, J) = 2 * A(I, J)
ENDDO
ENDDO
END
Normalerweise würde der Vektorisierer die Schleifen nicht verändern, da
sie schon so geordnet sind, daß auf aufeinanderfolgende Speicherplätze
auch direkt hintereinander zugegriffen wird. Durch das Weiterreichen der
konstanten Arraygrößen wird jedoch klar, daß es viel effizienter ist,
die lange statt der kurzen Schleife zu vektorisieren und dafür etwas
langsameren Speicherzugriff in Kauf zu nehmen. Daher werden die Schleifen
vertauscht.
Ein großes Problem bei der Vektorisierung ist das mögliche Überlappen
von Arrays, das sogenannte ''Aliasing''.11 In C kann es wegen der Möglichkeit von Pointern an
verschiedenen Stellen auftreten; daher ist der Optimierer gezwungen, den
schlimmsten Fall anzunehmen und vektorisiert viele Schleifen nicht, obwohl
es möglich wäre. In FORTRAN dagegen kommt Aliasing vor allem vor, wenn
ein Array mehrmals als Parameter in einem Routinenaufruf benutzt wird. Da
dies aber vom FORTRAN77-Standard explizit verboten wird, geht der
Optimierer auch davon aus, daß Array-Parameter in Unterroutinen zu
verschiedenen Speicherbereichen gehören und vektorisiert entsprechend.
Hat man sich nicht an den Standard gehalten, führt dies daher
normalerweise zu fehlerhaftem Programmverhalten. Der Application-Compiler
verfolgt alle Zeiger und Arrays durch das ganze Programm und kann daher in
einem Fall Fehler vermeiden, im anderen die Vektorisierungsmöglichkeiten
sehr verbessern.
Eine weitere Optimierung ist das ''Inlining'' von Routinen, d.h. das Ersetzen
eines Funktionsaufrufs durch den Code der Routine, natürlich unter
Anpassung der Parameter. Dies kann man beim Fortran-Compiler manuell
erreichen, beim C-Compiler gibt es eine solche Möglichkeit nicht. Der
Vorteil besteht nicht nur darin, daß der Overhead für einen
Funktionsaufruf entfällt, sondern es können sich dadurch ggf. auch neue
Optimierungsmöglichkeiten ergeben. Nachteil ist die dadurch wachsende
Programmgröße. Der Application-Compiler wägt Aufwand und Nutzen
gegeneinander ab, wobei er für jede Routine ihre Größe und die Zahl der
Aufrufe berücksichtigt, und führt dementsprechend ein Inlining geeigneter
Routinen durch. Durch Kommandozeilen-Parameter oder Direktiven kann man die
Kostenrechnung beeinflußen und selteneres oder häufigeres Inlining
ereichen (s. Abschnitt 3.3.5).
Manchmal hängt es von den Parametern einer Routine ab, wie sie sich am
besten optimieren läßt. Im obigen Beispielprogramm etwa würde man die
Reihenfolge der beiden Schleifen der Routine TWICE von dem Verhältnis der
Parameter M und N abhängig machen wollen. Kommen aber verschiedene
Parameter vor, wie z.B. wenn man den zweiten Aufruf von TWICE
entkommentiert, ist nicht klar, was der Compiler am besten tun soll. In
einem solchen Fall legt der Application-Compiler eine oder mehrere Kopien
einer Routine an, sogenannte ''Clones'', und optimiert jede anders.
Eine weitere wichtige Optimierung dient der Verringerung von
Speicherbank-Konflikten: Hat ein Array (mindestens) eine
Dimension, die ein Vielfaches der Anzahl der Speicherbänke beträgt,
vergrößert der Application-Compiler diese Dimension um eins, ähnlich wie
wir das im Beispiel-Programm linalg gemacht haben (vgl. 2.9.1).
Um seine Entscheidungen, z.B. über den Nutzen von Inling oder Cloning, zu
verbessern, kann der Application-Compiler auch Profiling-Daten benutzen,
allerdings z.Z. nur solche der Programme ''prof'' und ''gprof''. Eine Anpassung
an den CXpa ist geplant.
Ein häufiges Verhalten ist, daß durch eine der neuen Optimierungen des
Application-Compilers weitere Möglichkeiten zur Optimierung entstehen und
so das Laufzeitverhalten eines Programms verbessert wird. In unserem
Beispiel werden durch das Clonen von TWICE zwei Routinen erzeugt, die
jeweils nur einmal mit konstanten Werten aufgerufen werden. Beiden
Versionen können daher die Konstanten weitergegeben werden. Dies wiederum
ermöglicht das optimale Anpassen der Schleifen an die gegebenen Werte.
Peter Junglas 18.10.1993