Institut für Mathematik Universität Augsburg Logo des Instituts für Mathematik
Klaus Bernt
Klaus BERNT
Lehrstuhl für Rechnerorientierte Statistik und Datenanalyse
 

 

Tipps und Tricks für Programmierer

Programmier-Technik

  1. Das Programm soll einen Hinweis auf die Autoren enthalten (vor allem: Name; im Übungs-Betrieb auch Matrikelnummer).
     
  2. Die Variablennamen sollten denen im 'realen Leben' (hier also: denen in der Aufgabe) entsprechen: dies vermeidet Verwirrung.
     
  3. Standard-Namen: Wenn Sie eine Ihrer Variablen (oder Unterprogramme) genauso benennen, wie bereits ein Schlüsselwort oder eine Standard-Routine heisst, laufen Sie Gefahr, daß nicht Ihre Routine, sondern die aus der Standard-Bibliothek verwendet wird. Vermeiden Sie daher solche Namen! Insbesondere werden die Reaktionen von scilab sehr eigenartig.
     
  4. Einrückung: Dem Compiler ist das Layout der Datei vollkommen egal, einem Korrektor aber nicht. Das betrifft auch und vor allem Sie selbst, wenn Sie sich das Programm 2 Monate später noch einmal ansehen! Durch eine sinnvolle Einrück-Strategie wird die Programm-Struktur (Verzweigungen, Schleifen, Unterprogramme) deutlich dargestellt, was das Auffinden der gesuchten Programm-Zeile erleichtert oder überhaupt erst ermöglicht.
     
  5. Viele Sprachen verwenden Klammern (in C oder C++: {...}), um Programm-Blöcke zu markieren. Oft ist es dabei erlaubt, die Klammern wegzulassen, wenn nur eine einzige Anweisung den Block bildet. Insbesondere bei Abfragen und Schleifen gilt die dringende Empfehlung: lassen Sie die Klammern stehen! Grund: irgendwann werden Sie eine zweite Anweisung hinzufügen und vergessen, die Klammern zu setzen; die Folgen können schwerwiegend sein, und das Problem äußerst schwierig zu finden.
     
     
  6. Variablen-Initialisierung: In C und vielen anderen Sprachen sollen (müssen!) alle Werte definiert werden, bevor man sie in einer Rechnung verwendet. Es hängt sonst vom 'Runtime-System' (der speziellen Compiler-Version auf dem speziellen Betriebssystem) ab, was dort im Speicher steht. Manche Systeme initialisieren selbst (meist auf 0 bzw 0.0), andere stellen den Wert 'undefined' ein (z.B. SUN/Solaris), wieder andere tun garnichts und verwenden einfach den Speicherinhalt, den ein früheres Programm an dieser Hardware-Stelle hinterlassen hat; auch ein Verfahren, Zufallszahlen zu produzieren...
     
  7. Indexierung: einige Sprachen (z.B. C und C++) beginnen ihre Feld-Indexierung bei 0. Wenn Ihr Algorithmus die Indizes bei 1 beginnen läßt, sollten Sie den Index-Shift nicht im Code des Algorithmus unterbringen, sondern bei Eingabe/Initialisierung und Ausgabe. Grund: diese Phasen werden meist nur selten durchlaufen, während der Algorithmus-Code sehr oft durchlaufen wird. Außerdem erleichtert dies die Fehlersuche.
     
  8. Endlos-Schleifen sollten vermieden werden. Meist schafft eine Eingabe mit anschließender Abfrage, ob der Wert sinnvoll ist (z.B. 'n>0') die Gelegenheit zum kontrollierten Abbruch (in C: exit(0);).
     
  9. Unterprogramme: Oft sind Programm-Teile wiederholt verwendbar. Diese Teile sollten in Unterprogramme gesteckt werden. Dabei ist die Verwendung von "globalen Variablen" zu vermeiden; Stichwort: Seiteneffekte! Große Strukturen (z.B. Felder) sind als Referenz-Parameter zu übergeben, ebenso Parameter, deren Wert während des Unterprogramms verändert werden soll.
     
  10. Effizienz: Versuchen Sie, Operator- und Unterprogramm-Aufrufe auf ein Minimum zu reduzieren. Wenn Werte viel Zeit für die Berechnung brauchen, aber mehrfach verwendet werden sollen, sollten sie zwischengespeichert werden. Ebenso, wenn sich dieser 'teure' Wert im Laufe einer Schleife nicht ändert (dann vorher berechnen!).
    Standard-Funktionen sollten nur für Standard-Aufgaben verwendet werden; für Spezial-Aufgaben gibt es oft eine Zeit sparende Spezial-Funktion (auch wenn Sie diese selbst programmieren müssen). Der Aufruf einer Standard-Matrix-Inversion für eine Diagonalmatrix ist z.B. reine Verschwendung.
    Ferner ist auf eine günstige Reihenfolge der Operatoren zu achten: lassen sich gemeinsame Faktoren ausklammern?
     
  11. Wenn Sie ein Programm bei jemand anders abzuliefern haben, prüfen Sie genau das Programm, das Sie auch abliefern. "Kosmetische Änderungen" kurz vor der Abgabe erzeugen oft Fehler!
     
  12. Last, but not least: Selbstkritik! Prüfen Sie Ihre Lösung, ob sie stimmen kann.

 

