Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

 << zurück
Linux-UNIX-Programmierung von Jürgen Wolf
Das umfassende Handbuch – 2., aktualisierte und erweiterte Auflage 2006
Buch: Linux-UNIX-Programmierung

Linux-UNIX-Programmierung
1216 S., mit CD, 49,90 Euro
Rheinwerk Computing
ISBN 3-89842-749-8
gp Kapitel 17 Werkzeuge für Programmierer
  gp 17.1 Make
    gp 17.1.1 Erzeugen eines Makefiles
    gp 17.1.2 Variablen, Makros und Abkürzungen
    gp 17.1.3 Implizite Regeln
    gp 17.1.4 Musterregeln
    gp 17.1.5 make zur Installation verwenden
    gp 17.1.6 make-Optionen
    gp 17.1.7 Ausblick
  gp 17.2 Bibliotheken erstellen
    gp 17.2.1 Statische Bibliotheken erstellen
    gp 17.2.2 Dynamische Bibliotheken (Shared Libraries) erstellen
    gp 17.2.3 Dynamisches Nachladen von Bibliotheken
  gp 17.3 RPM
    gp 17.3.1 Verzeichnisse, die RPM benötigt
    gp 17.3.2 Ein eigenes RPM-Paket erstellen
    gp 17.3.3 Sources
    gp 17.3.4 Die Spec-Datei
    gp 17.3.5 Paket erstellen
    gp 17.3.6 Das Paket installieren
  gp 17.4 RCS und CVS
    gp 17.4.1 Software-Configuration-Management-Systeme (SCM)
    gp 17.4.2 RCS
    gp 17.4.3 CVS
  gp 17.5 Zeitmessung von Programmen
    gp 17.5.1 Einfache Zeitmessung mit TIME – Laufzeit von Prozessen
    gp 17.5.2 Profiling mit GPROF – Laufzeit von Funktionen
    gp 17.5.3 Analyse mit GCOV
  gp 17.6 Debuggen mit gdb und ddd
  gp 17.7 STRACE – Systemaufrufe verfolgen
  gp 17.8 Memory Leaks und unerlaubte Speicherzugriffe
    gp 17.8.1 efence
    gp 17.8.2 valgrind
  gp 17.9 Ausblick


Rheinwerk Computing

17.5 Zeitmessung von Programmen  downtop

Gerne werden vor der Fertigstellung der Software die einen oder anderen Laufzeitoptimierungen am Quellcode durchgeführt. Ob dieses Tuning des Codes überhaupt Sinn macht, lässt sich mithilfe von Laufzeitanalysen bestimmen. Solche Analysen durchzuführen, ist wesentlich komplizierter als man denkt. Funktionen der <time.h>-Bibliothek können höchstens mit einer einfachen Wanduhr-Analyse mithalten, denn es müssen eigentlich auch die unterschiedlichsten Auslastungen des Systems beachtet werden. Und gerade das macht es schwierig, die Laufzeit von nur einer Anwendung zu ermitteln. Noch ein wenig schwieriger dürfte das Unterfangen sein, die Laufzeit von einzelnen Funktionen bzw. Codeabschnitten zu ermitteln. Zum Glück sind hierfür sehr effektive Tools zur Laufzeit- und Profiling-Analyse entwickelt worden, von denen Sie im folgenden Abschnitt drei Anwendungen etwas näher kennen lernen werden:

gp  TIME oder /usr/time – Allgemeine Laufzeitermittlung von Prozessen
gp  GPROF – Laufzeitermittlung einzelner Funktionen
gp  GCOV – Laufzeitermittlung, wie häufig einzelne Zeilen ausgeführt werden.

Rheinwerk Computing

17.5.1 Einfache Zeitmessung mit TIME – Laufzeit von Prozessedowntop

Für eine einfache Zeitmessung der Laufzeit von einzelnen Prozessen lässt sich TIME verwenden. TIME eignet sich hervorragend, um sich einen groben Überblick über die Laufzeit des Prozesses zu machen. Die allgemeine Laufzeitbestimmung mit TIME wird gerne verwendet, wenn der Quellcode mit eingeschaltetem Optimizer übersetzt wurde. Als einfaches Beispiel sei hier ein Quellcode namens optimize.c verwendet.

$ gcc -o optimize optimize.c
$ time ./optimize
real    0m5.841s
user    0m0.210s
sys     0m0.430s

Hier bekommen Sie von TIME drei verschiedene Zeiten mit der folgenden Bedeutung zurück:

gp  real – Die gesamte verstrichene Ausführungszeit ab Ausführung der Kommandozeileneingabe
gp  user – Die CPU-Zeit für Benutzerfunktionen, genauer die Zeit, welche die Anwendung im User-Level verbracht hat
gp  sys – Die CPU-Zeit für Systemcalls; dies ist die Zeit, welche die Anwendung im Kernel-Level verwendet hat.

Hierzu sei nun derselbe Code mit Optimizer -O3 verwendet:

$ gcc -O3 -o optimize optimize.c
$ time ./optimize
real    0m7.779s
user    0m0.120s
sys     0m0.420s

Sie sehen, dass hiermit die CPU-Zeit für den Benutzerbereich verringert, aber dadurch die gesamte Ausführungszeit der Anwendung erhöht wurde. Leider ist hier keine genaue Aussage zu treffen, was hierbei länger gedauert hat – da es noch mehrere Prozesse auf dem System gibt, die ebenfalls Laufzeit (die CPU) benötigen und somit diese Zeitmessung immer ein wenig verfälschen können. Für eine genauere Zeitanalyse kommen Sie um das Profiling mittels GPROF zur Laufzeitmessung nicht herum.


Rheinwerk Computing

17.5.2 Profiling mit GPROF – Laufzeit von Funktionedowntop

Mit TIME haben Sie gewiss ein einfaches Verfahren für eine Zeitanalyse des gesamten Programms. Aber sobald es sich um das Codetuning einzelner Funktionen handelt, taugt TIME nicht mehr viel. Denn wollen bzw. müssen Sie nur einzelne Funktionen optimieren, bringt Ihnen eine Zeitanalyse des gesamten Programms recht wenig, insbesondere wenn es darum geht, um wie viel schneller denn die einzelnen Funktionen nun ausgeführt wurden. Für solche Aufgaben werden Programmprofile (auch als Leistungsdiagramm bezeichnet) benötigt. Um ein solches Profil zu erzeugen, sind folgende Schritte nötig:

gp  Beim Übersetzen des Quellcodes muss ein Profil erzeugt werden.
gp  Dann wird das speziell mit Profilen übersetzte Programm ausgeführt, woraufhin Sie ein Programmprofil erhalten.
gp  Dieses Programmprofil können Sie letztendlich mit dem Profiler GPROF auswerten.

Ein Profil für gprof erzeugen

Zuerst müssen Sie den Quellcode für ein Profil mit der Option -pg übersetzen:

$ gcc -pg -o fraktal fraktal.c berechnung.c \
  -L/usr/X11R6/lib -lX11  

Die so erzeugte ausführbare Datei ist natürlich erheblich größer als die Anwendung, die ohne die Compiler-Option -pg übersetzt wurde. Damit haben Sie quasi das Programm für die weiteren Schritte der Laufzeitanalyse mit GPROF eingerichtet.

Im nächsten Schritt müssen Sie diese Anwendung ausführen, womit ein Programmprofil mit dem Namen gmon.out erzeugt wird.

$ ./fraktal
$ ls -l gmon*
-rw-r--r--    1 tot   users       429 2004–04–12 13:41 gmon.out

Jetzt, im letzten Schritt, müssen Sie die Daten von gmon.out analysieren. Dazu müssen Sie lediglich GPROF mit dem Programm, das analysiert werden soll, als Argument übergeben. Anschließend gibt GPROF die Analyse auf die Standardausgabe aus, weshalb sich hierbei eine Umleitung in eine Datei hervorragend eignen würde (hier finden Sie die Ausgabe von GPROF in der Textdatei mytest_profil.txt).

$ gprof ./fraktal > mytest_profil.txt

Die nun folgende Ausgabe (oder, falls die Datei umgeleitet wurde, die Datei) besteht aus zwei Teilen:

gp  dem flachen Profil, das auflistet, wie sich die Programmzeit auf die einzelnen Funktionen verteilt
gp  dem Aufrufgraph-Profil, worin sich die aufrufenden und aufgerufenen Funktionen befinden

Das flache Profil

Im flachen Profil finden Sie aufgelistet, wofür die Programmzeit verwendet wurde. Der Zeitaufwand wird dabei für jede Funktion pro Aufruf berechnet. Am flachen Profil erkennen Sie die Funktionen, die am meisten Zeit verbrauchen und die es dann auch wert sind (zugegeben, es gibt auch Funktionen, die eben etwas mehr tun als andere ...), optimiert zu werden. Ein Beispiel des flachen Profils:

Flat profile:
Each sample counts as 0.01 seconds.
 %  cumulative  self             self     total
time  seconds  seconds   calls   s/call   s/call  name
99.90    30.91   30.91  798720     0.00     0.00  farbe
 0.06    30.93    0.02       1     0.02    30.94  fraktal
 0.03    30.94    0.01      52     0.00     0.59  berechne
 0.00    30.94    0.00       3     0.00     0.00  ermittle_shifts
 0.00    30.94    0.00       2     0.00     0.00  push_para
 0.00    30.94    0.00       1     0.00     0.00  init_colors
 0.00    30.94    0.00       1     0.00     0.00  posit

In der Ausgabe des flachen Profils lässt sich gleich erkennen, dass die Funktion farbe() die meiste Zeit (99.90 %) verwendet, weshalb diese Funktion in der Praxis auch ein Kandidat für die Codeoptimierung wäre. Für jede Funktion wird ein Zeileneintrag erstellt und entsprechend der benötigten Ausführungszeit sortiert. Die Zeit, die für die Interaktionen innerhalb von Funktionen verbraucht wird, wird allerdings nicht mitgezählt! Jede Funktion enthält bei dem Eintrag im flachen Profil folgende Informationen:

gp  %time – Anteil in Prozent der gesamten Ausführungszeit des Programms für die Ausführung der Funktion.
gp  cummulative seconds – Gesamtzeit in Sekunden, die für die Ausführung der Funktionen und alle im flachen Profil vorher aufgeführten Funktionen benötigt wurde.
gp  self seconds – Gesamtzeit in Sekunden, die für die Ausführung der Funktionen benötigt wurde, ohne die Zeit, die für die Nachkommen der Funktion verbraucht wurde.
gp  calls – Aufrufe der Funktion während des kompletten Programmablaufs
gp  self ms/call – Durchschnittlicher Zeitaufwand für die Ausführung der Funktion in Millisekunden, der sich aus allen Funktionsaufrufen bildet, ohne die Funktionen, die eventuell aus dieser Funktion aufgerufen werden.
gp  total ms/call – Durchschnittlicher Zeitaufwand für einen Funktionsaufruf mitsamt der weiteren Funktionen, die diese Funktion aufrufen.
gp  name – Name der Funktion

Im Großen und Ganzen kann man zum Programm oben sagen, dass hier eine Codeoptimierung nicht dem Aufwand trotzen kann, den man hierfür aufwenden muss. Rentabel sollte es hierbei werden, wenn eine Funktion mehrere Sekunden benötigt. Um hierbei den Code einer Funktion zu verbessern, haben Sie folgende Möglichkeiten:

gp  Den Algorithmus verbessern, damit der Code schneller abläuft.
gp  Die Routine so umschreiben, dass eine Funktion nicht so häufig aufgerufen werden muss.
gp  Funktionen, die relativ häufig aufgerufen werden, können auch direkt im Code der aufrufenden Funktion eingebaut werden.
gp  Compiler-Optionen zur Optimierung des Codes verwenden

Das Aufrufgraph-Profil

Für eine noch detailliertere Auflistung des Programmprofils lohnt sich ein Blick in das Aufrufgraph-Profil (auch strukturiertes Profil genannt). Hier erhalten Sie noch ein wesentlich genaueres Programmprofil. Hier wird für jede einzelne Funktion angezeigt:

gp  Anteil der Gesamtlaufleistung in Prozent (time).
gp  Aufrufende Funktion, und zwar von welchen Funktionen eine bestimmte Funktion wie oft aufgerufen wurde.
gp  Aufgerufene Funktionen. Hier wird gezeigt, welche Funktion die aktuelle Funktion wiederum wie oft aufgerufen hat.

Um hierbei den Überblick zu bewahren, finden Sie auch einen Indexeintrag zu jeder Funktion in diesem Profil. Betrachten Sie z. B. im anschließenden Aufrufgraph-Profil die erste Funktion mit dem Index [1], können Sie ganz unten im Profil den Namen der Funktion des entsprechenden Indexeintrags herauslesen. Im Beispiel wäre die Funktion mit dem Index [1] fraktal(), mit dem Index [3] berechne() usw. Hierzu das strukturierte Profil des Programmbeispiels fraktal:

Call graph (explanation follows)
granularity: each sample hit covers 4 byte(s) for 0.03 % of 30.94 seconds
index % time    self  children    called     name
                0.02   30.92       1/1       main [2]
