Was ist ein Profiler? Profiling-Tool« heißt etwa soviel wie »Profil-Werkzeug«. Es wird benötigt, wenn umfangreiche Programme analysiert oder optimiert werden sollen. Im allgemeinsten Falle kann man mit seiner Hilfe herausfinden, wie häufig eine bestimmte Prozedur oder Anweisung ausgeführt wurde.
Bei etwas raffinierteren Versionen werden zusätzlich noch Zeitmessungen vorgenommen, so daß nach der Ausführung eines großen Programmes leicht zu sehen ist, in welchen Prozeduren die meiste Arbeit getan wird. Will man das Programm optimieren, genügt es, sich auf diese i.a. wenigen Prozeduren zu konzentrieren.
Wer schreibt schon umfangreiche Programme in LOGO?«, werden Sie fragen.
Nun, manche tun's. Das ist allerdings nicht der eigentliche Grund, weshalb ich Ihnen den LOGO-Profiler vorstellen möchte. Der eigentliche Grund ist der, daß es nur wenige andere Programmiersprachen gibt, in denen ein solches Hilfsmittel mit der gleichen Leichtigkeit programmiert werden kann. In LOGO werden ganze 67 Programmzeilen dafür benötigt; andere Hochsprachen wie C, Modula-2 oder FORTRAN benötigen viele hundert Zeilen, um das gleiche Ergebnis zu erzielen. Kurzum, der Profiler ist eine ideale Anwendung, um die Leistungsfähigkeit von LOGO zu demonstrieren.
Ich möchte Ihnen hier nur eine einfache Version eines Profilers vorstellen: Nur die Anzahl der Aufrufe jeder Prozedur soll während der Programmausführung ermittelt werden. Das Zählen soll durch Anweisungen der Art
MAKE “COUNTER.n :COUNTER.n + 1
zu Beginn jeder Prozedur erfolgen. Dabei soll n für jede Prozedur einen anderen Wert haben. Das LOGO-Programm muß also vor der Ausführung instrumentiert werden. Man spricht auch von einem Preprocessor, weil das Programm vor der Ausführung verändert wird. Genau dieser Vorgang ist in LOGO besonders leicht zu realisieren, da Programme von LOGO genauso wie Daten behandelt werden -genauer gesagt, wie Listen. Der Vorgang, den wir benötigen - das Einfügen einer neuen Anweisung - entspricht also dem Einfügen eines neuen Listenelements in eine bereits existierende Liste.
Zunächst müssen wir uns die Liste aller Prozedurnamen des zu analysierenden Programmes beschaffen. Dies geschieht mit der LOGO-Anweisung
PROCLIST.
Aus verschiedenen Gründen möchten wir die Liste alphabetisch sortiert verwenden. Wir sagen also
SORT PROCLIST
und weisen diese sortierte Liste der globalen Variablen “$PROCS zu:
MAKE “$PROCS SORT PRO CLIST.
Da wir auch noch wissen möchten, wieviele Prozeduren das Programm überhaupt besitzt, zählen wir die Elemente von “$PROCS:
MAKE “$NCOUNT :$PROCS.
Nachdem wir nun wissen, wieviele Prozeduren es gibt, können wir alle benötigten Zählervariablen initialisieren. Dies geschieht mit dem Aufruf
$INIT :$N
Alle diese Anweisungen finden Sie im Listing 1 in der Prozedur $PROC.COUNT. Auf die Prozedur $INIT soll hier nicht detailliert eingegangen werden. Nur so viel: Sie bastelt aus dem Zähler i und dem Wort "$COUNTER eine ganze Reihe von neuen Variablen der Form
“$COUNTER.i
und initialisiert sie anschließend mit Null.
Sie werden sich vielleicht wundern, wieso alle globalen Variablen und Prozedurnamen mit $ anfangen, den doch nicht mal mehr die Finanzexperten heben. Der Grund dafür ist, daß alle Prozeduren unseres Profilers später mit dem zu analysierenden Programm gemeinsam im LOGO-Workspace liegen. Damit möglichst keine Namenskonflikte auftreten, wird der $ benutzt.
Nach der Initialisierung der Zählervariablen wird nun in $PROC.COUNT für jede Prozedur die Instrumentierungsroutine $INSTRUMENT aufgerufen:
MAKE "I 1
REPEAT :$N
[$INSTRUMENT (ITEM :I :$PROCS) :I
MAKE "I :I + 1]
Dabei pickt der Ausdruck (ITEM :I :$PROCS) gerade das i-te Listenelement - also den Namen der i-ten Prozedur - aus “$PROCS heraus und übergibt ihn an SINTRUMENT. Außerdem wird noch der Zähler I selbst übergeben, damit $INSTRUMENT weiß, mit welchem Zähler die Prozedur zu instrumentieren ist.
Das wirkich Spannende geschieht nun in SINSTRUMENT. Zunächst wird abgefragt, ob der übergebene Prozedurname mit $ anfängt. In diesem Fall nimmt SINSTRUMENT an, daß eine Routine des Profilers vorliegt und tut nichts. Die Folgen wären auch fatal: würde SINSTRUMENT weitermachen wie in allen anderen Fällen, so würde es sich früher oder später selbst instrumentieren und sich sozusagen im Vorübergehen selbst zerstören.
Liegt nun eine »normale« Prozedur vor, wird sie zunächst in Parameterliste und Prozedurrumpf zerlegt:
MAKE "PROC.BODY TEXT :PROC MAKE "INTRUC
LAST :PROC.BODY
TEXT :PROC verschafft uns zunächst den Quelltext der Prozedur, deren Name in “PROC gespeichert ist. Dieser Quelltext wird der Variablen "PROC.BODY zugewiesen. Er besteht aus zwei Teilen, der Parameterliste und dem Prozedurrumpf. LAST :PROC.BODY extrahiert hieraus den Rumpf und weist ihn der Variablen “INSTRUC zu. "INSTRUC ist nun eine Liste, deren Listenelemente die Anweisungen der zu instrumentierenden Prozedur sind. Nachdem wir wieder einen Zähler der Form
"$COUNTER.i
gebastelt haben, können wir hiermit die neue Anweisung
MAKE "$COUNTER. i :$COUNTER.i + 1
in den Prozedurrumpf einbauen. Dies geschieht mit Hilfe der Funktion FPUT, die ein neues Listenelement an den Anfang einer Liste setzt. Beachten Sie, daß wir die neue Anweisung gewissermaßen rückwärts einbauen müssen: Zuerst wird die 1, dann das +, dann der Variablenname usw. und zum Schluß das “MAKE eingebaut. Schließlich haben wir die neue Anweisung fertig eingesetzt und können den neuen Prozedurrumpf wieder mit der ursprünglichen Parameterliste vereinigen:
MAKE "PROC.BODY BL :PROC.BODY
MAKE "PROC.BODY LPUT : INSTRUC :PROC.BODY
Zunächst wird mit BL (das ist die Kurzform von BUTLAST) die Prozedur um ihren alten Rumpf gekürzt. Mit LPUT (dem Einfügen eines neuen Listenelementes am Ende der Liste) wird nun der neue Rumpf an die verbliebene Parameterliste angehängt. Schließlich müssen wir die neu geschaffene Prozedur dem LOGO-System als eine ausführbare Prozedur bekanntmachen. Dies geschieht mit
DEFINE :PROC :PROC.BODY
Dabei gibt :PROC den Namen der Prozedur an, der sich ja nicht verändert hat.
Damit ist fast alles getan, was zu tun war. Eigentlich muß das Programm nur noch ausgeführt werden, und die Zählerstände aller Zählvariablen müssen hinterher ausgegeben werden. Dies besorgt das »Hauptprogramm« unseres Profilers SSTART. Es ruft zunächst die Prozedurzähl- und Instrumentierroutine $PROC.COUNT, führt dann das Programm, dessen Name und Parameterliste beim Aufruf von $START angeben werden müssen, aus und ruft anschließend die Ausgaberoutine SPRINT auf.
SPRINT enthält keine besonderen Geheimnisse mehr, außer vielleicht die Routine SJUSTIFY, die dafür sorgt, daß die Ausgabe schön linksbündig und spaltengerecht erfolgt. Außerdem wird die Sortierung der Prozedurliste dazu benutzt, die Profiler-Zähler bei der Ausgabe zu überspringen ($ ist eines der kleinsten druckbaren ASCII- Zeichen). Auf beide Routinen soll hier jedoch nicht näher eingegangen werden.
Ich möchte die Handhabung an einem winzig kleinen LOGO- Programm demonstrieren. das Wabenmuster zeichnet I (s. Bild 1). Das Programm besteht aus vier Prozeduren, deren Ausführungshäufig-fceit gezählt werden soll. Fangen wir mit dem leeren LOGO-Workspace an. Laden Sie das Programm WABEN von Diskette, bzw. tippen Sie seine vier Prozeduren ein I (s. Listing 2). Falls noch nicht geschehen, sichern Sie sie auf jeden Fall auf Diskette, da sie ja im Laufe unserer Arbeit verändert werden. Sie können das Beispielprogramm nun schon einmal ausführen, um zu sehen, was es tut:
WABEN 10
Laden Sie nun die Prozeduren des Profilers dazu (Sie sollten das Beispielprogramm und die Profiler-Prozeduren möglichst getrennt eintippen und auch getrennt auf Diskette halten). Geben Sie nun das LOGO-Kommando
$START [WABEN 15]
"WABEN” ist das Programm, das wir analysieren möchten. Es benötigt einen numerischen Wert als Parameter, der die Kantenlänge der Waben bestimmt. Damit beides zu einer Einheit zusammengefaßt ausgeführt werden kann, muß es mit Hilfe der eckigen Klammern zu einer Liste gemacht werden. Die eckigen Klammern sind im übrigen auch nötig, wenn keine Parameterliste existiert.
Sie werden bemerken, daß eine kleine Weile vergeht, bevor WABEN seine Arbeit aufnimmt: Der Profiler muß ja zunächst die Instrumentierung durchführen. Hat WABEN seine Arbeit beendet, erscheint auf dem Bildschirm eine Liste aller Prozeduren und die Häufigkeit ihres Aufrufs (s. Bild 2). In unserem Beispielprogramm ist D.LINE die am häufigsten gerufene Routine. Leider ist sie so kurz, daß sie kaum eine Möglichkeit zur Optimierung bietet.
Bleiben nur noch zwei Dinge zu erwähnen. Falls sie WABEN nocheinmal - z.B. mit einem anderen Parameter - ausführen möchten, benutzen Sie den Aufruf
$RESTART [WABEN 20]
damit die Prozeduren nicht noch einmal instrumentiert werden. Möchten Sie ein Programm analysieren, das während der Ausführung auf einen Laufzeitfehler zuläuft, so müssen Sie nach dem fehlerhaften Lauf das Kommando
$PRINT
benutzen, da der SPRINT-Aufruf im Profiler wegen des fehlerhaften Programmlaufs nicht mehr ausgeführt wurde. Sie können so sehen, welche Teile Ihres Programms bisher ausgeführt wurden und können diese Informationen möglicherweise zur Fehlersuche benutzen.
Vielleicht sind Sie auf den Geschmack gekommen, sich auch einmal etwas intensiver mit LOGO zu beschäftigen. Wie wär’s mit einem kleinen Compiler? Wie man in LOGO neue Programme erzeugt, wissen Sie ja nun; alles andere ist fast nur noch Fleißarbeit.
1: TO $START :PGM
2: $PROC.COUNT
3: RUN :PGM
4: $PRINT
5: END
6:
7: TO $INIT :N
8: (LOCAL "NAM "I)
9: MAKE "I 1
10: REPEAT :N
11: [MAKE "NAM WORD "SCOUNTER. :I
12: MAKE :NAM 0
13: MAKE "I :I + 1]
14: END
15:
16: TO $ INSTRUMENT :PROC :I
17: (LOCAL "PROC.BODY "INSTRUC "NAM)
18: IF FIRST :PROC = "$ [STOP]
19: MAKE "PROC.BODY TEXT :PROC
20: MAKE "INSTRUC LAST :PROC.BODY
21: MAKE "NAM WORD "$COUNTER. :I
22: MAKE "INSTRUC FPUT "1 : INSTRUC
23: MAKE "INSTRUC FPUT "#+ : INSTRUC
24: MAKE "INSTRUC FPUT WORD ": :NAM : INSTRUC
25: MAKE "INSTRUC FPUT WORD "" : NAM : INSTRUC
26: MAKE "INSTRUC FPUT "MAKE : INSTRUC
27: MAKE "PROC.BODY BL :PROC.BODY
28: MAKE "PROC.BODY LPUT : INSTRUC :PROC.BODY
29: DEFINE :PROC .PROC.BODY
30: END
31:
32: TO $PROC.COUNT
33: (LOCAL "I)
34: MAKE "$PROCS SORT PROCLIST
35: MAKE "$N COUNT : $PROCS
36: $INIT :$N
37: MAKE "I 1
38: REPEAT :$N
39: [$INSTRUMENT (ITEM :I :$PROCS) :I
40: MAKE "I :I + 1]
41: END
42:
43: TO $PRINT
44: (LOCAL "I "CNT "DUMMY)
45: PR "
46: PR ”
47: PR [Liste aller Prozeduraufrufe:]
48: PR "
49: (PR $JUSTIFY 5 "Nr. $JUSTIFY 20 "Prozedurname "Anzahl# Aufrufe)
50: PR "
51: MAKE "I 8
52: REPEAT (:$N - 7)
53: [MAKE "CNT WORD "$COUNTER. :I
54: (PR $JUSTIFY 5 (:I - 7) $JUSTIFY 20 ITEM :I : $PROCS THING :CNT)
55: IF (REMAINDER (:I - 7) 20) =0 [TYPE "#-#-#-MEHR#-#-#-
56: MAKE "DUMMY RL]
57: MAKE "I :I + 1]
58: END
59:
60: TO $JUSTIFY :N :TXT
61: (LOCAL "CN "I "BLANK)
62: MAKE "CN (:N - COUNT :TXT)
63: IF :CN < 0 [MAKE "CN 0]
64: MAKE "BLANK "
65: REPEAT :CN
66: [MAKE "BLANK WORD :BLANK "# ]
67: OUTPUT WORD : TXT : BLANK
68: END
69:
70: TO $RESTART :PGM
71: $INIT COUNT :$PROCS
72: RUN :PGM
73: $PRINT
74: END
Listing 1
1: TO D.LINE :LEN
2: FD :LEN
3: RT 60
4: END
5:
6: TO SECHSECK :LEN
7: REPEAT 6
8: [D.LINE :LEN]
9: END
10:
11: TO RING :LEN
12: REPEAT 6
13: [SECHSECK :LEN
14: PU
15: D.LINE :LEN
16: LEFT 120
17: PD]
18: END
19:
20: TO WABEN :LEN
21: HT
22: CS
23: REPEAT 6
24: [RING :LEN
25: PU
26: D.LINE :LEN
27: D.LINE :LEN
28: D.LINE :LEN
29: PD
30: LEFT 120]
31: END
32:
Listing 2