minpic01.jpg

Occam and its GUYs

Occam-Strukturen disassembliert

Xiandeng Huang

Transputer sind der Inbegriff der parallelen Prozessor-Architektur. Um Programme für solche Prozessoren zu schreiben, war zuvor Occam entwickelt worden, das Parallelkonstrukte unterstützt. Nun gibt es aber Situationen, in denen man auch bei Transputern auf Assemblerprogrammierung nicht verzichten kann, dadurch aber auf Unterstützung der Hochsprache in bezug auf Parallelität verzichten muß. In dem Falle forscht man einfach Occams Geheimnisse aus, um es ihm gleichtun zu können.

Unterthema: Eine Anwendung des Assembler-Konstruktors GUY
Unterthema: Ein Beispiel für die vom Disassembler erzeugte Ausgabe
Unterthema: Occam-PAR-Konstrukt
Unterthema: Code des ALT-Konstrukts

Die auf dem Hoareschen Konzept `Communicating Sequential Processes´ (vgl. [3]) basierende Programmiersprache Occam (vgl. [7]) ist auf die effiziente Programmierung paralleler Prozesse auf Transputern zugeschnitten.

Der vom britischen Halbleiterhersteller Inmos entwickelte Occam-Compiler, der Bestandteil sowohl des `Transputer Development System - TDS´ als auch des `Occam-Compiler-Server-System - OCS´ ist, bietet darüber hinaus die Möglichkeit, mit dem Assembler-Konstruktor GUY in einem Occam-Programm auf Assemblerebene des Transputers zu programmieren. Das ist normalerweise selten nötig, da der Occam-Compiler effizienten Transputermaschinencode erzeugt.

Die Verwendung der Assemblersprache ist aber dann von Vorteil, wenn es um die Bewältigung spezieller Probleme geht, wie sie zum Beispiel das Debugging aufwirft, das manchmal die Analyse des ausführbaren Codes erfordert. Mit Hilfe eines Disassemblers für den Transputertyp T414 kann man ein Occam-Programm und seinen Transputercode zu diesem Zweck vergleichen. Im folgenden werden also der Konstruktor GUY und einige seiner Anwendungen sowie die Realisation der Haupt-Konstruktoren von Occam - PAR und ALT - auf Transputercodeebene vorgestellt.

GUY kann nicht nur wie alle anderen Occam-Konstruktoren benutzt werden, sondern ermöglicht auch die Verwendung der im herkömmlichen Occam-Programm deklarierten Variablen in einem Transputermaschinenbefehl. Dadurch kann man direkt auf Variablen, Kanäle, Register und Speicherzellen zugreifen. Diese Eigenschaft kann beim Testen von Occam-Programmen erfolgreich eingesetzt werden.

Transputer intern

Alle Befehle des Transputers (vgl. [6]) haben das gleiche, aus einem einzelnen Byte bestehende Format. Die vier höherwertigen Bits stellen den Operationscode dar, während die vier niederwertigen Bits als Operand dienen.

Der Befehlsatz ist von der internen Wortlänge eines Transputers (16 oder 32 Bit) unabhängig. Mit den Transputerbefehlen PFIX und NFIX kann die Größe eines Operanden beliebig erweitert und mit dem Operationscode OPR der Inhalt des Operand-Registers als die auszuführende Operation interpretiert werden. PFIX lädt seine vier Operand-Bits in das Operand-Register und verschiebt dann den Inhalt vier Bits nach links, so daß die vier niederwertigen Bits des Operand-Registers wieder frei und zum Laden des Operanden des nächsten Befehls bereit sind.

NFIX funktioniert genauso, außer, daß das Operand-Register vor dem Verschieben komplementiert wird. Um beispielsweise die Konstante 50 zu laden, werden die Befehle `PFIX 3´ und `LDC 2´ benötigt, während die Konstante -50 mit `NFIX 3´ und `LDC #E´ geladen wird. Es ist hier noch besonders zu erwähnen, daß PFIX und NFIX die einzigen Befehle sind, die das Operand-Register nach getaner Arbeit nicht löschen.

Ein Transputer hat einen nur aus den drei Registern A, B und C bestehenden Rechenstack, in dem die meisten arithmetischen und logischen Operationen durchgeführt werden. Außerdem besitzen Transputer

Für priorisierbare, parallellaufende Prozesse benötigt der Transputer noch folgende vier Register: FPtr0, BPtr0, FPtr1 und BPtr1 (vgl. [4]), die ebenfalls zum internen Registersatz gehören. Dabei zeigt FPtr0 auf den Anfang der Warteschlange der zur Ausführung bereiten Prozesse hoher Priorität (Priorität 0) und BPtr0 auf das Ende dieser Warteschlange. Prozesse niedriger Priorität (Priorität 1) werden entsprechend mit Hilfe der Register FPtr1 und BPtr1 verwaltet.

Für die Prozesse, die den Verzögerungsoperator AFTER enthalten, sind weitere spezielle Register vorgesehen. Außer den sechs Spezialregistern gibt es insgesamt noch acht weitere.

Konstruktor GUY

Nach dem Konstruktor GUY, der ähnlich wie die anderen Occam-Sprach-Konstruktoren verwendet wird, sind nur noch Transputerbefehle zugelassen, davon jedoch nicht alle, sondern nur die meisten. Ein Beispiel zeigt das Listing Fakultät.

Unter GUY gibt es folgende Vorteile:

GUY
  LDL x
  ADC-30
  STL x
GUY
  LDC #80000000
  LDDNL 9

minpic02.jpg

Format eines Transputerbefehls, das immer nur 1 Byte beansprucht.

minpic03.jpg

Der Registersatz eines Transputers, wobei A, B und C den Rechenstack bilden

minpic04.jpg

Belegung des Workspace; ein solcher wird jedem parallelen Prozeß für seine lokalen Variablen, Kanäle und Parameter zugeordnet.

Anwendung von GUY

Mit GUY kann man sowohl auf den Workspace-Zeiger als auch auf seinen Inhalt zugreifen. Beispielsweise enthält die Variable Ws.Adr nach Ausführung des Programmfragments

INT Ws.Adr, Ws.Inh:
  ...
  GUY
    LDL 0
    STL Ws.Inh
    LDLP 0
    STL Ws.Adr
    ...
den Workspace-Zeiger, also den Inhalt des Workspace-Registers, das momentan auf den Workspace des gerade ausgeführten Prozesses zeigt. Die Variable Ws.Inh enthält den Inhalt der nullten Speicherzelle im Workspace, also der vom Workspace-Zeiger referierten Speicherzelle.

Jeder parallele Prozeß hat einen eigenen Workspace, wo seine lokalen Variablen, Kanäle und Parameter abgespeichert sind. Der Workspace ist so vergeben, daß in der Notationsreihenfolge später deklarierte Variablen oder Kanäle kleinere Offsets relativ zum Workspace-Zeiger haben.

Der Workspace des Fakultät-Programms besteht, wie in der Abbildung zur Workspace-Belegung zu sehen, aus zwei Teilen, die hier als System- und Program-Workspace bezeichnet sind. Der Program-Workspace kann wieder aus mehreren lokalen Workspaces von parallel laufenden Prozessen bestehen, während ein Programm nur einen System-Workspace hat, der immer aus 17 Wörtern besteht und im wesentlichen für die vom TDS definierten Systemkanäle gebraucht wird:

Der Inhalt der Speicherzelle mit Offset 1 im Program-Workspace - das ist `(Anzahl)´ - wird für die Schleife

