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.

previous    contents     next

Peter Junglas 18.10.1993