Spezielle Aufgaben und ihre Lösung in C

  1. Verwendung von Kommandozeilen-Parametern
    Der folgende Code liefert ein Muster, wie Parameter auf der Kommandozeile verwertet werden können:
    void  main ( int argc, char** argv )
    {
      int  n, i ;
      double  d ;
      for  ( i = 1 ;   i < argc ;   i++ )
      {
        argv++ ;
        n = atoi ( *argv ) ;
        // analog:  d = atof ( *argv ) ;
        .
        .
        .
      }
    }
    
  2. Felder und Matrizen
    Die Konstruktion
    int MAXDIM = 100 ;
    double  array[MAXDIM] ;
    double  matrix[MAXDIM][MAXDIM] ;
    ...
    
    mag zwar funktionieren, hat aber den entscheidenden Nachteil, daß sie die Größe der Felder und Matrizen einschränkt. Wer mehr Platz braucht, muß das Programm ändern und neu übersetzen: lästig! Besser ist eine Konstruktion mit dynamischer Speicher-Beschaffung:
    int  n ;
    double  *array ;
    double  **matrix ;
    scanf ( "%d",&n ) ;
    int  i, j ;
    array = malloc ( n * sizeof ( double ) ) ;
    matrix = malloc ( n * sizeof ( double* ) ) ;
    for  ( i = 0 ;   i < n ;   i++ )
    {
      array[i] = 0.0 ;
      matrix[i] = malloc ( n * sizeof ( double ) ) ;
      for  ( j = 0 ;   j < n ;   j++ )
      {
        matrix[i][j] = 0.0 ;
      }
    }
    
    Die Funktion calloc(...) würde sogar die Vorbelegung zu 0.0 übernehmen. Die matrix in diesem Beispiel könnte auch verschieden lange Zeilen erhalten (z.B. für die effiziente Speicherung einer Dreiecksmatrix); hier ist sie quadratisch angelegt.

    Außerdem zeigt das Beispiel, wie man problemlos die Standard-C-Indexierung (beginnend bei 0) codieren kann. Wenn man ein solches Feld oder eine solche Matrix als Parameter einer Funktion verwenden will, muß der Typ des Parameters als double* bzw. double** verlangt werden. Im Funktions-Körper wird die Parameter-Variable wie jedes andere Feld behandelt. Einen besonderen Vorteil bietet diese Variante beim Tauschen von ganzen Zeilen der Matrix:

    double*  tmp = matrix[i] ;
    matrix[i] = matrix[j] ;
    matrix[j] = tmp ;
    
    tauscht die Zeilen i und j. Dabei werden lediglich zwei Adressen (die der Zeilenanfänge) vertauscht, nicht aber deren n Einträge; eine sehr schnelle Aktion.

  3. Function als Parameter einer Subroutine
    // Der Typ 'functional' benötigt die exakte Anzahl an Parametern
    // und deren Typen sowie den Ergebnis-Typ:
    typedef  double (*functional)( double ) ;
    
    // Einige Beispiele für 'functional'
    double  g_lin  ( double t )    { return ( t ) ; }
    double  g_sqr  ( double t )    { return ( t*t ) ; }
    double  g_cub  ( double t )    { return ( t*t*t ) ; }
    double  g_bisqr  ( double t )  { return ( t*t*t*t ) ; }
    
    functional  g[] = { g_lin, g_sqr, g_cub, g_bisqr } ;
    
    // Eine Funktion, die ein 'functional' als Parameter fordert:
    double  integrate_simple ( functional f, double a, double b )
    {
      return ( 0.5 * ( f(b) + f(a) ) / ( b - a ) ) ;
    }
    
    // Aufrufe der Funktion:
    double  ff = integrate_simple ( g[1], 0.0,1.0 ) ;
    double  gg = integrate_simple ( g_cub,0.0,1.0 ) ;
    
  4. Ausgaben in Datei
    #include <stdio.h>
    FILE*  ouf = fopen ( "dateiname","w" ) ;
    if  ( ouf != NULL )
    {
      fprintf ( ouf,format [, values] ) ;
      fclose ( ouf ) ;
    }
    

 

Spezielle Aufgaben und ihre Lösung in scilab

  1. Function als Parameter einer Subroutine
    Der Name der Function wird wie ein Skalar-Parameter übergeben. Im Korpus der Subroutine wird der lokale Parameter als Funktion (genauso wie eine Standard-Funktion) verwendet. Sie sollten jedoch darauf achten, daß Anzahl und Typ der Parameter beim Aufruf im Korpus zum aktuellen Subroutine-Parameter passen!

  2. Rechenzeit
      timer ( ) ;
      operation ;  //  oder mehrere
      tims(i) = timer ( ) ;
    
    Diese Folge speichert im Feld tims die Rechenzeit, die für die Operationen verbraucht wurde. Es empfielt sich, am Anfang des Programms eine zusätzliche Zeitmessung einzufügen; die Initialisierung der Stoppuhr wird bei der ersten Messung mitgezählt!

  3. Konvergenz- und andere Ordnungen
    Wenn Sie eine Untersuchung über die Konvergenzordnung oder den Aufwand eines Algorithmus brauchen, benötigen Sie zunächst Vergleichswerte wie z.B. Fehler bei verschiedenen Diskretisierungen oder die Rechenzeiten dabei. Diese Werte speichern Sie in einem Feld:
      for  i = 1:p
        N = 2 ^ i ;
        timer ( ) ;
        operationen(N) ;
        tims(i) = timer ( ) ;
      end
    
    Achten Sie darauf, daß Sie den für den Aufwand wesentlichen Parameter (hier N) stets um den gleichen Faktor erhöhen (hier 2). Dieser Faktor bildet die Logarithmen-Basis in der folgenden Formel; darin sollte der (erwartet) größere Wert im Zähler stehen. Bei Rechenzeiten ist dies der Wert zu größerem N, bei Fehler-Werten ist es hoffentlich umgekehrt. In der Ausgabe sind mehr als zwei Nachkommastellen unnötig, da es sich nur um grobe Näherungen handelt. Sie sind meist durch Terme niedrigerer Ordnung oder Nebeneffekte technischen Ursprungs (z.B. Swapping oder Time-Slice) verunreinigt.
      for  i = 2:p
        if  ( ( tims(i) ~= 0 ) & ( tims(i-1) ~= 0 ) ) then
          ko = log2 ( abs(tims(i)/tims(i-1)) ) ;
          printf ( "\t%4.2g", ko ) ;
        else
          printf ( "\t----" ) ;
        end
      end
    

  4. Ausgaben in Datei
    [ fd,err ] = mopen ( 'dateiname','w' ) ;
    if  ( fd ~= 0 )
    {
      fprintf ( fd,'formatstring' [, values] ) ;
      mclose ( fd ) ;
    }
    
    Der formatstring sieht so aus wie in C, aber Achtung: das Format '%lf' ist in C erlaubt (für double-Werte) und erzeugt in scilab einen Fehler Nr.998 mit unverständlicher Meldung. Die Formate '%g' und '%f' hingegen funktionieren.

 

Spezielle Aufgaben und ihre Lösung in Java

Eine Dokumentation der Java-Klassen (Version 1.4) befindet sich auf dem Mathematik-Web-Server.
  1. Rechenzeit
    Dem Autor ist keine Methode bekannt, die in Java eine Rechenzeit ausgibt. Diese Nicht-Existenz eines in anderen Sprachen als Standard angebotenen Service' könnte daran liegen, daß Java viele Programm-Teile in 'Threads' zerlegt: separate Prozesse, die vom Betriebssystem einzeln abgerechnet werden, und deren Verbrauch daher möglicherweise zum Zeitpunkt der Abfrage nicht mehr bestimmt werden kann, da die Unter-Prozesse bereits beendet sind. Die im folgenden genannten Methode bestimmt lediglich eine Verweildauer; sie ist eine Uhr, die ständig tickt und Millisekunden anzeigt. Wenn Ihr Rechner auch von anderen Nutzern oder Programmen beschäftigt wird (oder auch vom Betriebssystem!), wird diese Zeit mitgezählt. Gerade kurze Zeiten sind daher mit besonderer Vorsicht zu geniessen; sie sagen wenig bis garnichts über die Rechenzeit aus!
      tix = System.currentTimeMillis ( ) ;
      operation ;  //  oder mehrere
      tims(i) = System.currentTimeMillis ( ) - tix ;
    
  2. Ausgaben in Datei
    import java.io.* ;
    
    BufferedWriter  bw = null ;
    try
    {
      bw = new BufferedWriter ( new FileWriter ( "dateiname" ) ) ;
      bw.write ( ... ) ;
      bw.close ( ) ;
    }
    catch  ( IOException iox )
    {
      iox.printStackTrace ( ) ;
    }
    

 

Beliebte Fehler

  1. "=" versus "=="
    Die Versuchung ist groß, in einem Vergleich den Operator = zu verwenden. Das ist kein syntaktischer Fehler, denn C nimmt als Wert einer Zuweisung (darum handelt es sich) den Wert der rechten Seite an, und jeder Wert ungleich 0 (bzw. 0.0) wird, wenn ein Wahrheitswert erwartet wird, als wahr interpretiert; nur die Zahl 0 (bzw. 0.0) entspricht dem Wahrheitswert false.
    Anders ausgedrückt: die Abfrage if  ( v=1.0 ) wird immer in den ersten Zweig (then) springen, niemals in den zweiten (else), egal, welchen Wert die Variable v zuvor hatte (danach hat sie den Wert 1.0).
  2. Tippfehler im Variablennamen
    In C und Java werden solche Fehler (meist) vom Compiler entdeckt. Wenn der vertippte Name jedoch an anderer Stelle im Programm tatsächlich verwendet wird, sind solche Fehler extrem schwer zu finden. mathlab/scilab kennen keine Variablen-Deklaration, weshalb das Problem hier stets latent vorhanden ist. Funktionen wie who, whos und who_user (Liste der bekannten Variablennamen) können helfen, sie zu finden.
  3. Initialisierung von Variablen
    Generell sollte man jede Variable nach der Deklaration mit einem Initial-Wert belegen. Grund dafür ist, daß sich verschiedene Laufzeitsysteme unterschiedlich verhalten, wenn eine nicht explizit initialisierte Variable um einen Wert erhöht oder verringert wird (z.B. v+=wert;). Manche Systeme initialisieren bei der Deklaration selbst (dann meist auf den Wert 0 bzw. 0.0), andere besetzen mit undefined vor, wieder andere tun garnichts. Speziell die letzte Variante führt dazu, daß dort eine Zahl steht, die von dem Programm abhängt, daß diese Speicherzelle zuvor benutzt hat - eine Zufallszahl also. Wenn ein Programm ohne Initialisierung auf dem einen Rechner funktioniert und dann auf einen Rechner eines anderen Typs transferiert wird, kann es leicht sein, daß es dort nicht mehr funktioniert oder nur noch Daten-Müll berechnet.
  4. globale Variablen
    verringern zwar den Aufwand bei der Parameter-Versorgung von Unterprogrammen, haben aber die entscheidende Gefahr von sog. Seiteneffekten: mehrere Unterprogramme verwenden die gleiche globale Variable, das eine verändert ihren Wert, und ein anderes, das sich auf den ursprünglichen Wert verläßt, läuft deswegen ins Nirwana. Die 'freundlichste' Art, als Programm darauf zu reagieren, ist der sofortige Absturz; wenn das Programm mit diesem Fehler weiter läuft, entstehen Folgefehler, deren Ursache immer schwerer zu lokalisieren ist.
  5. Vektoren als Parameter einer Subroutine
    Einige C-Compiler verstehen einen Funktionsaufruf der Form f(double[] v) als einen Call-by-value. Das bedeutet, daß innerhalb des Unterprogramms eine lokale Kopie des Vektors v angelegt wird, deren Werte dort auch verändert werden können. Nach Beendigung des Unterprogramms stellt man aber fest, daß sich in dem Parameter-Vektor nichts verändert hat - weil ja nur mit einer Kopie gearbeitet wurde.
    Besser ist ein Call-by-reference in der Art f(double* v). Dabei wird nicht das gesamte Feld als Parameter übergeben, sondern nur seine Startadresse. Alle Modifikationen des Vektors im Unterprogramm finden im angegebenen Speicherbereich statt, bleiben also auch nach Abarbeitung der Routine erhalten. Auch für Vektoren, die als reine Eingabe-Parameter fungieren sollen, ist die Referenz-Variante besser, da sie nur 4 Byte (die Adresse) kopieren muß gegenüber n×8 Byte bei der Value-Variante.
    Analoge Aussagen gelten für Matrizen; diese sollten mit dem Typ double** an die Routine übergeben werden.
  6. integer-Division liefert den Typ int
    Wer den Typ int verwendet, obwohl float oder double angemessen wäre, kann bei Addition, Subtraktion und Multiplikation Glück haben: diese Operationen liefern, wenn die Operanden im geeigneten Bereich liegen, wieder eine wohldefinierte Ganzzahl. Wird jedoch eine Division von zwei Ganzzahlen einem double zugewiesen, so wird zuerst die Division nach int-Regeln ausgeführt (Division ohne Rest), deren Ergebnis in ein double konvertiert und dieses zugewiesen. Meist ist dies unerwünscht; dann muß einer der Operanden vor der Division Typ-konvertiert werden.
  7. "abs()" versus "fabs()"
    Die C-Standard-Funktion abs() benötigt einen int-Parameter und liefert einen int-Wert; analog ist fabs() für double definiert. Wenn die falsche Funktion verwendet wird (z.B. abs(double)), kann es, abhängig vom Laufzeitsystem, zu absonderlichen Fehl-Auswertungen kommen.
  8. "l" versus "1", "o,O" versus "0"
    Da man die Zeichen l, o und O leicht mit den Ziffern 1 bzw. 0 verwechseln kann, sollte man sie nicht als einbuchstabige Variablennamen verwenden.
  9. Selbstvertrauen
    ist eine gute Sache; dennoch sollten Sie Ihre Programme testen, bevor Sie sie verkaufen (oder einem Korrektor übergeben). Dabei ist auch mittels Test-Daten zu prüfen, ob das Programm die Ergebnisse liefert, die theoretisch herauskommen sollten. Erfüllt das 'Resultat' die Gleichungen, die es erfüllen soll?

 