[1]    100.0    0.02   30.92       1         fraktal [1]
                0.01   30.91      52/52      berechne [3]
-----------------------------------------------
                                             <spontaneous>
[2]    100.0    0.00   30.94                 main [2]
                0.02   30.92       1/1       fraktal [1]
                0.00    0.00       3/3       ermittle_shifts [5]
                0.00    0.00       2/2       push_para [6]
                0.00    0.00       1/1       init_colors [7]
                0.00    0.00       1/1       posit [8]
---------------------------------------------
                0.01   30.91      52/52      fraktal [1]
[3]     99.9    0.01   30.91      52         berechne [3]
               30.91    0.00  798720/798720  farbe [4]
---------------------------------------------
               30.91    0.00  798720/798720  berechne [3]
[4]     99.9   30.91    0.00  798720         farbe [4]
---------------------------------------------
                0.00    0.00       3/3       main [2]
[5]      0.0    0.00    0.00       3         ermittle_shifts [5]
---------------------------------------------
                0.00    0.00       2/2       main [2]
[6]      0.0    0.00    0.00       2         push_para [6]
---------------------------------------------
                0.00    0.00       1/1       main [2]
[7]      0.0    0.00    0.00       1         init_colors [7]
---------------------------------------------
                0.00    0.00       1/1       main [2]
[8]      0.0    0.00    0.00       1         posit [8]
---------------------------------------------
...
...
Index by function name
   [3] berechne                [1] fraktal
   [6] push_para               [5] ermittle_shifts
   [7] init_colors             [4] farbe
   [8] posit

Rheinwerk Computing

17.5.3 Analyse mit GCOtoptop

Welche Codeteile innerhalb des Programms, genauer, innerhalb einer Funktion wie häufig ausgeführt wurden, kann mit GCOV ausgewertet werden. Damit wird eine Abdeckungsanalyse ausgeführt, die mitzählt, wie oft eine Programmzeile durchlaufen wurde. Eine solche Abdeckungsanalyse wird in den folgenden Schritten erstellt:

1. Beim Übersetzen des Listings wird mit den Optionen -g -fprofile-arcs und -ftest-coverage eine Abdeckungsanalyse eingerichtet:
       
$ gcc -ggdb3 -fprofile-arcs -ftest-coverage -o \    fraktal fraktal.c berechnung.c -L/usr/X11R6/lib -lX11
    Damit wird für jede Quelldatei, die Sie angeben, eine Hilfsdatei mit der Extension *.bgg angelegt, im Beispiel (berechnung.bbg und fraktal.bbg), die den Kontrollflussgraphen beinhalten und diesen im Quelltext abbilden.
       
2. Im nächsten Schritt wird das Programm ausgeführt, um eine Abdeckungsanalyseinformation zu erzeugen. Diese wird in einer Datei mit der Endung *.da geschrieben.
       
$ ./fraktal
3. Jetzt kann mit GCOV eine lesbare Kopie der Abdeckungsanalyse generiert werden:
       
$ gcov fraktal.c File `fraktal.c' Lines executed:71.01 % of 207 fraktal.c:creating `fraktal.c.gcov' $  gcov berechnung.c File `berechnung.c' Lines executed:100.00 % of 21 berechnung.c:creating `berechnung.c.gcov'

Die Angabe des Listings fraktal.c von 71,01 % sagt aus, dass der komplette Quellcode bis auf 29,99 % der 207 Zeilen ausgeführt wurde. Beim zweiten Quellcode, der berechnung.c, ist die Abdeckung des Quellcodes gar 100 %. Das heißt, hier wurde jede Zeile ausgeführt. Das alleine sagt natürlich noch nichts aus. Daher müssen Sie auch noch die erzeugten Dateien berechnung.c.gcov und fraktal.c.gcov genauer unter die Lupe nehmen. Ein Ausschnitt aus der Abdeckungsdatei fraktal.c.gcov sieht etwa so aus (ein für das Buch angepasstes Beispiel):

...
#####:  336: rueck = posit();
    -:  337: }
   52:  338: if (rueck == NORMAL_WEITER) {
   52:  339:         parainfo.br_par = breite;
   52:  340:         parainfo.y_par = yr0;
   52:  341:         parainfo.x_par = xr0 + xdelta * breite * 
                     (segmentzahl - 1);
   52:  342: parainfo.delx_par = xdelta;
   52:  343: parainfo.dely_par = ydelta;
   52:  344: laenge = berechne(parainfo, punktfeld);
   52:  345: prozess.xbb     = breite  * (segmentzahl - 1);
   52:  346: prozess.ybb     = 0;
   52:  347: segmentzahl--;
798772: 348: for(ptr = punktfeld; laenge; laenge--) {
    -:  349: /* Punkt fuer Punkt zeichnen: */
798720: 350: XSetForeground(display, gcs,
              SETZE_FARBE(*ptr++));
798720: 351: XDrawPoint(display, ZIEL, gcs, 
               prozess.xbb, prozess.ybb);
798720: 352: if (++prozess.ybb >= MAXY) { /* aktualisieren */
  1040: 353: prozess.ybb = 0;
  1040: 354: prozess.xbb++;
    -:  355: }
    -:  356: }
    -:  357: #ifdef BACKUP
    -:  358: XCopyArea(display, backup, windowId, gcs,
             prozess.xbb - breite, 0,
    -:  359: breite, MAXY, prozess.xbb - breite, 0);
    -:  360: #endif
    -:  361: }
    -:  362: }
    1:  363: printf("Zeitdauer: %d Sekunden\n", 
             time(NULL) - zeit);
    1:  364: XFlush(display); /* Displaypuffer raeumen */
...

Dies lässt sich folgendermaßen lesen. Ganz links finden Sie die Anzahl der Durchläufe, wie oft diese Codezeile ausgeführt wurde. Steht hier #####, bedeutet dies, dass diese Codezeile nicht ausgeführt wurde. Hier könnten Sie z. B. die for()-Schleife optimieren, die 798772 mal aufgerufen wird und worin auch 798772 mal ein Pixel gezeichnet wird. Ein kurzer Blick in die Datei berechnung.c.gov zeigt weitere Optimierungsmöglichkeiten:

   798720:   44:   a = xr0; b = yr0; /* a + bi = Z0 */
   798720:   45:   zaehler = 0;
   798720:   46:   qua = a * a; qub = b * b;
839599651:   47:   do {
839599651:   48:   b = 2 * a * b - yr0; /* neuer Imaginaerteil */
839599651:   49:   a = qua - qub - xr0; /* neuer Realteil      */
839599651:   50:   qu=(qua=a* a) + (qub = b * b); /* qu = |Z| */
839599651:   51:   }
        -:   52:   while(qu<DIVERGENZGRENZE&&qu>KONVERGENZGRENZE
        -:   53:          && zaehler++ < ZYKLEN);
   798720:   54:   return(zaehler & 0xff);

Die do-while-Schleife wird hier 839599651 mal durchlaufen. Dabei lässt sich sicherlich mit einigen mathematischen Operationen und Compiler-Flags noch etwas Leistung herauskitzeln. Häufig aufgerufene Berechnungen und Funktionsaufrufe gilt es, besonders im Auge zu behalten und eventuell zu optimieren.


Hinweis   Bitte beachten Sie, dass die Abdeckungsanalyse bei einem erneuten Programmstart nicht gelöscht wird, sondern an die alte Datei angehängt und die einzelnen Zeilen summiert werden. Wollen Sie eine komplette neue Abdeckungsanalyse erstellen, müssen Sie zuvor noch die Datei mit der Extension *.da löschen.


Zusammenfassung

Natürlich lässt sich zu den Tools GPROF und GCOV noch eine Menge mehr schreiben. Aber die grundlegenden Arbeiten dieser Werkzeuge wurden hier erwähnt und hoffentlich auch verstanden. Sie wissen jetzt zumindest, wie Sie kritische Stellen im Quellcode finden können, um diese zu optimieren. Wie Sie Programme und Quellcode optimieren können, ist, bis auf bestimmte Optionen des Compilers, nicht Diskussionspunkt dieses Buchs.

 << zurück
  
  Zum Rheinwerk-Shop
Neuauflage: Linux-UNIX-Programmierung
Neuauflage:
Linux-UNIX-
Programmierung

bestellen
 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchtipps
Zum Rheinwerk-Shop: Linux-Server






 Linux-Server


Zum Rheinwerk-Shop: Das Komplettpaket LPIC-1 & LPIC-2






 Das Komplettpaket
 LPIC-1 & LPIC-2


Zum Rheinwerk-Shop: Linux-Hochverfügbarkeit






 Linux-
 Hochverfügbarkeit


Zum Rheinwerk-Shop: Shell-Programmierung






 Shell-
 Programmierung


Zum Rheinwerk-Shop: Linux Handbuch






 Linux Handbuch


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
Info





Copyright © Rheinwerk Verlag GmbH 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern