Unterschiede zwischen Assembler, Interpreter und Compiler

Aus C64-Wiki
Zur Navigation springenZur Suche springen

Einleitung[Bearbeiten | Quelltext bearbeiten]

Die ersten Computer der Geschichte wurden weniger programmiert, sondern eher konfiguriert. Die als Programmierung bezeichnete Tätigkeit war sehr aufwändig und erfolgte oftmals durch eine Vielzahl von Schaltern. An bis zu 16 Schaltern wurde die Adresse (binär) im Speicher eingestellt, danach wurde an weiteren acht Schaltern der Datenwert eingestellt und durch einen weiteren Taster dieser in den Speicher geschrieben. Auslesen ging genauso, nur dass anstatt der acht Schalter Leuchtdioden die Anzeige übernahmen. Hat man sein Programm Bit für Bit (im wahrsten Sinne des Wortes!) in den Speicher getastet, musste man noch den Instruction Counter der CPU auf diese Art und Weise laden und das Programm konnte starten. So wurde auch der erste Heimcomputer, der Altair 8800 auf diese Art und Weise programmiert.

Mit den immer schneller werdenden Computern, den immer aufwändigeren Programmen und insbesondere mit der Einführung der Dialogbedienung musste eine andere Form der Programmierung geschaffen werden. Die ersten Programmiersprachen entstanden (Fortran, Algol und COBOL) und diese orientierten sich an der menschlichen Sprache, um so für den Programmierer verständlicher zu sein. Die Programme wurden in Textform geschrieben und mussten dann dem Computer (z.B. durch Lochkarten) zugeführt werden. Erst später erfolgte die interaktive Programmierung am Computer selbst. Doch ein Problem blieb: Der Programmtext musste erst in eine für den Computer lesbare Form (Maschinensprache) überführt werden und hier gibt es drei Ansätze.

Assembler[Bearbeiten | Quelltext bearbeiten]

Es ist der erste Ansatz und die einfachste Form der Übersetzung. Das Programm wird zwar in Textform geschrieben, der Assemblersprache, wird aber 1:1 in Maschinensprache übersetzt. Jedem Maschinenbefehl ist damit ein Wort (Mnemonic) zugeordnet und Assembler ist daher vor allem eine lesbare Form der Maschinensprache. Verwendet wird jedoch die Logik der zugehörigen CPU und es erfolgt keine Abstraktion der Maschine, vielmehr muss die Logik des Prozessors verwendet werden. Dies bedeutet, dass der Assemblerprogrammierer die CPU-Architektur und den Hardware-Aufbau für die entsprechenden Abläufe kennen muss. Das führt auch dazu, dass Assemblercode für jede Plattform erneut entwickelt werden muss - selbst ein Programm für den C64 läuft nicht ohne Änderungen auf einem PET, obwohl beide eine zueinander kompatible CPU verwenden.

Assembler - vor allem Makro-Assembler - erleichtern dennoch die Arbeit wesentlich und deuten bereits einen Hauch von Hochsprachenprogrammierung an. Sie ersparen dem Programmierer aufwändiges Vorberechnen von Adressen durch Verwendung von Labels (bei Sprungbefehlen oder Datenzugriffen) und durch Makros lassen sich Standardcodeabfolgen beliebig oft (sogar parametrisiert) wiederverwenden ohne jedes mal neu programmiert werden zu müssen.

Vorteile:

  • Möglichkeiten: Nur in Assembler ist eine vollständige Kontrolle und Nutzung der Maschine möglich - dies verdeutlicht folgender Spruch: „Was man nicht in Assembler programmieren kann, muss man löten“.
  • Geschwindigkeit: Da Assemblerprogramme unmittelbar auf CPU-Ebene und damit sonst keinen Ballast mit sich tragen, ist Assembler grundsätzlich sehr schnell.
  • Programmgröße: In keiner anderen Sprache lassen sich solch kompakte Programme wie in Assembler schreiben.

Nachteile:

  • Aufwand: Da in Assembler nahezu jede Aufgabe aus vielen Assemblerschritten zusammengesetzt werden muss, ist die Assemblerprogrammierung aufwändig. Hinzu kommt, dass der Ablauf oder ein Algorithmus sich im Gegensatz zu Hochsprachen nur schwer aus dem Code ersehen lässt.
  • Fehlerträchtigkeit: Assemblerprogrammieren ist ein Drahtseilakt ohne Netz. Fehler führen fast immer zum Absturz des Rechners. Die Fehlersuche ist mitunter so aufwändig, dass man sie seinem schlimmsten Feind nicht wünschen möchte.
  • Fachkenntnis: Um erfolgreich in Assembler programmieren zu können, muss die Maschine verstanden und der Umgang mit anderen Zahlensystemen (Hex, Binär, Oktal) in Fleisch und Blut übergegangen sein. Auch intime Kenntnis über die Verwendung und das Verhalten von verschiedenster Peripherie (für eine eine jeweilige CPU-Familie) ist langfristig unvermeidbar.