Konstruktion von Test-Daten

  1. L-R-Zerlegung
    Matrizen L (normierte untere Dreiecksmatrix) und R (obere Dreiecksmatrix) vorgeben, daraus A=L*R berechnen. Mit diesem A das Verfahren testen.
    'normiert' heisst: Einsen in der Diagonale.
  2. Reguläre Matrix
    Geben Sie sich L (untere Dreiecksmatrix, nicht notwendig normiert) und R (obere Dreiecksmatrix) vor, beide mit Nicht-Null-Diagonalelementen (also regulär). A=L*R ist dann auch regulär, aber im allgemeinen voll besetzt.
    Die Determinante von A können Sie kontrollieren durch die Determinanten der beiden Faktor-Matrizen, die widerum als Produkte der jeweiligen Diagonalelemente berechnet werden.
  3. Lineares Gleichungssystem
    Um zu prüfen, ob ein LGS Ax=b korrekt gelöst wird, braucht man Daten für A (reguläre Matrix; siehe oben) und b. Man wählt sich einen Vektor x nach Belieben (z.B. als Einsen-Vektor), berechnet A*x und übergibt A und A*x (als b) an das Verfahren.

 

Abbruchkriterien für Iterationen

  1. maximale Anzahl der Iterationen
    sollte immer verwendet werden, um bei Fehlern im Programm oder bei zu langsamer Konvergenz des Verfahrens ein Ende zu finden.
  2. Fehler-Abschätzung 'a posteriori'
    Wenn es eine solche gibt, sollte sie verwendet werden. Ungeeignet sind absolute Werte, denn Sie kennen den Größenbereich der Parameter der nächsten Aufgabe nicht. Beispiel: hat die exakte Lösung der Aufgabe Komponenten im Bereich 1e-10, so ist ein absoluter Fehler von 1e-7 nicht akzeptabel; liegen die exakten Ergebnisse bei 1e+10, so kann man eine so kleine Abweichung nicht erreichen. In beiden Fällen ist ein absoluter Fehler von 1e-7 mithin unsinnig.
    Relative Bedingungen, z.B. 'Norm des Fehlers der Iterierten' kleiner als 1e-6 * 'Norm der Parameter', sind besser geeignet. Welchen relativen Fehler Sie zulassen, hängt von der Aufgabe, Ihren Präzisionswünschen und dem verwendeten Datentyp (float oder double) ab: bei den 7 signifikanten Stellen eines float-Wertes wäre ein relativer Fehler von 1e-9 nicht erreichbar (gleichbedeutend mit 0.0), bei double-Rechnung hingegen problemlos.
  3. Fehler-Abschätzung 'a priori'
    Daraus kann oft eine maximale Iterationszahl berechnet werden.

 

Klaus Bernt, 22.07.2015

to be continued...

[Uni Augsburg]  [Math.-Nat. Tech. Fakultät]  [Institut für Mathematik]  [Lehrstuhl]  [Print-Version]  [E-Mail]  aktualisiert am: 22.07.2015;  © Uni Augsburg