SEQ i = 1 FOR n 
gebraucht und enthält die Anzahl der noch zu verarbeitenden Schleifendurchläufe. Im folgenden wird für den Begriff `Program-Workspace´ die Abkürzung `Workspace´ verwendet.

Auch um direkt auf das Instruktions-Register zugreifen zu können, kann man GUY benutzen. Auf Assemblerebene im Schutze von GUY sieht das folgendermaßen aus:

INT Instr, Instr.Adr:
  ...
  GUY
    LDC -1
    LDPI
    STL Instr.Adr
    LDL Instr.Adr
    LDNL 0
    STL Instr
    ...
Die Speicheradresse des Befehls LDPI wird dabei in Instr.Adr abgelegt. Instr enthält vier Befehle (4 Bytes), den Befehl LDPI selbst und seine Nachbarn. Die letzten zwei Bits in Instr.Adr geben darüber Auskunft, welches der Bytes den Befehl LDPI beinhaltet; zum Beispiel Byte 1, wenn die letzten beiden Bits gleich 01 sind.

Ein Kanal k befindet sich im Kommunikationszustand `leer´, falls keiner der beiden durch ihn verbundenen Prozesse kommunizieren will. k hat in diesem Fall den Zustandswert #80000000. Ist ein Prozeß bereit, über den Kanal k Nachrichten zu senden oder zu empfangen, sein Kommunikationspartnerprozeß aber noch nicht zum Empfangen oder Senden bereit, so wird der Deskriptor des zur Kommunikation bereiten Prozesses in k abgespeichert. (Ein Deskriptor (vgl. [5, S. 29]) eines Prozesses ist die Summe seines Workspace-Zeigers und seiner Priorität, die nur den Wert 0 oder 1 annehmen kann und im letzten Bit des Prozeß-Deskriptors untergebracht ist.)

minpic05.jpg

Nach dem Auslesen des Instruktions-Registers findet man den Befehlscode des adressierten Befehls in einem bestimmten Byte der Variablen.

In der Warteschlange

Der Prozeß wird dann einfach aus der Warteschlange der zur Ausführung bereiten Prozesse herausgenommen (descheduled), bis sein Partnerprozeß auch zur Kommunikation bereit ist und ihn wieder weckt. Nach der Kommunikation hat k wieder den Leerzustand #80000000. Deshalb kann man durch die Untersuchung der Kommunikationszustände von Kanälen erfahren, ob ein Deadlock vorliegt oder Prozesse wegen nicht befriedigter Kommunikationsanforderungen blockiert sind. Dies ist nur auf Assemblerebene möglich.

Nach Ausführung des Programmfragments:

CHAN OF INT k:
INT k.Zustand:
...
  GUY
    LDL k
    STL k.Zustand
  ... 
enthält die Variable k.Zustand den Kommunikationszustand des Kanals k.

Die Systemkanäle werden wie normale Kanäle behandelt, zum Beispiel nach Ausführung des Programmfragments

PROC scr.keyb(CHAN OF ANY keyboard, screen)
      INT scr.Zustand, scr.Adr, keyb.Zustand, keyb.Adr:
      ...
           LDL screen
           STL scr.Zustand
           LDLP screen
           STL scr.Adr
           LDL keyboard
           STL keyb.Zustand
           LDLP keyboard
           STL keyb.Adr
      ...
enthalten scr.Zustand und keyb.Zustand jeweils die Zustände von screen und keyboard. Die Adressen von screen und keyboard sind den Variablen scr.Adr und keyb.Adr zugeordnet. Unter TDS gilt:

scr.Adr = #801F0054
keyb.Adr = #801F004C
Sie werden im System-Workspace abgespeichert. screen enthält als Anfangszustand übrigens nicht #80000000, sondern #801EFE7D.

Wenn ein Prozeß wegen einer Verzögerungsoperation (AFTER) auf den Ablauf seiner Verzögerungszeit wartet, wird er vom Scheduler aus der Warteschlange der zur Ausführung bereiten Prozesse herausgenommen. Sein Workspace-Zeiger wird abgespeichert.

Alle solchen Prozesse bilden eine Zeit-Warteschlange. Die Speicherzelle #80000024 (bzw. #80000028) enthält den Workspace-Zeiger des ersten Prozesses in der Zeit-Warteschlange für Prozesse mit Priorität 0 oder für solche mit Priorität 1. Mit Hilfe von GUY kann man auf sie zugreifen:

...
INT TPtrLoc0, TPtrLoc1:
...
  GUY
    LDC #80000024
    LDNL 0
    STL TPtrLoc0
    LDC #80000028
    LDNL 0
    STL TPtrLoc1
  ...
Wenn kein Prozeß auf den Ablauf seiner Verzögerungszeit wartet, dann haben TPtrLoc0 und TPtrLoc1 den Wert #80000000.

In Occams Tiefen

Die Konstruktoren PAR und ALT kennzeichnen die parallele Programmiersprache Occam. In diesem Abschnitt werde ich die Implementation der Konstruktoren PAR und ALT im TDS-Occam-Compiler für den T414 durch ausführliche Kommentare vorstellen.

Die vom TDS-Compiler erzeugte ausführbare Datei mit der Extension `.CE1´ kann mit dem Disassembler disassembliert werden. Die Eingabe des Disassemblers ist der ausführbare Maschinencode eines Programms, hier zum Beispiel des abgedruckten Quellprogramms `addieren´; die entsprechende Ausgabe des Disassemblers ist darunter abgebildet.

Die zweite Spalte (hinter dem Doppelpunkt) gibt den Inhalt der CE1-Datei an, also den ausführbaren Maschinencode. Die relative Adresse des Maschinencodes in der CE1-Datei befindet sich in der ersten Spalte. Die dritte und vierte Spalte (vor dem Semikolon) enthält Assemblerbefehle für den T414. Adresse und Code beziehungsweise Operand der Befehle sind in hexadezimaler Darstellung angegeben.

Die CE1-Datei ist folgendermaßen aufgebaut. Das erste 4-Byte-Wort (Adressen #0 bis #3) stellt die um eins verminderte Größe der CE1-Datei dar (Anzahl der Wörter). In diesem Beispiel ist das erste Wort gleich #0000000E, das heißt, die Größe der CE1-Datei beträgt #F (= #E + 1 = 15) Wörter, also 60 Bytes.

Die 7 nachfolgenden Wörter (Adressen #4 bis #1F) sind nur eine Markierung. Aus dem achten Wort (Adressen #20 bis #23) ergibt sich die Größe des benötigten Workspace für den Lauf des Occam-Programms. Der eigentliche Maschinencode eines Occam-Programms beginnt in dieser Darstellung immer bei Adresse #24. Die Adresse des ersten auszuführenden Befehls ergibt sich aus dem um 4 erhöhten Wert des vorletzten Wortes (Adressen #34 bis #37):

#00000020 vorletzt_Wort + 4 = #24 
Es ist nur ein Zufall, daß in diesem Beispiel die Anfangsadresse #24 und die Adresse des ersten auszuführenden Befehls übereinstimmen. Das letzte Wort ist immer gleich 0. Die wesentliche Befehlsfolge für das Occam-Programm `addieren´ befindet sich zwischen den Adressen #26 bis #2D. Die Offsets der lokalen Variablen a, b und c relativ zum Workspace-Zeiger sind in diesem Beispiel jeweils 2, 1 und 0.

