Programmieren in Fortran 90/95


  1. Datenfelder (engl. arrays) oder indizierte Variablen
  2. Datenfelder (auch engl. arrays oder indizierte Variablen genannt) werden benötigt, wenn viele gleichartig strukturierte Daten mit einem Programm verarbeitet werden sollen. Arrays sind ebenso notwendig, falls der Gesamtumfang der Datensätze bei der Entwicklung des Programms noch nicht feststeht.

    Denn in solchen Fällen wäre es äußerst umständlich, wenn man sich immer neue Variablen mit leicht modifizierten Namen definieren und im Programm einsetzen müsste. Viel einfacher und viel sinnvoller ist es, wenn man sich die einzelnen Datensätze in durchnummerierten Namen abspeichern und mit Hilfe eines Indizes auf den einzelnen Datensatz zugreifen kann. Zum Beispiel:

     a(1), a(2), ...., a(100) 
    
    statt
     a1, a2, ..., a100
    
    Im ungünstigen 2. Fall müsste man 100 verschiedene Variablen deklarieren und auf die einzelne Variable durch Angabe des Namens zugreifen, während man im 1. Fall durch die Indizierung leicht über eine Schleife auf alle Komponenten des Datenfelds zugreifen kann. Will man z.B. von jeder Komponente des Datenfelds den natürlichen Logarithmus bilden, könnte man im 1. Fall schreiben
        integer :: i
    
        do i = 1, 100
            a(i) = log( a(i) )
        end do
    
    Im ungünstigen 2. Fall müsste man die 100 Variablen einzeln aufführen
        a1 = log(a1)
        a2 = log(a2)
        a3 = log(a3)
        ...
        a100 = log(a100)
    
    Schon anhand dieses Beispiels lassen sich die immensen Vorteile bei der Verwendung indizierter Variablen (Datenfelder) in Programmen erahnen. Als Weiterentwicklung von Fortran 77 bietet Fortran 90/95 erweiterte Zugriffsmöglichkeiten auf die Komponenten eines Datenfelds als Ganzes, z.B. Wertzuweisungen, Ausgaben, Additionen etc. die die Programmerstellung nochmals erleichtern. In Fortran 90/95 lässt sich sogar schreiben, nachdem a als Array vom Datentyp real mit 100 Komponenten deklariert wurde, um an allen Komponenten des Datenfeldes den natürlichen Logarithmus der Komponente im Datenfeld stehen zu haben
       a = log(a) 
    

    Deklaration von Datenfeldern (statisch)

    Im Deklarationsteil einer Programmeinheit muss bei einer statischen Deklaration für jedes Datenfeld der Datentyp, die Dimension und der Name festgelegt werden. Beispiel: einen Vektor v mit den 3 Komponenten v(1), v(2) und v(3) deklarieren:
       real, dimension(3) :: v
    
    alternativ ließe sich noch die alte Fortran 77 - Syntax verwenden (hier sei dies nur der Vollständigkeit halber erwähnt, bitte nicht mehr in neuen Fortran 90/95-Programmen einsetzen!)
       real :: v(3)
    
    Beispiele:

    Die implizite do-Schleife bei der Ein- und Ausgabe von Feldern (Fortran 77)

    Durch eine implizite do-Schleife lassen sich die Elemente eines Datenfelds sukzessive in einer Zeile ausgeben. Dieses Verfahren entspricht dem alten Fortran 77 - Syntax. In Fortran 90/95 geht es aber
    noch viel einfacher. Nach dem alten Fortran 77 - Syntax, kann zur Ausgabe alle Komponenten eines Feldes eine implizite do-Schleife einsetzen. Will man z.B. alle Elemente eines dreikomponentigen Vektors v in einer Zeile ausgeben, so kann man dies durch
       write(*,*) ( v(i), i=1,3 )
    
    realisieren. Siehe auch

    Mit einer impliziten do-Schleife lassen sich analog Werte zeilenorientiert von der Standardeingabe oder von Dateien einlesen und sukzessive den einzelnen Komponenten eines Datenfelds zuweisen.

    Ein- und Ausgabe eines eindimensionalen Arrays in Fortran 90/95

    In Fortran 90/95 lässt sich die implizite do-Schleife zur Ausgabe von Datenfeldern durch ein einfacheres Konstrukt ersetzen. Zum Beispiel wenn vom obigen Vektor v alle Komponenten nacheinander (in einer Zeile) ausgegeben werden sollen:
       write(*,*) v 
    
    bzw., wenn man nur Unterbereiche des Vektors v, z.B. hintereinander die 1. und 2. Komponente ausgeben möchte, so kann man dies einfach durch Angabe des Indexbereiches realisieren:
       write(*,*) v(1:2) 
    
    Siehe auch

    Analog zum obigen Beispiel kann man in Fortran 90/95 auch zeilenorientiert einlesen.

    Deklaration und Ausgabe zweidimensionaler Datenfelder (statisch)

    Bei der statischen Deklaration zweidimensionaler Datenfeldern wird - wie in der Mathematik - die erste Angabe in dem Attribut dimension mit der Zeilendimension und der zweite Wert mit der Spaltendimension assoziiert. Zum Beispiel definiert
          integer, dimension(3,4) :: a
    
    ein zweidimensionales Datenfeld a, welches 3 Zeilen und 4 Spalten und somit 12 Komponenten aufweist (a ist somit eine 3 x 4 - Matrix mit der Zeilendimension 3 und der Spaltendimension 4)
           a(1,1)  a(1,2)  a(1,3)  a(1,4)
           a(2,1)  a(2,2)  a(2,3)  a(2,4)
           a(3,1)  a(3,2)  a(3,3)  a(3,4)
    
    Im Speicher werden diese Werte nacheinander abgelegt.

    Achtung: Dabei werden im Memory des Rechners die Komponenten des Arrays a spaltenweise abgelegt. In den Speicherplätzen stehen somit aufeinanderfolgend

    a(1,1) a(2,1) a(3,1) a(1,2) a(2,2) a(3,2) a(1,3) a(2,3) a(3,3) a(1,4) a(2,4) a(3,4)
    
    Bei einem Speicherzugriff auf die (i,j)-te Komponente eines n x m Datenfeldes, also auf a(i,j) wird immer die relative Indexposition zum Beginn der ersten Komponente des Datenfeldes (hier a(1,1) berechnet. Von dieser Startposition aus wird zur Speicherstelle der (i,j)-ten Komponente gesprungen und danach der dortige Inhalt je nach Befehl ausgelesen oder abgelegt.

    Will man die (i,j)-te Komponente eines n x m - Datenfeldes auslesen, so muss sich der Zeiger, der auf den Inhalt des auszulesenden Datenfeldes zeigen soll, relativ zum Beginn des Speicherplatzes des Datenfeldes

            ( (j-1) * Zeilendimension + (i-1) ) * Anzahl der Byte pro Datentyp
            = ((j-1) * n + (i-1) * * Anzahl der Byte pro Datentyp
    weiterbewegen.

    Konkreter anhand des obigen Zahlenbeispiels: Will man z.B. den Wert von a(2,4), die 4. Komponente in der 2. Zeile des Feldes a, auslesen, so wird von dem Beginn des Datenfeldes

            ( (j-1) * Zeilendimension + (i-1) ) * Anzahl der Byte pro Datentyp
            = ( (4-1)*3+(2-1) ) * 4 Byte für den Datentyp integer
            = ( 3*3 + 1 ) * 4 Byte
            = 40 Byte

    weitergegangen und die folgenden 4 Bytes als Wert der Komponente a(2,4) aus dem Speicher ausgelesen. Als Gegenprobe stellt man fest, dass sich aufgrund der spaltenweisen sequentiellen Datensicherung, die Komponente a(2,4) an 11. Stelle in der Speicherreihenfolge befindet, so dass beim Datentyp integer bei 4 Byte als interne Repräsentation der Zahlen relativ zum Beginn des Speicherbereichs der Matrixkomponenten 40 Byte übersprungen werden müssen, bevor die zu a(2,4) gehörenden 4 Byte ausgelesen und weiterverarbeitet werden können.

    Dementsprechend lässt sich als zweites Beispiel z.B. der Wert von a(3,2) (die Matrixkomponente in der 3. Zeile und der 2. Spalte) in den 4 Byte ab dem

            ( (2-1)*3+ (3-1) )*4 Byte
            = 5*4 Byte

    ab dem 20. Byte relativ zum Beginn des Datenfeldes finden.

    Werden unmittelbar aufeinanderfolgende Speicherstellen eines Datenfeldes ausgelesen, z.B. a(1,3) nach a(3,2), so muss der Computer diesmal nicht die relative Position berechnen, sondern "weiss", dass er nur die folgenden (hier 4) Bytes auszulesen braucht und erspart sich den sonst anfallenden Zeitaufwand für die relative Positionsberechnung.

    Fazit: Insbesondere bei grossen mehrdimensionalen Datenfeldern ist der Zugriff auf unmittelbar aufeinanderfolgende Speicherplätze sehr viel schneller, wenn der Zugriff spaltenorientiert erfolgt, da die relativen Positionen bezüglich des Anfangspunktes des Arrays in diesem Fall nicht extra berechnet werden müssen.

    Merke: eine laufzeitoptimierte Programmierung im Umgang mit Datenfeldern muss sich an der internen Organisation des Speichers orientieren.

    Demo-Beispiel für eine speicherzugriffsoptimierte Programmierung:

    Verschiebung der Indexgrenzen in Datenfeldern bei der statischen Deklaration

    Möchte man das erste Element eines Datenfeldes nicht mit dem Index 1 beginnen lassen, so kann man sich die (statische) Felddeklaration entsprechend anpassen:
         real, dimension(0:2) :: x 
    
    würde das Feld x mit den Komponenten x(0), x(1) und x(2) definieren.

    Einfaches Beispiel:

    Es lassen sich auch negative ganze Zahlen als Indexgrenzen angeben.

    Bei der Angabe der Indexgrenzen muss stets der als untere Grenze angegebene Wert kleiner als die Zahl für die Obergrenze sein.

    Beispiel:

    Initialisierung von Datenfeldern (engl. arrays) mit Werten (statisch)

    Bei der statischen Deklaration von Datenfeldern ist es in Fortran 90/95 möglich, durch eine direkte Zuweisung alle Komponenten mit dem angegebenen Wert vorzubelegen. Beispielsweise wird mit
       real, dimension(10) :: v = 0.0
    
    jede der 10 Komponenten des eindimensionalen Datenfelds (Vektor) v(0) v(1),..., v(10) auf den Wert 0.0 gesetzt.

    Will man ein Datenfeld bei der Initialisierung mit verschiedenen Werten vorbelegen, kann man diese durch einen Datenfeld-Konstruktor angeben. Beispiel:

       real, dimension(3) :: w = (/ 1.1, 2.2, 3.3 /)
    
    setzt w(1) auf den Wert 1.1, w(2) auf 2.2 und w(3) auf 3.3.

    Dieses Beispiel der Wertzuweisung mittels eines Array-Konstruktors lässt sich noch um den Fall mit impliziten Berechnungsvorschriften bei der Deklaration ergänzen. Innerhalb des Konstruktors kann man verschachtelte Schleifen einbauen und so raffinierte Vorbelegungen eines Datenfeldes in Abhängigkeit von den Indizes kreieren.

    Angenommen, es soll die Sequenz

      1.  2.  6.  4.  5.  12.  7. 8. 18. 10. 11. 24.
    
    dem Datenfeld z(1), z(2), ... z(12) zugeordnet werden, so kann man dies in der Form
       integer :: i, j
       real, dimension(12) :: z =  (/ ( (1.0*i+3.0*(j-1),i=1,2), &
                                         6.0*j,j=1,4) /)
    
    tun.

    Demo-Programm zum Ausprobieren und Weiterknobeln:

    Array-Konstruktoren lassen sich bei der Datentyp-Deklaration auch einsetzen, um mehrdimensionale Felder mit Werten vorzubelegen. In diesem Zusammenhang muss man sich in Erinnerung rufen, wie die Werte mehrdimensionaler Datenfelder im Speicher abgelegt werden. Bei einem zweidimensionalen Datenfeld geschieht dies spaltenorientiert. In dieser speicheräquivalenten Reihenfolge müssen die zu initialisierenden Wertepaare angegeben werden - zusätzlich ist die Funktion reshape und Angabe der gewünschten mehrdimensionalen Form des Datenfeldes notwendig. Angenommen, eine Matrix matrix_1 mit 4 Zeilen und 3 Spalten soll die Vorbelegung

          1.0 2.0 3.0
          1.0 2.0 3.0
          1.0 2.0 3.0
          1.0 2.0 3.0
    
    bekommen, so lautet die zugehörige Fortran 90/95-Zeile zur Initialisierung
    real, dimension(4,3) :: matrix_1 = &
             reshape( (/1.,1.,1.,1.,2.,2.,2.,2.,3.,3.,3.,3./), (/4,3/) )
    
    Das vollständige Demo-Programm finden Sie hier:

    Die auf die obigen Arten bei der Deklaration der Datenfelder zugewiesenen Werte lassen sich jederzeit innerhalb des Anweisungsteils der Programms wieder verändern. Man kann auf einzelne Komponenten eines Arrays zuweisen oder man kann z.B. mit Hilfe von do-Schleifen und Schleifenindices das gesamte Datenfeld oder Teilbereiche durchlaufen, um die dort gespeicherten Werte auszugeben, weiterzuverarbeiten oder zu verändern.

    do-Schleifen lassen sich auch nutzen, um Datenfelder erst nach dem Deklarationsteil im Anweisungsteil mit Werten vorzubelegen, wie dies z.B. in

    geschehen ist.

    Zugriff auf einzelne Komponenten eines Datenfeldes

    Wie wir schon mehrmals gesehen haben, lässt sich auf eine einzelne Komponente eines Datenfeldes durch die Angabe des Namens und des entsprechenden Indizes innerhalb des Anweisungsteils jederzeit zugreifen.

    Eine Gefahr bei dem Zugriff auf Datenfelder besteht darin, dass manche Compiler nicht selbstständig auf den Indexbereich des Datenfeldes achten. Auch unser f90-Compiler tut dies erst, wenn man explizit statt mit f90 mit f90 -C das Programm übersetzt. Erst mit dem zusätzlichen Compiler-Schalter -C wird bei der Übersetzung zusätzlicher Code generiert, der bei der Programmausführung bei der Überschreitung des Indexbereichs zu einer Fehlermeldung führt.

    Programmbeispiel, in dem auf ein Element ausserhalb des Index-Bereichs zugegriffen wird:

    Zugriff auf Datenfelder als Ganzes (Fortran 90/95)

    Will man nicht nur einen einzelnen Wert in einem Datenfeld verändern, sondern mathematische Operationen nach einem vorgegebenen Schema an allen Komponenten des Datenfeldes vornehmen, so geht dies jederzeit über do-Schleifen.

    Das folgende ausführlichere Beispielprogramm zeigt diese Möglichkeiten der älteren Fortran 77-Syntax auf und ergänzt diese mit einigen neueren Möglichkeiten der Verarbeitung von Datenfeldern als Ganzem aus Fortran 90/95:

    Datenfelder der gleichen Ausdehnungen (gleiche Dimensionen, unabhängig vom Indexbereich) lassen sich in Fortran 90/95 paarweise addieren, subtrahieren, multiplizieren und dividieren.

    Es lassen sich auf ein Datenfeld als Ganzes die in Fortran 90/95 intrinsisch eingebauten Funktionen anwenden. Diese mathematischen Funktionen lassen sich nur durch die Angabe des Namens des Datenfeldes auf alle Komponenten anwenden - ohne, dass wie dies noch in Fortran 77 notwendig war, do-Schleifen zum Durchlaufen der Felder über Indizes aussen herum programmiert zu werden brauchen.

    Zugriff auf Untermengen mehrdimensionaler Datenfelder und in Fortran 90/95 enthaltene Matrixoperationen

    In Fortran 90/95 sind viele Erweiterungen zu Fortran 77 enthalten. Insbesondere bei der Umsetzung von Fragestellungen der numerischen Mathematik in Lösungswege, welche sich von mathematischen Hintergrund her mittels Matrizen und Vektoren formulieren lassen, bietet Fortran 90/95 eine Vielzahl von zusätzlichen nützlichen Funktionen im Vergleich zu Fortran 77 und anderen algorithmisch orientierten Programmiersprachen wie z.B. C und Pascal.

    Unter anderem werden folgende Fortran 90/95 spezifischen Features angeboten und lassen sich in der Praxis hervorragend einsetzen:

    Übersichtstabelle über zulässige Operationen auf Datenfeldern als Ganzem (Fortran 90/95)

    Die Befehle maxloc, minloc, maxval, minval, sum und product erlauben es, hinter dem Datenfeldnamen mit einem Komma getrennt zusätzlich ein zweites Datenfeld mit exakt gleicher Dimensionierung anzugeben, dessen Komponenten vom Datentyp logical sein müssen. Man spricht von einer sogenannten Maske, welche vor dem Ausführen der gewünschten Operation über das Datenfeld gelegt wird, so dass nur noch diejenigen Komponenten berücksichtigt werden, bei denen sich an der korrespondierenden Stelle der Maske der Wert .TRUE. befindet.

    Beispielprogramm zu logischen Masken: matrix_demos.f90, Bildschirmausgabe: matrix_demos.erg

    Formatierte Ausgabe von Datenfeldern

    Will man Datenfelder formatiert und nicht listengesteuert ausgeben und ist die Anzahl der pro Zeile auszugebenden Komponenten bekannt, so kann man den entsprechenden Formatbeschreiber direkt angeben. Siehe z.B. vektor.f90.

    Kann sich jedoch die pro Zeile auszugebende Anzahl an Werten ändern, so empfiehlt es sich, die Formatbeschreiberkette mittels Zeichenkettenverarbeitung zusammenzubauen. Sind pro Zeile z.B. n Werte, wobei n zwischen 1 und maximal 9 liegen darf, formatiert zu schreiben, kann man mittels der Funktionen iachar und achar die Zahl n in ihr korrespondierendes character-Zeichen umwandeln: vektor1.f90

    Sollen auch größere Werte von n zugelassen werden, z.B. n zwischen 1 und 999999, so kann man sich auch hier mittels Zeichenkettenverarbeitung einen passenden Formatbeschreiber konstruieren: vektor2.f90 Von zentraler Bedeutung bei der Erstellung des Formatbeschreibers für diesen allgemeineren Fall ist, dass mittels eines internal writes der Zahlenwert von n in die entsprechende Zeichenkette fb1 umgewandelt wird

        write(fb1,'(I6)') n
    
    Um evtl. noch vorhandene Leerzeichen aus der Zeichenkette fb1 zu entfernen, wird trim(adjustl(fb1)) eingesetzt, bevor dieser Teilstring mit den restlichen Teilstrings zur Formatbeschreiberkette zusammengesetzt wird.

    Das where-Konstrukt (Fortran 90/95)

    Soll auf ein Datenfeld als Ganzem zugegriffen werden, aber dabei bestimmte Umformungen oder mathematische Funktionen nur durchgeführt werden, wenn bestimmte Vorbedingungen erfüllt sind, müsste man in Fortran 77 neben den do-Schleifen zum Durchlaufen der Indexbereiche der Datenfelder zusätzlich if-Abfragen einbauen. Fortran 90/95 bietet hier die komfortable Lösung über ein where-Konstrukt.

    Beispiel: Von allen Komponenten einer Matrix soll der natürliche Logarithmus gebildet werden. Dies ist natürlich nur möglich, solange die Komponenten einen Wert größer als Null aufweisen, denn für 0 und negative Werte ist der Logarithmus nicht definiert. Das Programm soll bei Komponentenwerten kleiner oder gleich 0 der neue Wert auf -99999. gesetzt werden, von den Komponenten größer als Null wird wunschgemäß der natürliche Logarithmus berechnet.

    Das where-Konstrukt in Fortran 90 hat die Syntax

    where ( < Zugriffsbedingung auf ein Datenfeld > )  
      < Name eines Datenfeldes > = < arithmetischer Ausdruecke mit dem Datenfeldnamen >
    else where 
      Alternativen, wenn die Zugriffsmaskenbedingung bei
      einzelnen Komponenten nicht erfuellt ist 
    end where
    
    Das where-Konstrukt lässt sich analog zu den if-Konstrukten und den do-Schleifen mit einem Namen versehen (benanntes where-Konstrukt)
    <Name des where-Konstrukts >: where (...)
              ...
            else where <Name des where-Konstrukts>
              ...
    end where <Name des where-Konstrukts>
    
    und ähnlich der if-Konstruktion lässt sich bei einer nicht notwendigen Alternative und einer einzigen notwendigen Anweisen die Konstruktion in einen Einzeiler verkürzen:
    where ( < Zugriffsmaske auf ein Datenfeld > ) Anweisung 
    
    Fortran 95 bietet die erweiterte Möglichkeit in Analogie zu dem if then - else if - else if ... - else - end if-Konstrukt, weitere else where mit anderen Bedingungen vor dem obigen else where dazwischenzuschalten, so dass immer eine der angegebenen Anweisungsblöcke auf die Komponenten des Datenfeldes angewandt werden, je nachdem, welche der Bedingungen wahr ist.

    Ein wichtiger Hinweis zum Umgang mit Datenfeldern in Fortran 90/95

    Was beim Betrachten der Beispielprogramme auffällt, ist, dass die Fortran 90/95 - Erweiterungen im Umgang mit mehrdimensionalen Datenfeldern auf die explizite Programmierung von Schleifen zum Durchlaufen der Indexbereiche, wie sie in FORTRAN 77 notwendig sind, nicht mehr benötigen.

    Als Weiterentwicklung von FORTRAN 77 sind die Befehle von Fortran 90/95 im Umgang mit gesamten Datenfeldern so konstruiert, dass sie dem Compiler auf Mehr-Prozessor-Maschinen und Vektorrechnern die Möglichkeit geben, Programmteile zu parallelisieren und zu vektorisieren und damit Ihrem Programmcode zu optimieren. Verwenden Sie deshalb in Ihren Programmen, immer, wenn dies möglich ist, die Fortran 90/95-Befehle, die es erlauben, am Stück gesamte Datenfeldern oder Unterdatenfelder mit einem Befehl zu verarbeiten (z.B. kann man mathematische Funktionen und Ausdrücke auf ein gesamtes Datenfeld oder Unterdatenfeld anwenden und die speziellen Befehle für Datenfelder wie z.B. matmul, dot_product, transpose einsetzen). Dadurch erlauben Sie dem Compiler eine an die jeweilige Rechnerarchitektur angepasste Optimierung. Zusätzlich gibt es natürlich rechner- und compilerspezifische Optimierungsmöglichkeiten, die sie mit Hilfe der Manuals evtl. weiter ausschöpfen können, wenn Sie Ihren Code auf einem bestimmten Rechner laufen lassen wollen.

    Untersuchungsfunktionen (engl. inquiry functions) für Datenfelder

    Dynamische Speicherallokierung für Datenfelder (Fortran 90/95)

    Falls bei Umsetzung einer Programmieraufgabe nicht bekannt ist, wieviel Elemente ein Datenfeld umfassen wird, so bietet Fortran 90/95 (noch nicht Fortran 77) die Möglichkeit, den für ein Datenfeld benötigten Speicher dynamisch zu verwalten, d.h. den Speicher erst im Anweisungsteil für die Datenfelder zu reservieren, sobald dieser benötigt wird (dynamische Speicherallokierung) und den dynamisch belegten Speicher wieder freizugeben (deallokieren) sobald dieser nicht mehr benötigt wird. Auf diese Art und Weise können Sie den Speicher Ihres Rechners optimal nutzen, es treten keine Lücken im Speicher auf wie dies evtl. bei einer statischen (im Deklarationsteil) überdimensionierten Speicherreservierung der Fall sein würde.

    Mit der dynamischen Speicherallokierung haben Sie die Möglichkeit bei Bedarf mit der Anzahl der zu verarbeitenden Komponenten flexibel bis an die Grenzen des Hauptspeichervolumens zu gehen.

Zurück zur Vorlesungsseite


Heidrun.Kolinsky@uni-bayreuth.de
(Dr. Heidrun Kolinsky, Rechenzentrum der Universität Bayreuth, Gebäude NW2, Raum 159, Universitätsstraße 30, D-95440 Bayreuth, Tel. 0921/55-2687)