Compiler[Bearbeiten | Quelltext bearbeiten]

Die Forderung nach systemunabhängigen Sprachen mit entsprechenden Formulierungsmöglichkeiten von Datenstrukturen, Kontrollstrukturen, später Funktionen und Prozeduren, lassen sich durch Assembler nicht mehr umsetzen. Hier spricht man von Hochsprachen, und eine 1:1 Übersetzung ist nicht mehr möglich. Compilersprachen sind der zweite Ansatz, trennen die Beschreibung der Lösung von der Hardware und leisten daher die semantische Umsetzung der Hochsprachenlogik in die Assemblersprache. Häufig besteht das Ergebnis eines Compilers aus Assemblercode, der erst in einem nachgelagerten Assemblerlauf in Maschinencode übersetzt wird. Dieser zweite Lauf ist bei vielen Compilern transparent, weil der Assembler oftmals direkt in den Compiler integriert ist. Diese Trennung ist wichtig, da somit gewährleistet wird, dass eine Hochsprache auf mehreren Zielplattformen kompilierbar ist. Das Ergebnis muss nicht zwingend Maschinensprache sein, sondern ist heute oftmals ein Bytecode, der dann durch eine simple und hocheffiziente virtuelle Maschine für diesen Pseudomaschinencode ausführt wird - Java und .NET gehen heute diesen Weg.
Im Gegensatz zum Assemblerprogrammierer muss der Hochsprachenentwickler im Idealfall keine Kenntnis über die darunterliegende Hardware und Systemarchitektur aufweisen.

Reine Compilersprachen sind: C/C++, Pascal

Vorteile:

  • Relativ schnell: Vor allem C und Pascal eignen sich sowohl für schnelle Kompilate als auch für effiziente und schnelle Compilerläufe.
  • Portabel: Oftmals lassen sich Programme ohne Änderung auf andere Plattformen transferieren - sie müssen dort nur neu kompiliert werden.

Nachteile:

  • Aufwändig: Nach jeder Änderung des Quellcodes erfolgen die Schritte Kompilieren, Assemblieren und Linken. Da ein Compiler umfangreicher als ein Assembler ist, war es in der 8-Bit-Ära oftmals nicht möglich, alle drei im Speicher zu halten, und somit mussten die Schritte hintereinander durch Laden der einzelnen Komponenten durchgeführt werden, wobei die Zwischenschritte allein oftmals viel Zeit beanspruchten.
  • Fehlersuche: Sie ist zwar einfacher als bei Assemblern, da zumindest eine syntaktische Analyse während des Kompilierens durchgeführt wird - dennoch: Das „Dreierspiel“ Kompilieren, Assemblieren, Linken ist nach jeder Fehlerbeseitigung notwendig.
  • Speicherproblem: Die 8-Bitter der damaligen Zeit hatten selten mehr als 64KByte Speicher - das ist für viele Compilersprachen einfach zu wenig. So unterstützten die meisten C-Compiler für den C64 nur ein Subset der Funktionen nach K&R- oder ANSI-Standard.

Interpreter[Bearbeiten | Quelltext bearbeiten]

Assembler und Compiler haben gemeinsam, dass das Ergebnis ein ausführbares Programm ist, welches genau das macht, was der Programmierer im Quellcode vorgeben hat. Die Idee eines Interpreters ist es, auf die Übersetzung des Quellcodes (abgesehen von einer eventuellen effizienteren Zwischendarstellung mittels Token) zu verzichten und zwischen dem Quellcode und der Maschine eine Softwareschicht einzuziehen (eben den Interpreter), der zur Laufzeit auf Basis des Quellcodes Routinen aufruft, die genau das machen, was der Entwickler beschrieben hat. Entgegen weit verbreiteter Vorstellungen übersetzt der Interpreter nicht zur Laufzeit in Maschinencode - er ist kein Compiler/Assembler, produziert somit kein Kompilat und nachfolgend keinen Maschinencode und gehört damit auch nicht zu einem der zuvor genannten Ansätzen. Ein Interpreter ist schlicht und ergreifend ein Programm. Trifft der Interpreter z.B. auf ein PRINT, dann wird im Interpreter ein Codeabschnitt aufgerufen, der den Text auf den Bildschirm schreibt. Jeder BASIC-Befehl - um BASIC als Beispiel zu nennen - hält somit sein Äquivalent in Form einer Subroutine innerhalb des Interpreters bereit. Die wenige Ausnahmen betreffen syntaktische Strukturen oder Elemente der Sprache (z.B. THEN, ELSE, TO).

Interpretersprachen sind: BASIC (alte Versionen), Lisp, Logo, Perl, PHP, Ruby, Scripting bei Shells

Vorteile:

  • „Easy to use“: Ein BASIC-Interpreter war in den 8-Bit-Rechnern bereits eingebaut und man konnte gleich nach dem Einschalten loslegen. Es wurde keine Zusatzsoftware benötigt, die vorher aufwändig geladen werden musste.
  • Fehlersuche: Ein Interpreter hält die Ausführung einfach an, wenn ein Fehler auftritt. Der Fehler kann direkt beseitigt und das Programm ohne den Aufwand eines Compilerlaufs sofort neu gestartet werden.

Nachteile:

  • Ausführungsgeschwindigkeit: Der Grund für die Langsamkeit von Interpretersprachen sind nicht die aufgerufenen Funktionen, sondern das zur Laufzeit notwendige, immer wiederkehrende Parsing des Quellcodes.
  • Speicherverbrauch: Der Interpreter benötigt einen Teil des Speichers zur Laufzeit selbst.
  • Möglichkeiten: Der Interpreter ist das Limit! Es kann nur das programmiert werden, für das der Interpreter entsprechende Befehle vorgesehen hat. Das war auch der Grund, warum BASIC 2.0 des C64 eben keine Grafik oder Sound direkt unterstützte und dafür Programmierer entweder auf entsprechend ergänzte Systemroutinen - nachgeladen oder entsprechend per POKE in den Speicher geschrieben - oder auf eine BASIC-Erweiterung wie z.B. Simons Basic zurückgreifen mussten.

Compreter (Just-In-Time-Compiler)[Bearbeiten | Quelltext bearbeiten]

Compreter sind der dritte Ansatz, den es damals zu Zeiten des C64 noch nicht gab. Compreter machen eigentlich genau das, was man damals den Interpretern zu sprach: Der Quellcode wird nicht vorab, sondern erst zur Laufzeit (Just-In-Time = JIT) in Maschinensprache übersetzt. Diese recht junge Technologie ist erst seit Ende der 1990er möglich und populär geworden, weil die Computer schnell genug wurden, um dies in Echtzeit zu ermöglichen. Just-In-Time-Compiler verwenden auch die virtuellen Maschinen von Java und .NET für den Bytecode - nicht aber die Sprachen selbst! Java und C# beispielsweise sind reine Compilersprachen.

Vertreter dieser Gattung sind: Java, alle CLR (.NET) Sprachen (Visual Basic, C#, C++.NET).

Vorteile:

  • Entwicklungsaufwand: Vereint die Vorteile einer Interpretersprache (interaktive Fehlersuche) mit der eine Compilersprache (Performance).
  • Plattformübergreifend: Ist für eine Plattform eine virtuelle Maschine der Sprache implementiert und damit verfügbar, läuft der Code dort ohne Änderungen.

Nachteile:

  • „In the Box“: Die speziellen Möglichkeiten der darunterliegenden Hardware können nicht oder nur durch Umwege genutzt werden.
  • Speicherverbrauch: Um das Programm laufen zu lassen, muss stets die gesamte Umgebung für die virtuelle Maschine der Sprache geladen sein.

Hybride Technologien[Bearbeiten | Quelltext bearbeiten]

War noch in den 1990er-Jahren die Trennung zwischen den einzelnen oben genannten Techniken klar definiert, gibt es heute immer mehr hybride Ansätze. BASIC wurde als Visual Basic zur Compretersprache, Java und C# werden erst compiliert und dann in einer virtuellen Umgebung ausgeführt (Kombination von Interpretation und JIT-Compiling).

Aber schon in den 1960er-Jahren entstand eine Sprache, die der Hybridstatus anhaftet, nämlich Forth, welche eine Kombination aus Compiler und einem Inner Intepreter, der den "Zwischencode" abarbeitet, darstellt. Als Idee liegt hier die schnelle Portierungsmöglichkeit und die Kompaktheit des Gesamtsystems zu Grunde, auch wenn dies mit Abstrichen bei der Ausführungsgeschwindigkeit verbunden ist.

Auch für das gute alte BASIC gab es Compiler - diese waren aber mehr von der Art, die Pseudo-Code produzieren. Letztlich ersetzten sie den Quellcode durch P-Code oder Subroutinenaufruffolgen und ersparten damit lediglich die aufwändigsten Aufgaben des Parsings der sonst zur Interpreterlaufzeit passiert. Der im ROM vorliegende Interpreter ist dabei weiterhin in notwendig, um die tatsächlichen Basic-Befehle abzuarbeiten oder die Datentypen zu unterstützen, z.B. die Fließkommaarithmetik.

C++ gibt es auch als .NET-Version, die sich in Anwendung und Syntax deutlich von nativem C++ unterscheidet.

Auch die Kombination zwischen einer Compilersprache und Assembler gab es schon zu Zeiten des C64: Alle guten C-Compiler ermöglichen Inline-Assembling, also die Verwendung von Assemblercode direkt in C.
Selbst reine Interpretersprachen wie Interpreter-Basic ermöglichten es, Programme in Assembler oder C aufzurufen oder direkt auf den Speicher zuzugreifen - POKE/PEEK und SYS lassen grüßen. Das Konzept in BBC-Basic sieht sogar einen Inline-Assembler vor, wo Maschinencode direkt mit dem Basic-Programm verschmilzt.