minpic06.jpg

Belegung des Workspace für PAR

Der Konstruktor PAR

Aus den Listings zum Programm `addieren´ können Sie entnehmen, daß kein zusätzlicher Befehl für den Konstruktor SEQ gebraucht wird. Im Gegensatz dazu wird der Konstruktor PAR als eine Befehlsfolge kompiliert, die die zugehörigen parallel laufenden Prozesse starten kann. Jeder einzelne der parallellaufenden Prozesse wird als ein unabhängiges Codefragment übersetzt, dessen Ende durch den Befehl ENDP gekennzeichnet ist.

In den Listings zum PAR-Konstruktor sind P1, P2 und P3 jeweils die Fragmente #24-#2B, #2C-#31 und #32-#3D. Sie können nur durch die dem Konstruktor PAR entsprechende Befehlsfolge (#46-#63) gestartet werden, die folgendermaßen wirkt:

minpic07.jpg

Belegung des Workspace für ALT

Konstrukt ALT

Der alternative Konstruktor ALT kommt normalerweise in einem der parallel laufenden Prozesse vor. Das letzte Abbildungs-Set aus Quellprogramm- und Disassemblerlisting sowie Workspace-Darstellung zeigt ein vereinfachtes Beispiel.

Dem Disassemblerlisting ist zu entnehmen, daß zunächst die letzte Guard eines ALT-Prozesses getestet wird. (Guards sind die formulierten Bedingungen, die über Ausführbarkeit eines Lesevorganges entscheiden.) Wenn mehrere Guards gleichzeitig ausführbar (vgl. [7, S. 130]) sind, wird immer die letzte davon ausgewählt. Diese Implementation weicht von Occams Sprachdefinition ab, die ein nichtdeterministisches Verhalten für diesen Fall vorsieht.

Der Konstruktor ALT wird als eine Befehlsfolge (#24-#3F) interpretiert, die grundsätzlich aus zwei Teilen besteht (#26-#2F und #32-#3D). Von #26 bis #2F werden alle Guards nacheinander getestet, ob sie ausführbar sind. Wenn keine Guard ausführbar ist, wird der ALT-Prozeß bis zum Befehl ALTWT (#30-#31) ausgeführt und aus der Warteschlange der zur Ausführung bereiten Prozesse herausgenommen - so lange - bis eine der Guards ausführbar wird.

Von #32 bis #3D werden alle Guards nochmals nach der dargestellten Reihenfolge durchgeprüft. Die zuerst angetroffene ausführbare Guard, das heißt die letzte im Quellprogramm, wird ausgewählt und ihre Sprungweite von ALTEND zum entsprechenden Zweig im Workspace abgespeichert (#35-#37 bzw. #3B-#3D). Aufgrund dieser Sprungweite verzweigt ALTEND zur ausgewählten ALTernative.

Den an der Assemblersprache des Transputers interessierten Lesern kann ich die Lektüre von [4, 5 und 2] empfehlen. In [4] wird die Wirkung aller Transputerbefehle für den T414, T212 und M212 grafisch dargestellt. [5] zeigt die Implementation von Occam auf dem Transputer und [2] beschreibt die Semantik der Befehle.

Zum Schluß möchte ich Herrn Detlef Feldmann und Herrn Frank Scherwa herzlichen Dank für ihre wichtigen Vorschläge und sprachlichen Verbesserungen sagen. (bb)

Literatur
[1] Heinz Ebert, Starke Familienbande - die Transputerbausteine, c't 10/87, S. 180

[2] Gerd Häußler, Die Assemblersprache des Transputers, mc, Juni 1988

[3] C. A. R. Hoare, Communicating Sequential Processes, Comm. ACM 21(8), 1978

[4] Inmos, T2/T4 transputer instruction set, (72 TRN 106 00), July 1986

[5] Inmos, The transputer instruction set - a compiler writers´ guide, (72 TRN 119 01), February 1987

[6] David May, Roger Shephard, Catherine Keane, Communicating Process Architecture: Transputer and Occam, Inmos April 1986

[7] Alois Schütte, Programmieren in Occam, Addison-Wesley, 1988

[8] Inmos, Transputer development system 2.0 Programming interface, (72 TDS 112 00) IMS D700 C / IMS D800 C, April 1987

[9] Inmos, Transputer development system 2.0 User manual, (72 TDS 110 00) IMS D700 C / IMS D800 C, April 1987

Kasten 1


Eine Anwendung des Assembler-Konstruktors GUY

PROC Fakultaet(CHAN OF ANY keyboard, screen)
  -- eine positive ganze Zahl n von der Tastatur einlesen
  -- x = n! auf dem Bildschirm ausgeben
  #USE "\tdsiolib\userio.tsr"
  INT n, x, char, end:
  SEQ
    write.full.string(screen, "n = ")
    read.echo.int(keyboard, screen, n, char)
    -- n wird von der Tastatur eingelesen.
    x := 1
    SEQ i = 1 FOR n
      SEQ
        GUY       -- x := x * i
          LDL x   -- lokale Variable x geladen
          LDL i   -- lokale Variable i geladen
          MUL     -- x mit i multipliziert
          STL x   -- Reg. A (= x * i) in x abgespeichert
    write.full.string(screen, "x = ")
    write.int(screen, x, 0)
    -- x = n! wird auf dem Bildschirm gezeigt.
    keyboard ? end
:
Kasten 2

Ein Beispiel für die vom Disassembler erzeugte Ausgabe: oben das Occam-Quellprogramm und darunter die disassemblierte CE1-Datei.

PROC addieren(CHAN OF ANY keyboard, screen)
  INT a, b, c:
  SEQ
    a := 4
    b := 5
    c := a + b
:

00000000: 00            J     00000000 ; jump
00000001: 00            J     00000000 ; jump
00000002: 00            J     00000000 ; jump
00000003: 0E            J     0000000E ; jump
00000004: 6162636465666768696A6B6C6D6E6F70 LDL 6A4C2E00;
                                         load local
00000014: 71            LDL   00000001 ; load local
00000015: 72            LDL   00000002 ; load local
00000016: 73            LDL   00000003 ; load local
00000017: 74            LDL   00000004 ; load local
00000018: 75            LDL   00000005 ; load local
00000019: 76            LDL   00000006 ; load local
0000001A: 77            LDL   00000007 ; load local
0000001B: 78            LDL   00000008 ; load local
0000001C: 79            LDL   00000009 ; load local
0000001D: 7A            LDL   0000000A ; load local
0000001E: 7B            LDL   0000000B ; load local
0000001F: 7C            LDL   0000000C ; load local
00000020: 00            J     00000000 ; jump
00000021: 00            J     00000000 ; jump
00000022: 00            J     00000000 ; jump
00000023: 05            J     00000005 ; jump
00000024: 60BD          AJW   FFFFFFFD ; adjust workspace
00000026: 44            LDC   00000004 ; load constant
00000027: D2            STL   00000002 ; store local
00000028: 45            LDC   00000005 ; load constant
00000029: D1            STL   00000001 ; store local
0000002A: 72            LDL   00000002 ; load local
0000002B: 71            LDL   00000001 ; load local
0000002C: F5            ADD            ; add
0000002D: D0            STL   00000000 ; store local
0000002E: B3            AJW   00000003 ; adjust workspace
0000002F: 22F0          RET            ; return
00000031: 50            LDNLP 00000000 ; load non-local pointer
00000032: 50            LDNLP 00000000 ; load non-local pointer
00000033: 50            LDNLP 00000000 ; load non-local pointer
00000034: 00            J     00000000 ; jump
00000035: 00            J     00000000 ; jump
00000036: 00            J     00000000 ; jump
00000037: 2000          J     00000000 ; jump
00000038: 00            J     00000000 ; jump
00000039: 00            J     00000000 ; jump
0000003A: 00            J     00000000 ; jump
0000003B: 00            J     00000000 ; jump
Kasten 3

Das Occam-PAR-Konstrukt resultiert in einer eigenen Befehlsfolge.

...
INT y, x:
CHAN OF INT  k1, k2:
PAR
  -- P1
  k1 ? y
  -- P2
  k2 ! 10
  -- P3
  SEQ
    k1 ! 5
    k2 ? x
...

---------- P1 ----------
  -- k1 ? y
  24: 70   LDL   0
  25: 55   LDNLP 5   -- Adresse von y geladen
  26: 70   LDL   0
  27: 53   LDNLP 3   -- Adresse von k1 geladen
  28: 44   LDC   4   -- 4 Bytes Nachrichten
  29: F7   IN        -- eingeben
  -- P1 endet       
  2A: 11   LDLP  1   -- WS-PAR geladen
  2B: F3   ENDP      -- P1 beendet
---------- P2 ----------
  -- k2 ! 10
  2C: 71   LDL   1
  2D: 52   LDNLP 2   -- Adresse von k2 geladen
  2E: 4A   LDC   #A  -- Konstante 10 (dez.) geladen
  2F: FF   OUTWORD   -- ein Wort ausgeben
  -- P2 endet 
  30: 16   LDLP  6   -- WS-PAR geladen
  31: F3   ENDP      -- P2 beendet
---------- P3 ----------
  -- k1 ! 5
  32: 71   LDL   1
  33: 53   LDNLP 3   -- Adresse von k1 geladen
  34: 45   LDC   5   -- Konstante 5 geladen
  35: FF   OUTWORD   -- ein Wort ausgeben
  -- k2 ? x
  36: 71   LDL   1
  37: 54   LDNLP 4   -- Adresse von x geladen
  38: 71   LDL   1
  39: 52   LDNLP 2   -- Adresse von k2 geladen
  3A: 44   LDC   4   -- 4 Bytes Nachrichten
  3B: F7   IN        -- eingeben
  -- P3 endet
  3C: 1B   LDLP  11  -- WS-PAR geladen
  3D: F3   ENDP      -- P3 beendet
---------- Befehlsfolge des Konstruktors PAR ----------
  -- Anfang von PAR
  46: 43   LDC   3   -- Anzahl der par. lauf. Prozesse ist 3
  47: D1   STL   1   -- und wird mit Offset 1 abgespeichert
  48: 2148 LDC  #18
  4A: 21FB LDPI
  4C: D0   STL   0   -- #64 (=#4C+#18) Nachfolgeadr. von PAR
                     -- mit Offset 0 abgespeichert, d.h.
                     -- nach Terminieren von PAR laeuft
                     -- das Programm ab #64 weiter
  -- P1 gestartet
  4D: 10   LDLP  0   -- WS-PAR geladen
  4E: 60DF STL   -1  -- mit Offset -1 abgespeichert
  50: 634F LDC   -#31
  52: 601F LDLP  -1
  54: FD   STARTP    -- P1 gestartet, Anfangsadr. ist #24
                     -- (=#55-#31), Workspace-Zeiger WS-P1
                     -- hat Offset -1 relativ zu WS-PAR.
  -- P2 gestartet
  55: 10   LDLP  0   -- WS-PAR geladen
  56: 60DB STL   -5  -- mit Offset -5 abgespeichert
  58: 634F LDC   -#31
  5A: 601A LDLP  -6
  5C: FD   STARTP    -- P2 gestartet, Anfangsadr. ist #2C
                     -- (=#5D-#31), Workspace-Zeiger WS-P2
                     -- hat Offset -6 relativ zu WS-PAR.
  -- P3 gestartet
  5D: 10   LDLP  0   -- WS-PAR geladen
  5E: 60D6 STL  -#A  -- mit Offset -#A abgespeichert
  60: 60B5 AJW  -#B  -- Workspace-Zeiger WS-P3 hat Offset
                     -- -#B relativ zu WS-PAR
  62: 630E J    -#32 -- zu #32 (=#64-#32) Anfangsadr. von
                     -- P3 springen, P3 gestartet
---------- Nachfolge des gesamten PAR-Prozesses ----------
  64:
Kasten 4

Am Code des ALT-Konstrukts erkennt man die Aufteilung in die beiden Bereiche Guard-Tests sowie Ausführungsalternativen.

...
INT x:
CHAN OF INT ein1, ein2, aus:
PAR
  ALT
    ein1 ? x
      aus ! 1
    ein2 ? x
      aus ! 2
  ...

---------- Befehlsfolge des Konstruktors ALT ----------
-- Anfang von ALT
  24: 24F3 ALT       -- ALT-Anfangszustand MinInt+1
                     -- im Workspace abgespeichert
-- Guard2 (ein2 ? x) getestet, ob schon zur Komm. bereit
  26: 71   LDL   1
  27: 53   LDNLP 3   -- Adresse von ein2 geladen
  28: 41   LDC   1
  29: 24F8 ENBC      -- Komm.Zustand von ein2 getestet
-- Guard1 (ein1 ? x) getestet, ob schon zur Komm. bereit
  2B: 71   LDL   1
  2C: 54   LDNLP 4   -- Adresse von ein1 geladen
  2D: 41   LDC   1
  2E: 24F8 ENBC      -- Komm.Zustand von ein1 getestet
  30: 24F4 ALTWT     -- warten, wenn kein Proz. bereit ist,
                     -- ueber ein1/ein2 Nachrichten zu senden.
-- Guard2 getestet, ob mittlerweile zur Komm. bereit
  32: 71   LDL   1
  33: 53   LDNLP 3   -- Adresse von ein2 geladen
  34: 41   LDC   1
  35: 4B   LDC   B   -- B ist die Sprungweite 
                     -- von ALTEND zu Zweig 2
  36: 22FF DISC      -- Komm.Zustand von ein2 getestet
-- Guard1 getestet, ob mittlerweile zur Komm. bereit
  38: 71   LDL   1
  39: 54   LDNLP 4   -- Adresse von ein1 geladen
  3A: 41   LDC   1
  3B: 40   LDC   0   -- 0 heisst kein Sprung 
                     -- von ALTEND zu Zweig 1
  3C: 22FF DISC      -- Komm.Zustand von ein1 getestet
  3E: 24F5 ALTEND    -- ALT beendet, nach dem Testergebnis zu 
                     -- dem ausgewaehlten Zweig springen
---------- Zweig 1 ----------
  -- ein1 ? x
  40: 71   LDL   1
  41: 55   LDNLP 5   -- Adresse von x geladen
  42: 71   LDL   1
  43: 54   LDNLP 4   -- Adresse von ein1 geladen
  44: 44   LDC   4   -- 4 Bytes Nachrichten
  45: F7   IN        -- eingeben
  -- aus ! 1
  46: 71   LDL   1
  47: 52   LDNLP 2   -- Adresse von aus geladen
  48: 41   LDC   1   -- Konstante 1 geladen
  49: FF   OUTWORD   -- ein Wort ausgeben
  4A: 0A   J     A   -- zu #55 (=#4B+#A) springen
---------- Zweig 2 ----------
  -- ein2 ? x
  4B: 71   LDL   1
  4C: 55   LDNLP 5   -- Adresse von x geladen
  4D: 71   LDL   1
  4E: 53   LDNLP 3   -- Adresse von ein2 geladen
  4F: 44   LDC   4   -- 4 Bytes Nachrichten
  50: F7   IN        -- eingeben
  -- aus ! 2
  51: 71   LDL   1
  52: 52   LDNLP 2   -- Adresse von aus geladen
  53: 42   LDC   2   -- Konstante 2 geladen
  54: FF   OUTWORD   -- ein Wort ausgeben
---------- Nachfolge des gesamten ALT-Prozesses ---
  55: