Forum: Projekte & Code Ein Bytecode-Interpreter für ATMega644 u.a.


von Florian K. (makrocontroller)


Angehängte Dateien:

Lesenswert?

Eigentlich wollte ich ein Text-basiertes Spiel, das ich vor langer Zeit 
mal auf dem C64 in BASIC programmiert habe, auf einem ATMega644 zum 
Laufen bringen, nachdem ich es früher schon mal in C auf einen PC 
portiert hatte. Ich stellte aber fest, dass man ziemlich schnell an die 
Grenzen des Programmspeichers kommt (allerdings muss ich sagen, dass ich 
in dem C-Programm auch großzügig long Integers benutzt hatte, obwohl es 
oftmals auch kleinere Datentypen getan hätten).

Da das Programm nicht zeitkritisch ist, entschloss ich mich, zur 
Reduzierung des Speichers einen Bytecode-Interpreter zu schreiben. 
Außerdem programmierte ich einen Compiler, der aus einer C-ähnlichen 
Programmiersprache Bytecode erzeugt. Genauer gesagt ist es kein 
eigenständiger Compiler, sondern durch C++-Klassen, Operatorüberladungen 
und einige #defines habe ich den C++-Compiler dazu überredet, aus einem 
C-ähnlichen Programm ein auf dem PC lauffähiges Programm zu erzeugen, 
welches nach dem Start schließlich Bytecode erzeugt. Eine Alternative 
wäre z.B. LCC oder ein Parser-Generator; da ich mit beiden jedoch keine 
Erfahrung habe, bemühte ich lieber den C++-Compiler. Leider entspricht 
die Syntax nicht 100 %ig ANSI C, v.a. bei der Definition von Variablen, 
Funktionen, structs usw.. Ausdrücke (mathem., logische) sind jedoch fast 
100 % ANSI C kompatibel, weil man die C++-Operatoren so gut überladen 
kann. Einzige Ausnahme ist das x ? y : z -Konstrukt; man muss statt 
dessen cond(x,y,z) schreiben.

Ich habe den Bytecode-Interpreter getestet, indem ich die 
float64-Bibliothek nach kleinen Syntax-Anpassungen mit dem Compiler 
kompiliert habe. (Fast) alle Funktionen der Bibliothek werden mit 
Zufallszahlen aufgerufen, und die Ergebnisse der unter avr-gcc 
kompilierten Funktionen werden mit den jeweiligen Ergebnissen der nach 
Bytecode kompilierten Funktionen verglichen. Bei irgendeiner Abweichung 
wird eine Fehlermeldung ausgegeben. Nach mehreren Stunden Laufzeit hat 
sich kein Fehler ergeben. Aber bei dem nicht gerade kleinen Programm 
(Compiler + Interpreter) werden sich leider durchaus irgendwo anders 
noch bugs versteckt haben. Für viele Anwendungen ist es jedoch zu 
langsam, wenn man die float64-Bibliothek auf dem Bytecode-Interpreter 
laufen lässt. Es werden ca. 20 Additionen und 12 Divisionen pro Sekunde 
ausgeführt. Sinus-Berechnungen und Konversion nach dezimal sind noch 
langsamer. Die reine float64-Bibliothek (ohne den Code zum Testen) hat 
einen Bytecode von ca. 5700 Bytes. Der Code wurde durch ein 
Kompressionsverfahren vom Compiler komprimiert und wird zur Laufzeit 
dekomprimiert. Winzip verringert in den Standardeinstellungen die 
Codegröße um ca. 11 % (aber der Bytecode-Interpreter kann Winzip 
gepackte Dateien nicht "on the fly" entpacken). Durch gewisse 
Verbesserungen am Komressionsverfahren könnte man jedoch den Bytecode 
noch um einige % kleiner machen (parameterbehaftete Code-Makros). Der 
Bytecode-Interpreter ist bereits in der Lage, parameterbehaftete 
Code-Makros zu entpacken; das entsprechende Kompressionsprogramm werde 
ich ggf. noch in den Compiler einbauen.

Während die float64-Bibliothek als Bytecode wie gesagt ca. 5700 Bytes 
braucht, hat sie als C-Code nach Kompilierung mit avr-gcc ca. 26000 
Bytes; somit ist das Verhältnis ca. 20-25 %. Bei anderen Programmen, die 
vorwiegend mit 8- oder 16-Bit Datentypen auskommen, wird die 
Codegrößenersparnis jedoch deutlich geringer sein, und wenn man die 
Größe des Bytecode-Interpreters (ca. 15000 Bytes) hinzurechnet, kann 
sich unterm Strich vielleicht gar keine Ersparnis ergeben. 
Wahrscheinlich ist der Einsatz des Bytecode-Interpreters nur bei 
Programmen sinnvoll, die viel mit 32 oder 64-Bit Datentypen rechnen. 
Denn ein 8-Bit Prozessor muss diese aus vielen 8-Bit Kommandos 
zusammensetzen. Beim Bytecode-Interpreter dagegen ist die Codegröße 
unabhängig von der Breite der Datentypen (wenn man mal davon absieht, 
dass 64-Bit Konstanten natürlich auch mehr Speicher als kleinere 
Konstanten benutzen).

Der Bytecode-Interpreter wurde in Bezug auf kleinen Code optimiert, was 
auch zu Lasten der Ausführungsgeschwindigkeit geht. Für zeitkritsche 
Programme ist er daher nicht geeignet. Wenn jedoch nur Teile 
zeitkritisch sind, kann man sich eventuell dadurch behelfen, dass der 
Bytecode-Interpreter "externe" Funktionen aufruft, also Funktionen, die 
mit avr-gcc kompiliert sind und daher schneller sind.

Momentan ist noch wenig dokumentiert. Je nach Zeit und Lust werde ich 
das ggf. in nächster Zeit nachholen. Wer jedoch mit C vertraut ist (und 
ggf. zumindest Grundkenntnisse in C++ hat), wird jedoch bei Betrachten 
des Quellcodes der float64-Bibliothek, den der Bytecode-erzeugende 
Compiler akzeptiert, schon mal einen Eindruck bekommen, wie die Syntax 
des C-Dialekts sich von ANSI C unterscheidet. Zusätzlich habe ich noch 
ein Hallo-Welt Programm beigefügt. Bei beidem Programmen gibt es jeweils 
einen Ordner "PC" und einen Ordner "ATMega644". Wenn man Änderungen am 
Quellcode durchgeführt hat, muss man zunächst die PC-Version öffnen (mit 
Visual Studio 2005/2008 oder mit gnu C++ (DevCpp)), den C++-Kompiler 
starten und dann das Programm ausführen. Der Bytecode wird aktuell in 
eine Datei "d:\program.txt" geschrieben; den Dateinamen kann mit in der 
Datei Main.cpp ändern. Den Inhalt dieser Datei (den Bytecode) muss man 
dann kopieren und ihn in das Array mem[] der ATMega644-Version einfügen 
(Ordner ATMega644, Datei main.cpp). Wichtig ist noch, die ggf. geänderte 
Adresse der auszuführenden Funktion in die Anweisung 
ip.execute_bytecode(...); (am Ende von main.cpp) einzutragen. Nun muss 
unter avr-gcc kompiliert werden, und nach dem Hochladen auf den ATMega 
sollte es hoffentlich funktionieren.

Die Kompilierung dauert länger, wenn in der Datei BytecodeInterpreter.h 
das #define USE_CODE_MAKRO_COMPRESSION gesetzt ist; allerdings wird dann 
auch etwas kleinerer Code erzeugt. MS Visual Studio schafft es nicht 
(oder nicht in endlicher Zeit...), das Programm in der Release-Version 
zu kompilieren (genauer: zu linken), DEV CPP (gnu c++) dagegen schon. In 
der Release-Version ist die Kompilierung bei gesetztem #define 
USE_CODE_MAKRO_COMPRESSION deutlich schneller.

Ein Nachteil des durch C++-Klassen realisierten Compilers ist, dass bei 
Fehlermeldungen nicht ohne weiteres die Zeilennummer ermittelt werden 
kann, in der der Fehler auftritt (da nützt auch das Makro _LINE_ 
nichts). Um dennoch zu erfahren, wo der Fehler auftrat, kann man in der 
C++-Entwicklungsumgebung in der Funktion report_error() der Datei 
BytecodeTargetedCompiler.h einen Breakpoint setzen und den C++-Compiler 
und dann das erzeugte Programm im Debug-Modus starten. Bei einem Fehler 
stoppt der Debugger dort, und wenn man in Call Stack etwas zurückgeht, 
kommt man an die Stelle, wo der Fehler ist.

Aktuell ist aufgrund eines bugs bzw. eines noch nicht implementierten 
Details die Größe des Bytecodes auf 32 kBytes beschränkt. Es wird eine 
der nächsten Aufgaben sein, dies zu beheben, so dass prinzipiell auch 
deutlich größere Codes möglich wären (sofern genügend Flash vorhanden 
ist).

von Mitesser (Gast)


Lesenswert?

Warum nimmst du nicht den Maschinencode als Bytecode :-/

von Florian K. (makrocontroller)


Lesenswert?

Weil der Maschinencode eben in solchen Fällen, wenn viel mit 32/64 Bit 
Datentypen gerechnet wird, deutlich mehr Speicher in Anspruch nimmt. Ein 
8-Bit Prozessor wie ein ATMega muss z.B. eine 32 Bit Addition aus 4 
8-Bit Additionen zusammensetzen. Der Bytecode-Interpreter hat z.B. einen 
ADD-Befehl, welcher die beiden obersten Stackelemente addiert und das 
Ergebnis wieder auf dem Stack ablegt. Die Codebreite des ADD-Befehls ist 
unabhängig von der Bit-Breite der Operanden.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Florian K. schrieb:
> Der Bytecode-Interpreter hat z.B. einen
> ADD-Befehl, welcher die beiden obersten Stackelemente addiert und das
> Ergebnis wieder auf dem Stack ablegt. Die Codebreite des ADD-Befehls ist
> unabhängig von der Bit-Breite der Operanden.

Wie "mächtig" kann man sich die Maschine denn vorstellen an 
darstellbaren Operationen/Aktionen?

Ist das eine Auswertung auf einem Kellerspeicher, also einem eigens 
reservierten RAM-Bereich, oder kann man damit ganze Programme 
formulieren inclusive Kontrollstrukturen, Prozeduraufrufen und 
Speicherzugriffen?

Ist es möglich, über ein Interface "normale" C-Funktionen aufzurufen 
bzw. vom C-Code aus den Interpreter ein Schnippel Bytecode zu übergeben, 
ausführen zu lassen und Rechenergebnisse zu bekommen?

Johann

von Florian K. (led)


Lesenswert?

Zur Info: Ich habe nun einen neuen Nickname (makrocontroller ==> LED).

> Wie "mächtig" kann man sich die Maschine denn vorstellen an
> darstellbaren Operationen/Aktionen?
Die Mächtigkeit eines einzelnen Befehls ist vergleichbar mit einem 
Befehl eines 64 bit Mikroprozessors. Es sind arithmetische, logische 
Operatoren, bedingte/unbedingte Sprünge, Funktionsaufrufe usw. 
implementiert. Die Maschine arbeitet aber im Gegensatz zu einem 
Mikroprozessor Stack- und nicht registerorientiert. Eine Liste der 
Opcodes (Befehle) des Bytecode-Interpreters erhält man, indem man in der 
Datei BytecodeInterpreter.h nach enum Opcodes sucht. Nicht alle sind 
dokumentiert, aber teilweise selbsterklärend, weil vom Namen ähnlich wie 
Assembler-Befehle.

> Ist das eine Auswertung auf einem Kellerspeicher, also einem eigens
> reservierten RAM-Bereich, oder kann man damit ganze Programme
> formulieren inclusive Kontrollstrukturen, Prozeduraufrufen und
> Speicherzugriffen?
Man kann ganze Programme ausführen, d.h. fast alles, was man in ANSI C 
programmieren kann, kann der Bytecode-Interpreter nach Compilierung auch 
ausführen (mit Ausnahme von z.B. Hardware-Zugriffen (Ports usw.); diese 
müssen durch Aufruf externen C-Funktionen erfolgen).
Der ausführbare Code wird im FLASH abgespeichert und vom Bytecode 
Interpreter über die vom Benutzer übergebene Funktion(sadresse) 
get_code_nibble() ausgelesen. Der RAM wird nicht für Code, sondern nur 
für Daten verwendet, z.B. für den Stack. Der Benutzer übergibt beim 
Aufruf des Bytecode-Interpreters die Startadresse zweier Arrays im SRAM, 
welcher der Bytecode-Interpreter dann als Stack bzw. für globale 
Variablen nutzt.

> Ist es möglich, über ein Interface "normale" C-Funktionen aufzurufen
> bzw. vom C-Code aus den Interpreter ein Schnippel Bytecode zu übergeben,
> ausführen zu lassen und Rechenergebnisse zu bekommen?

Man kann sowohl von einem auf dem Bytecode-Interpreter laufenden 
Programm normale C-Funktionen aufrufen als auch umgekehrt, d.h. von 
einem C-Programm Funktionen des Bytecode-Interpreters aufrufen. Beim 
letzterem können momentan jedoch noch keine Funktionsargumente übergeben 
werden; es dürfte aber kein größerer Aufwand sein, dies noch einzubauen.
Wenn man vom Bytecode-Programm normale C-Funktionen aufrufen will, muss 
dies momentan über eine Hilfsfunktion extern_function_invoker() 
erfolgen. Wenn der Bytecode-Interpreter den Opcode CALLEXT abarbeitet, 
ruft er die vom Benutzer bereitgestellte Funktion 
extern_function_invoker() auf und übergibt die Nummer der aufzurufenden 
C Funktion. Leider kann der Bytecode-Interpreter nicht selbst die 
C-Funktion aufrufen, weil es in C keine Möglichkeit gibt, eine Funktion 
mit einer erst zur Laufzeit bekannten Zahl von Parametern aufzurufen. 
Wenn jemand jedoch Erfahrung in AVR Assembler hat (im Gegensatz zu mir) 
und außerdem weiß, nach welchem Schema AVR-GCC die Parameter einer 
Funktion in Registern bzw. auf dem Stack übergibt, könnte er eventuell 
durch Assembler-Code erreichen, dass der Bytecode-Interpreter C 
Funktionen automatisch (ohne dem Umweg über extern_function_invoker()) 
aufruft.

Momentan kann ein Bytecode-Programm mit einem normalen C Programm noch 
nicht über globale Variablen kommunizieren. Ich werde jedoch bei 
Gelegenheit in den Compiler einbauen, dass er automatisch #defines 
erzeugt, mit welchen man dann sehr einfach von einem C Programm auf die 
globalen Variablen zugreifen kann, die im Bytecode-Programm verwendet 
werden und die in dem Array memory[] abgespeichert werden, das beim 
Aufruf des Bytecode-Interpreters übergebenen wurde.

von Mitesser (Gast)


Lesenswert?

>Weil der Maschinencode eben in solchen Fällen, wenn viel mit 32/64 Bit
>Datentypen gerechnet wird, deutlich mehr Speicher in Anspruch nimmt.

Dafür ist es langsam...

Also, es zwingt Dich niemand alle Befehle zu unterstützen.
Bytecode kann ja schon Sinn machen, ich denke da auch an BASIC-Zeiten 
mit 1kByte RAM (PET, C64). Auch die FORTH-Sprache ist ein schönes 
Beispiel und warum unterstützt du nicht FORTH. Oder warum machst du dir 
nicht ein paar Makros für den Assembler, das geht doch auch.


>Der Bytecode-Interpreter hat z.B. einen
>ADD-Befehl, welcher die beiden obersten Stackelemente addiert und das
>Ergebnis wieder auf dem Stack ablegt.

Die Ergebnisse stehen so in Registern, das ist besser ->Harvard 
Architektur. Der Prozessor vom C64 ist ungeeignet für Compilersprachen, 
Bytecode für Basic die einzige Möglichkeit dem Benutzer das Ding 
schmackhaft zu machen.



Skurril, Java hat einen JIT-Compiler: Eine Hürde bei den Mikros ist die 
fehlende Möglichkeit für selbstmodifizierenden Code, also im RAM kann 
kein Code stehen, was trickreich gelöst werden müsste.

->MikroVM und BASIC-Briefmarke?

von Matti (Gast)


Lesenswert?

... also im RAM kann kein Code stehen ...

Es gibt Controller die Code im RAM ausführen können (z. B. 51er, ARM7).

von Mitesser (Gast)


Lesenswert?

> Leider kann der Bytecode-Interpreter nicht selbst die
>C-Funktion aufrufen, weil es in C keine Möglichkeit gibt, eine Funktion
>mit einer erst zur Laufzeit bekannten Zahl von Parametern aufzurufen.

Kennst du die Ellipse (...) ?

Also ich hatte schon mal einen C++ Compiler gebaut, der das konnte was 
du möchtest. Es gab mehrere Möglichkeiten der Codeerzeugung. Compilat 
und Skript konnten immer transparent kommunizieren, das war erste 
Bedingung und auch der Zweck des Ganzen, leider ist das nicht 
"bulletproof", also der Computer konnte damit abstürzen. Ich habe es 
über Exceptionhandling abgefangen. Bei der Entwicklung hatte ich LISP im 
Hinterkopf, es gab eine EVAL-Funktion (Sprache Groovy, PHP)

Einzelne Skriptfunktionen konnten per Compilerschalter ist 3 Modi 
erzeugt werden:

- Abarbeitung des RDP-Tree im Skriptmodus (genausoschnell wie Java)
- Erzeugung von VM-Code (Bytecode) mit der Möglichkeit das zu speichern
- Erzeugung von Maschinencode - nicht optimiert

Weil ich eigentlich helfen möchte finde ich du solltest besser 
gleich_irgendeinen_ Standard unterstützen (Java, Javascript, PHP, FORTH, 
Basic, C) sonst wird das ein Rohrkrepierer.

Du könntest z.B. GCC dazu bringen deinen Bytecode zu erzeugen, das wäre 
dann für einen großen Nutzerkreis und auch nicht mehr auf Mikros 
beschränkt. Der Aufwand hält sich - vermutlich gegen deine Erwartungen - 
in Grenzen. Gut die 2 Teile müssen sich schon etwas auf einander 
zubewegen aber dafür wirfst du die Last des Compilerbaus ab.

Leider liegt mein Projekt schon mehr als 15 Jahre zurück und der Markt 
hatte es damals nicht angenommen, die Nutzung blieb auf ein Projekt 
beschränkt.

von Florian K. (led)


Lesenswert?

Ich verwende einen C-Dialekt (als Quellsprache für den Compiler, der 
Bytecode erzeugt), weil ich mich mit C gut auskenne und ein bereits 
existierendes C Programm speichersparend auf einem µC zum Laufen bringen 
will. Wer sich mit Forth gut auskennt, wird wahrscheinlich Forth 
bevorzugen. Zu anderen Vor- und Nachteilen von Forth vs. C kann ich 
nichts sagen, da ich Forth nicht kenne.

Der Bytecode-Interpreter implementiert nicht alle Befehle irgendeines 
Mikroprozessors (weder vom C64 noch von irgend einem andereren), sondern 
den Vergleich habe ich nur gemacht, um eine Vorstellung über die 
"Mächtigkeit" eines Befehls zu vermitteln.

Der Bytecode-Interpreter ist tatsächlich langsam. Dies hat verschiedene 
Gründe. Allgemein habe ich kleinen Code gegenüber höhere Geschwindigkeit 
vorgezogen, weil ich von vornherein auf nicht zeitkritische Anwendungen 
abzielen wollte. Es wäre sicher auch möglich, einen Bytecode-Interpreter 
mit ähnlichem Befehlssatz mehr auf Geschwindigkeit statt auf Codegröße 
zu optimieren, um ihn z.B. an einem µC zu betreiben, welcher Code 
asynchron (d.h. nicht synchron mit dem Bustakt) aus einem externen Flash 
liest.

Beim Test der float64-Bibliothek habe ich ermittelt, dass ein einzelner 
Befehl des Bytecode-Interpreters durchschnittlich ca. 3000 
Prozessortakte in Anspruch nimmt. Das erscheint schon sehr hoch. Ein 
Grund ist sicher auch, dass parallel zu Daten (Variablen) auch der Typ 
der jeweiligen Variable im SRAM abgespeichert wird, um den Typ nicht 
jedes Mal im Code angeben zu müssen. Vorteil ist kleinerer Code, 
Nachteil geringere Geschwindigkeit und genau 25% mehr Speicher für 
(lokale) Variablen, die auf dem Stack gespeichert werden. Globale 
Variablen brauchen jedoch nicht mehr SRAM Speicher, weil der Typ meist 
aus der Adresse ermittelt werden kann (außer bei globalen 
struct-Objekten).

Aber wenn jemand Interesse an meinem Bytecode-Interpreter hat, könnte er 
ggf. die Geschwindigkeit erhöhen, indem er Teile davon in Assembler 
schreibt (ich habe keine Erfahrung mit AVR Assembler). Und der 
Interpreter ist ja vom Codeumfang deutlich "übersichtlicher" als der 
Compiler.

Die Ellipse (...) für Funktionsargumente kenne ich. Man kann sie aber 
nur nutzen, um innerhalb von Funktionen wie z.B. printf eine zur 
Laufzeit unbekannte Zahl von Argumenten auszulesen. Aber wenn die 
Argemente und Datentypen einer Funktion z.B. in einem Array
stehen (Anzahl und Datentypen zur Comiplierzeit unbekannt) und die 
Adresse einer Funktion bekannt ist, kann man der Funktion zwar die 
Adresse des Arrays übergeben, sie aber nicht mit den "richtigen" 
Parametern aufrufen.

Hört sich nicht schlecht an, dass du schon mal einen C++-Compiler 
geschrieben hast. Zum Theme GCC: Ich stelle es mir sehr aufwändig vor, 
mich erst mal in den Quellcode von GCC einzuarbeiten, um ihn dann dazu 
zu bringen, Bytecode zu erzeugen. Das hätte dann allerdings den Vorteil, 
dass man sogar C++-Code kompilieren kann. Ich gehe mal davon aus, dass 
es weniger Aufwand machen würde, einen Compiler mit einem 
Parser-Generator zu schreiben. Hast Du Erfahrung mit Parser-Generatoren 
und kannst Du einen empfehlen ?

Florian

von Mitesser (Gast)


Lesenswert?

>Hört sich nicht schlecht an, dass du schon mal einen C++-Compiler
>geschrieben hast.

Ist nicht wesentlich anders als C. Die Funktionen haben eine Signatur, 
d.h. die Argumente sind im Namen codiert. Es gibt zusätzliche 
Prioritäten/Regeln für die Abarbeitung, die für den Menschen 
offensichtlich schwieriger zu durchschauen sind als für ein Programm. 
Manche behaupten sogar der Compiler "denkt". Mein Ding war unter Borland 
C++ lauffähig unter Visual C++, gcc müsste man viel überarbeiten, 
praktisch neu schreiben.

>Zum Theme GCC: Ich stelle es mir sehr aufwändig vor,
>mich erst mal in den Quellcode von GCC einzuarbeiten, um ihn dann dazu
>zu bringen, Bytecode zu erzeugen.

Da kommt man nicht drumrum...

>Das hätte dann allerdings den Vorteil,
>dass man sogar C++-Code kompilieren kann.

Das ist nicht unbedingt ein Vorteil. Ich sehe C++ nur als Verkleidung 
und versuche soweit wie möglich alles in C zu halten. Beispielsweise 
läuft die Überladung des operators [] auf eine entsprechend benannte 
C-Funktion, "funktionale Programmiersprachen" lassen grüssen.

>Ich gehe mal davon aus, dass
>es weniger Aufwand machen würde, einen Compiler mit einem
>Parser-Generator zu schreiben.

Das ist schon richtig. So macht es gcc und mit zusätzlichen Konstrukten. 
Auch awk, perl und wie sie alle heissen. Viel schlimmer ist, keine Sau 
hat sich mein durchaus interessantes Teil auch nur angeguckt. Zugegeben 
damals gab es noch kein Internet so wie heute und Java hiess noch Oak, 
Konkurrenzprodukte gab es schon jeher (USCD-Pascal).

>Hast Du Erfahrung mit Parser-Generatoren
>und kannst Du einen empfehlen ?

->lex, flex, bison, byacc, gibt es auch lauffähig unter Windows.
Die sind schon eine Erleichterung. Ich habe aber nur einen kleinen, 
eigenen "recursive descent parser" eingesetzt. Die Compilerbauertools 
sind multimegabyte gross und dienen auch mitunter zu Benchmarkzwecken.

Schau Dir auch mal "CINT" an. Das gab es mal unter Linux. Ich sehe jetzt 
gerade läuft das unter der Bezeichnung "ROOT" von Masaharu Goto. Da 
kannst du mal sehen wovon es abhängt, wie weit sich ein Produkt 
entwickelt.

Deine Sache ist schon interessant ich muss es mir demnächst genauer 
anschauen....

Gruss

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Mitesser schrieb:
> Weil ich eigentlich helfen möchte finde ich du solltest besser
> gleich_irgendeinen_ Standard unterstützen (Java, Javascript, PHP, FORTH,
> Basic, C) sonst wird das ein Rohrkrepierer.
>
> Du könntest z.B. GCC dazu bringen deinen Bytecode zu erzeugen, das wäre
> dann für einen großen Nutzerkreis und auch nicht mehr auf Mikros
> beschränkt. Der Aufwand hält sich - vermutlich gegen deine Erwartungen -
> in Grenzen. Gut die 2 Teile müssen sich schon etwas auf einander
> zubewegen aber dafür wirfst du die Last des Compilerbaus ab.

Florian K. schrieb:

> Die Ellipse (...) für Funktionsargumente kenne ich. Man kann sie aber
> nur nutzen, um innerhalb von Funktionen wie z.B. printf eine zur
> Laufzeit unbekannte Zahl von Argumenten auszulesen. Aber wenn die
> Argemente und Datentypen einer Funktion z.B. in einem Array
> stehen (Anzahl und Datentypen zur Comiplierzeit unbekannt) und die
> Adresse einer Funktion bekannt ist, kann man der Funktion zwar die
> Adresse des Arrays übergeben, sie aber nicht mit den "richtigen"
> Parametern aufrufen.
>
> Hört sich nicht schlecht an, dass du schon mal einen C++-Compiler
> geschrieben hast. Zum Theme GCC: Ich stelle es mir sehr aufwändig vor,
> mich erst mal in den Quellcode von GCC einzuarbeiten, um ihn dann dazu
> zu bringen, Bytecode zu erzeugen. Das hätte dann allerdings den Vorteil,
> dass man sogar C++-Code kompilieren kann. Ich gehe mal davon aus, dass
> es weniger Aufwand machen würde, einen Compiler mit einem
> Parser-Generator zu schreiben.

Ein gcc-Port kam mir auch gleich in den Sinn; daher auch meine Frage 
nach der "Mächtigkeit" des Instruktionssatzes. Für gcc scheint alles 
vorhanden zu sein:
-- Load/Store/Move (zwischen Speicher und/oder Registern,
   Konstanten, Adressen)
-- Arithmetik (Shift, Plus, Minus, Vergleich)
-- Logik (And, Or, Xor, Not)
-- Bedingte und unbedingte (direkte und indirekte) Sprünge
   (Branch, Goto, Labels)
-- Call (direkt, indirekt)/Return
-- Sign-Extend, Zero-Extend (zb von 8-Bit Wert zu 16-Bit Wert)

Allerdings geht gcc von einer Registermaschine aus, nicht von Befehlen, 
die auf einem Stapel operieren.

Wenn man weiß, wie man's anzupacken hat, bekommt man einen null-gcc 
(also einen, der eine leere Funktion übersetzen kann) schon innerhalb 
einer Woche. Für eine erste funktionieren Version meines letzten 
gcc-Ports brauchte ich rund 3 Wochen, also mit überschaubarem Aufwand.

Für ein Hobby-Projekt muss man aber schon was an Zeit einplanen, und im 
ersten Schritt wär's ja auf jeden Fall was Hobby-mässiges.

Die gcc-Quellen muss man dazu nicht kennen, wesentlich ist aber jemand 
zu haben, der einem Sachen erklären kann. Die Grundlagen dazu, wie ein 
Port zu geschehen hat, sind dokumentiert in den Internals
   http://gcc.gnu.org/onlinedocs/gccint/
die leider fern davon sind, vollständig zu sein.

Der erste Schritt eines solchen Ports wäre es, sich zu überlegen, wie 
die "Maschine" aussehen soll: Registersatz, ISA (Instruction Set 
Architecture) d.h. Befehlssatz, Adressierungsarten, etc.

Gcc zu porten hätte folgende Vorteile:
1) Man braucht sich nicht mit Parser auseinandersetzten.
2) Man muss sich nicht mit Sprachen wie C++ und deren Fallstricken und
   Gemeinheiten ausseinandersetzen
3) Man bekommt optimierten (Byte)code!
4) Man kann die ISA so entwerfen, daß sie schmackaft für gcc ist.
   Stolpersteine, die reale Architekturen immer bereithalten,
   kann man vermeiden.
5) Die Libc, Libm etc. sind in C geschrieben und könnte übernommen
   werden! Dito für andere Bibliotheken!
6) Man lernt viel über GCC. Nicht unbedingt von Nachteil...
   Compilerbauer sind gesuchte Fachkräfte (auch in der Krise)

Und Nachteile
1) Es ist aufwändig. Allerdings wurde in dein Projekt auch einiges
   an Arbeit reingesteckt, wenn man so durch die Quellen blättert.
2) GCC gest davon aus, daß er Code für einen Assembler erzeugt. *)
3) bc-gcc hätte die selben Probleme mit der Harvard-Architektur
   wie avr-gcc
4) Nicht zu vernachlässigende Wahrscheinlichkeit, daß das Projekt
   versackt.

ad *)
Das bedeutet zB, daß bc-gcc (der hypotethische Bytecode-gcc), Module 
erzeugen wird. Ein solches Modul kann unaufgelöste Referenzen enthalten 
wie etwa in der C-Quelle
1
extern int i;
2
extern int bar (int);
3
4
int foo (void)
5
{
6
   return bar(i);
7
}
bzw, dem daraus hervorgehenden Objekt. Für die C-Quelle würde der 
bc-Assembler etwa so aussehen. Das wäre die Ausgabe, die bc-gcc 
schreibt. Also noch kein fertiger Bytecode:
1
.section  .progmem.data,"a",@progbits
2
.global foo
3
foo:
4
   load.16 R0, i ; Lade i aus dem RAM in Register R0
5
   return
Hier müsste also ein Reloc für die Adresse von i erzeugt werden. Mit 
einem bc-as würde man diesen lesbaren Bytecode-Assembler umformen in 
"normalen" AVR-Assembler. Weiter geht es dann mit den bekannten 
avr-tools (Assembler, Linker) und das so erhaltene Modul wird zu der 
Anwendung hinzugelinkt:
1
.section  .progmem.data,"a",@progbits
2
.global foo
3
foo:
4
   .byte ??? ; Opcode für load.16 R0, *
5
   .word i   ; avr-as erzeugt den Reloc: avr-ld trägt zur Linkzeit
6
             ; die Adresse von i (16 Bits) ein
7
   .byte ??? ; Opcode für return
Man könnte diese Opcodes auch in bc-gcc ausdrücken, aber eine lesbare 
asm-Datei möchte man schon...

Johann

von Florian K. (led)


Lesenswert?

Mitesser schrieb:
> Die Ergebnisse stehen so in Registern, das ist besser ->Harvard
> Architektur.

Ich habe mir auch Gedanken über Vor- und Nachteile von Registern bzw. 
Stack gemacht. Ich habe mich aus folgenden Gründen für eine 
Stack-orientierte Maschine entschieden:
-- Ausdrücke (mathematische, logische usw.) sind hierarchisch 
organisiert. Daher erscheint es "natürlich", die Zwischenergebnisse auf 
dem Stack abzulegen.
-- Wenn man v.a. Wert auf kleinen Code legt, ist man mit einer 
Stack-orientierten Maschine unter Umständen besser dran: Man braucht 
nicht bei jeder Operation anzugeben, aus welchen Registern die Operanden 
gelesen werden sollen, sondern sie werden implizit oben vom Stack 
geholt. Bei einer Registermaschine werden sich aber oft Befehlssequenzen 
ergeben, in denen ein Ergebnis in einem Register abgelegt wird, und im 
nächsten Befehl wird wieder aus dem gleichen Register gelesen ==> die 
Nummer des Registers muss doppelt abgespeichert werden.
-- Wenn man einen Compiler selbst schreibt, ist es wahrscheinlich viel 
schwieriger, ihn für eine Registermaschine zu optimieren. Die Verwendung 
von Registern entspricht nicht der natürlichen Hierarchie, mit der 
Ausdrücke aufgebaut sind. Der Compiler muss immer "wissen", welche 
Register überschrieben werden dürfen und bei welchen der Inhalt 
vielleicht noch für später benutzt werden kann. Und beim Aufruf einer 
anderen Funktion müssen manche Register auch auf den Stack "gerettet" 
werden.

Wenn man keine virtuelle Maschine hat, sondern einen Prozessor, dann mag 
es sein, dass durch die Verwendung von Registern vielleicht schnellerer 
Code erzeugt werden kann.

Aber natürlich kann meine Maschine nicht nur jeweils auf das oberste 
Stackelement zugreifen, sondern auch auf tiefere Elemente, z.B. auf 
lokale Variablen, die im Stack gespeichert werden. Und es kann auf 
globale Variablen zugegriffen werden. Register sind sozusagen globale 
Variablen; meine Maschine kann bei Bedarf also auch eine den Registern 
entsprechende Funktionalität nutzen.

Ich habe momentan zwar nicht so viel Zeit, aber bei Gelegenheit werde 
ich mal einen Blick auf die Anleitung für einen gcc-Port werfen oder 
mich auch mit Parser Generatoren näher beschäftigen.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Florian K. schrieb:
> Mitesser schrieb:
>> Die Ergebnisse stehen so in Registern, das ist besser ->Harvard
>> Architektur.

hmmm. Das hat doch nix mit Harvard oder nicht-Harvard zu tun. Harvard 
sagt was darüber aus, wie auf peicher zugegriffen werden kann bzw. wie 
adressiert werden muss (aus Compiler-Sicht)

> Ich habe mir auch Gedanken über Vor- und Nachteile von Registern bzw.
> Stack gemacht. Ich habe mich aus folgenden Gründen für eine
> Stack-orientierte Maschine entschieden:
> -- Ausdrücke (mathematische, logische usw.) sind hierarchisch
> organisiert. Daher erscheint es "natürlich", die Zwischenergebnisse auf
> dem Stack abzulegen.
> -- Wenn man v.a. Wert auf kleinen Code legt, ist man mit einer
> Stack-orientierten Maschine unter Umständen besser dran: Man braucht
> nicht bei jeder Operation anzugeben, aus welchen Registern die Operanden
> gelesen werden sollen, sondern sie werden implizit oben vom Stack
> geholt. Bei einer Registermaschine werden sich aber oft Befehlssequenzen
> ergeben, in denen ein Ergebnis in einem Register abgelegt wird, und im
> nächsten Befehl wird wieder aus dem gleichen Register gelesen ==> die
> Nummer des Registers muss doppelt abgespeichert werden.

Schon. Wenn man bei einer realen Maschine mal ausrechnen würde, wieviel 
in einem Programm nur zur Codierung der Register-Nummern verbraucht 
wird, käme man auf nen recht hohebn Anteil: Bei AVR codiert ne Adition 
Rn += Rm in 16 Bits; 10 Bits davon müssen n und m codieren. Dito für 
ADD, SUB, CMP, XOR, ...

Allerdings würde ich schätzen, ein agressiv optimierender Compiler auf 
Register-Basis schneidet besser ab als ein nicht-optimierender auf 
Stapel-Basis.

> -- Wenn man einen Compiler selbst schreibt, ist es wahrscheinlich viel
> schwieriger, ihn für eine Registermaschine zu optimieren. Die Verwendung
> von Registern entspricht nicht der natürlichen Hierarchie, mit der
> Ausdrücke aufgebaut sind. Der Compiler muss immer "wissen", welche
> Register überschrieben werden dürfen und bei welchen der Inhalt
> vielleicht noch für später benutzt werden kann. Und beim Aufruf einer
> anderen Funktion müssen manche Register auch auf den Stack "gerettet"
> werden.

Ja. Register-Allokierung ist ein sehr komplexer Teil eines Compilers. 
Zumindest dann, wenn es gut machen und Register gut auslasten will (wie 
zB GCC). Wenn man keinen Wert darauf legt wie BASCOM, dann ist's simpel 
bzw. garnicht vorhanden.

Reale Maschinen helfen sich was Codedichte angeht oft mit mehreren 
Ausprägungen einer Instruktion. ZB gibt es dann eine 4-Byte Addition
   Ra = Rb + Rc
aber für den Fall, daß 2 Register übereinstimmen, gibt es eine 2-Byte 
Addition
   Ra += Rb
Oder es gib Opcodes, in denen ein bestimmtes Register implizit codiert 
ist; sowas wie ein Akkumulator. Oder es gibt Opcodes für kompexere, oft 
auftauchende Funktionalitäten wie
   Ra = Rb*Rc + Rd
   Ra = MAX (Rb,Rc)

> Wenn man keine virtuelle Maschine hat, sondern einen Prozessor, dann mag
> es sein, dass durch die Verwendung von Registern vielleicht schnellerer
> Code erzeugt werden kann.

Reale Prozessoren haben num mal Register, und daher wird versucht, diese 
möglichst gut auszunutzen. Speicher für lokalle Variablen soll so weit 
als möglich vermieden werden, weil die Zugriffe Platz und Zeit kosten, 
und die Variable RAM belegt. Daher ist GCC zB so heiß drauf, lokalen 
Variablen den Garaus zu machen. Dennoch bestehen die meisten Programme 
zum Großteil aus Speicherzugriffen und wenig komplexen Ausdrücken. Sehr 
komplexe Ausdrücke gibt es bestenfalls in Mathe-Algorithmen. Etwa wenn 
man ein Blick in die Berechnung der Γ-Funkton in der libm wagt.

> Aber natürlich kann meine Maschine nicht nur jeweils auf das oberste
> Stackelement zugreifen, sondern auch auf tiefere Elemente, z.B. auf
> lokale Variablen, die im Stack gespeichert werden. Und es kann auf
> globale Variablen zugegriffen werden. Register sind sozusagen globale
> Variablen; meine Maschine kann bei Bedarf also auch eine den Registern
> entsprechende Funktionalität nutzen.
>
> Ich habe momentan zwar nicht so viel Zeit, aber bei Gelegenheit werde
> ich mal einen Blick auf die Anleitung für einen gcc-Port werfen oder
> mich auch mit Parser Generatoren näher beschäftigen.

Was ist das eigentich für eine Software, für der der Interpreter 
geschrieben wurde?

Wär auch mal interessant zu wissen, wie die Größenverhältnisse an 
(Binär)Code sind für
-- Interpreter
-- Bytecode
-- Native C-Routinen

Johann

von Florian K. (led)


Lesenswert?

Johann L. schrieb:
> Allerdings würde ich schätzen, ein agressiv optimierender Compiler auf
> Register-Basis schneidet besser ab als ein nicht-optimierender auf
> Stapel-Basis.
Ein paar einfache Optimierungen führt auch mein Compiler durch. Sicher 
macht da GCC viel mehr. Aber ein nicht unerheblicher Teil der 
Optimierungen von GCC wird wohl der Ausnutzung der Register dienen, was 
bei einer Stapel-Maschine entfällt. Zusätzlich kann mein Compiler 
bestimmte Optimierungen machen, die GCC vom Prinzip her nicht kann: 
Code-Makros + Huffman-ähnliche Codierung der oft benötigten Opcodes mit 
kleineren Bit-Längen. Und wie erwähnt, schneidet mein Compiler bei 32- 
oder 64-Bit-Datentypen besser ab, weil die Operationen nicht aus vielen 
8-Bit Operationen zusammengesetzt werden müssen.
Aber wenn ich eine Register-Maschine implementieren würde, könnte ich 
manche Optimierungen ebenfalls nicht so gut wie GCC durchführen, aber 
die anderen weiterhin. Ich gehe davon aus, dass sich eine 
Register-Maschine nur bei einem GCC-Port lohnt (wenn fertige Teile von 
GCC die Optimierungen schon durchführen), nicht jedoch bei einem selbst 
geschriebenen Compiler.

> Was ist das eigentich für eine Software, für der der Interpreter
> geschrieben wurde?
Ein Text-basiertes Spiel ("Finanzspiel") für 2 oder mehr Personen, das 
ich mal auf dem C64 in BASIC geschrieben habe. Man kann Rohwaren 
einkaufen, diese zu Fertigwaren produzieren, Häuser und Grundstücke 
kaufen, diese hoffentlich teurer verkaufen oder vermieten u.a. Und 
manches wird in Dollar abgerechnet; manchmal muss man Dollar in gute 
alte DM umtauschen, und hoffentlich bei einem möglichst hohen Dollarkurs 
(welcher abhängig ist von der Zahl im "Markt" vorhandener Dollars). Wer 
am Ende am meisten Geld hat, gewinnt.
Sicher, dieses Programm ist nicht unbedingt der einzige Grund für mich, 
einen Bytecode-Interpreter zu schreiben. Vielleicht könnte ich das Spiel 
auch als normalen C-Code in 64K unterbringen, wenn ich mehr 8- oder 
16-Bit Datentypen verwenden würde. Aber mir ging es auch darum, mal 
etwas prinzipielle Erfahrung mit der Programmierung eines 
Bytecode-Interpreters zu sammeln.

> Wär auch mal interessant zu wissen, wie die Größenverhältnisse an
> (Binär)Code sind für
> -- Interpreter
> -- Bytecode
> -- Native C-Routinen
Das habe ich oben mal angedeutet: Der Bytecode-Interpreter braucht ca. 
15000 Bytes - je nach Konfiguration mehr oder weniger. Wenn man z.B. die 
avr-gcc-floats benutzen will, muss man die float(32 Bit) Bibliothek 
hinzurechnen. Meine float64-Bibliothek (komplett mit allen Funktionen) 
braucht als Bytecode etwa 5700 Bytes, während sie als compilierter 
normaler C-Code etwa 26000 Bytes braucht. Die Größeeinsparung wird bei 
anderen Programmen, die mehr mit 8- oder 16-Bit-Datentypen rechnen, 
wahrscheinlich deutlich geringer sein.

von Florian K. (led)


Lesenswert?

Um wenigstens mal einen Anhaltspunkt zu erhalten, wie die 
Codegrößenverhättnisse zwischen normalem kompilierten C Code und meinem 
Bytecode sind, wenn ein Programm vorwiegend 8- oder 16 Bit-Datentypen 
benutzt, habe ich einfach mal in der float64-Bibliothek alle 64- und 
32-Bit Variablen mit der Replace-Funktion des Editors in 16 Bit 
Variablen umgewandelt und außerdem 0x durch (uint16)0x ersetzt, wodurch 
die meisten 32- oder 64 Bit-Konstanten ebenfalls kürzer werden. 
Natürlich tut das Programm dann nichts sinnvolles mehr. Avr-gcc liefert 
dann ein Kompilat von ca. 8600 Bytes, und mein Bytecode ist wie erwartet 
nur geringfügig kleiner als in der 64-Bit-Version, nämlich 5150 Bytes. 
Aber der Bytecode ist in diesem Fall immer noch ca. 40 % kleiner als der 
Maschinencode.

Danach habe ich noch alle 16-Bit-Variablen durch 8-Bit-Variablen 
ersetzt: In diesem Fall erzeugt avr-gcc ca. 4500 Bytes vs. 5100 Bytes 
beim Bytecode. Wenn also ausschließlich 8 Bit-Arithmetik verwendet wird, 
ist hier avr-gcc etwas besser als der Bytecode. Es kann jedoch auch 
sein, dass durch die Reduzierung auf 8 Bit viele sinnlose Anweisungen 
entstehen, z.B. if-Anweisungen, die immer oder nie erfüllt sind, und 
dass diese von avr-gcc herausoptimiert werden. Dies kann mein Compiler 
nicht. Diese Größenverhältnisse müssen wegen des entstehenden sinnlosen 
Programms natürlich nicht unbedingt repräsentativ für andere Programme 
sein, die mit 8- oder 16-Bit Daten arbeiten.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Florian K. schrieb:
> Johann L. schrieb:
>> Allerdings würde ich schätzen, ein agressiv optimierender Compiler auf
>> Register-Basis schneidet besser ab als ein nicht-optimierender auf
>> Stapel-Basis.
> Ein paar einfache Optimierungen führt auch mein Compiler durch. Sicher
> macht da GCC viel mehr. Aber ein nicht unerheblicher Teil der
> Optimierungen von GCC wird wohl der Ausnutzung der Register dienen, was
> bei einer Stapel-Maschine entfällt.

GCC geht zunächst von einer unbegrenzten Anzahl an Registern aus. Auf 
diesen wird optimiert und man kann beliebig neue Register erzeugen und 
verwenden. Recht spät in der Compilierung (etwa Pass 180 von über 200 
Passes) wird diese unbegrenzte Anzahl an Registern auf in der Maschine 
wiklich vorhandene Register abgebildet. Falls die realen Register nicht 
ausreichen, werden Werte auf den Stack ausgelagert. Nach der 
Register-Allokierung ist der Code kaum noch formbar, und es llaufen nur 
noch vergleichsweise wenige Optimierungs-Passes darauf.

> Zusätzlich kann mein Compiler
> bestimmte Optimierungen machen, die GCC vom Prinzip her nicht kann:
> Code-Makros + Huffman-ähnliche Codierung der oft benötigten Opcodes mit
> kleineren Bit-Längen.

Im Compiler geht sowas natürlich nicht. Einen für einen Compiler gut 
verdaulichen Instruktionssatz zu entwerfen ist die Hausaufgabe des 
Hardwareherstellers. Um das tun zu können, braucht er schon einen 
Compiler (bzw. einen Compilerbauer), der Hinweise darauf gibt, welche 
Befehle sich lohnen (könnten) und welche eher esotherisch sind und kaum 
Anwendung finden würden. Das hängt natürlich von dem geplanten 
Einsatzfeld des Kerns ab, ist aber eine ganz interessante Aufgabe.

Für aufwändige Routinen kann ein Compiler Aufrufe zu 
Bibliotheksfunktionen erzeugen anstatt immer wieder den Code 
hinzuschreiben. Das dauert zwar länger, spart aber Programmspeicher. 
Beispiel dafür sind etwa die Division/Modulo-Routinen, die avr-gcc von 
Hause aus mitbringt.

> Und wie erwähnt, schneidet mein Compiler bei 32-
> oder 64-Bit-Datentypen besser ab, weil die Operationen nicht aus vielen
> 8-Bit Operationen zusammengesetzt werden müssen.

Klar, hier bietet ne VM/Interpreter mit einem an das Problem angepassten 
Instruktionssatz deutliche Vorteile.

Wo avr-gcc helfen könnte wäre eine bessere Unterstützung von 64-Bit 
Integers. Das würde zwar nicht den Bytecode verkleinern, aber den 
Interpreter merklich kleiner machen und auch schneller (ausser natürlich 
man schreibt Interpreter in (Inline) Assembler oder hat nix mit 64-Bit 
am Hut). Dito gilt für
   Beitrag "64 Bit float Emulator in C, IEEE754 compatibel"

Ob eine solche Unterstützung in avr-gcc wirklich praktikabel ist, sieht 
man leider erst nach einer Implementierung in avr-gcc...

Ich hatte mal angedacht die 64-Bit-Unterstützung hinzuzufügen, aber nach 
einigem Nachdenken darüber (und aus organisatorischen Gründen) die Idee 
wieder auf Eis gelegt:
Zwei Zahlen zu addieren würde schon 16 Register belegen, und die 64-Bit 
Brummer hin- und herzubewegen wäre nicht trivial und das komplizierteste 
daran. Sinnvoll wäre womöglich die Unterstützung von Operationen direkt 
auf dem Speicher, denn das würde die Registerlast deutlich erniedrigen. 
Allerding würde das den Aufwand noch erhöhen, und eine solche 
Erweiterung muss auch die pathologischen Testfälle der Testsuites 
überstehen.

> Aber wenn ich eine Register-Maschine implementieren würde, könnte ich
> manche Optimierungen ebenfalls nicht so gut wie GCC durchführen, aber
> die anderen weiterhin. Ich gehe davon aus, dass sich eine
> Register-Maschine nur bei einem GCC-Port lohnt (wenn fertige Teile von
> GCC die Optimierungen schon durchführen), nicht jedoch bei einem selbst
> geschriebenen Compiler.

Bei einem selbst geschriebenen Compiler ist eine Registermaschine 
Horror. Zumindest dann, wenn man die Register-Allokierung gut machen 
will: Möglichst viele Register verwenden, möglichst wenig Werte aufm 
Stapel zwischenspeichern müssen, und das ganze wird erschwert dadurch, 
daß jeder reale Prozessor über unterschiedlichste Arten von Registern 
verfügt ... auf dem kleinen AVR gibt's schon mindestens 7 davon!

Man kann allerdings auch den BASCOM-Ansazu wählen und keine 
Registerallokierung machen, also immer: Wert(e) Laden, Operation, 
Scheiben. Vielleicht ist das sogar ne Stapelmaschine; so gut kenn ich 
BASCOM nicht.

>> Was ist das eigentich für eine Software, für der der Interpreter
>> geschrieben wurde?
> Ein Text-basiertes Spiel ("Finanzspiel") für 2 oder mehr Personen, das
> ich mal auf dem C64 in BASIC geschrieben habe.

hehe, da kommt mit gleich VICE in den Sinn. Echt Klasse das Teil! 
Unbedingt antesten den C64-Simulator, wenns den noch nicht kennst!
Volle Simulation, inclusive SID und VIC, es gibt ne 1541 und zahlreiche 
Klassiker auf (virtueller) Diskette, die man laden und ausführen kann!
   http://www.viceteam.org/
   http://c64games.de/phpseiten/emulatoren.php

> Sicher, dieses Programm ist nicht unbedingt der einzige Grund für mich,
> einen Bytecode-Interpreter zu schreiben. Vielleicht könnte ich das Spiel
> auch als normalen C-Code in 64K unterbringen, wenn ich mehr 8- oder
> 16-Bit Datentypen verwenden würde. Aber mir ging es auch darum, mal
> etwas prinzipielle Erfahrung mit der Programmierung eines
> Bytecode-Interpreters zu sammeln.

Das schwierige ist dabei der Compiler. Ich hab auch schon mal einen 
simplem Interpreter gebastelt, der in Java geschrieben war (über 
Parser-Generator cup und Lexer jflex), der C-ähnlichen Code ausführte, 
den man ins Applet-Tag schreiben konnte.

> Der Bytecode-Interpreter braucht ca.
> 15000 Bytes - je nach Konfiguration mehr oder weniger. Wenn man z.B. die
> avr-gcc-floats benutzen will, muss man die float(32 Bit) Bibliothek
> hinzurechnen. Meine float64-Bibliothek (komplett mit allen Funktionen)
> braucht als Bytecode etwa 5700 Bytes, während sie als compilierter
> normaler C-Code etwa 26000 Bytes braucht. Die Größeeinsparung wird bei
> anderen Programmen, die mehr mit 8- oder 16-Bit-Datentypen rechnen,
> wahrscheinlich deutlich geringer sein.

Wie gesagt: wenn du dir mal anschaust, wie avr-gcc zwei 64-Bit Werte 
addiert... da wird einem echt übel :o/

Johann

von Florian K. (led)


Lesenswert?

Wäre nicht eine Alternative zu einer GCC-Portierung, dass man LCC 
verwendet:
http://www.cs.princeton.edu/software/lcc/
Es handelt sich um einen ANSI C-Compiler, der irgendeinen Zwischencode 
erzeugt (der wahrscheinlich ähnlich wie ein Bytecode ist), aus dem man 
sich dann selbst einen für eine Maschine passenden Code erzeugen kann. 
Es werden zwar Register unterstützt, aber ich gehe davon aus, dass man 
genau so Code für eine Stack-Maschine erzeugen kann. Vielleicht wäre die 
Einarbeitungszeit in LCC geringer als wenn man GCC portieren will ?

Vice habe ich noch nicht ausprobiert, aber CCS64. Vor längerem (d.h. zur 
Zeit von Intel 486 Prozessoren) hatte ich mein Spiel mal auf einem C64 
Emulator ausgeführt (dessen Name ich nicht mehr weiß). In dem Programm 
werden Zufallszahlen erzeugt, indem durch den SID-Soundchip "weißes" 
Rauschen bei ausgeschaltetem Ton erzeugt wird und jeweils ein aktuelles 
Sample gelesen wird. Das konnte der Emulator außerst schlecht, d.h. die 
vom Emulator erzeugten Zufallszahlen waren ziemlich schlecht.

Ein anderes Projekt, dass ich nun vielleicht machen werde, ist ein Jump 
and Run Spiel mit einem LMG6401PLG Display und einem ATMega644. Ich habe 
solch ein Spiel schon mal unter MS DOS programmiert (siehe google: 
gamble94). Wenn man eine Landschaft aus vielen "Zeichen" (z.B. 8x8 
Blöcke) aufbaut, kann man mit begrenztem Speicher relativ große 
Spielflächen erzeugen. Mit dem LMG6401PLG Display kann man vertikal 
pixelweise scrollen, aber horizontal leider nur zeichenweise.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Florian K. schrieb:
> Wäre nicht eine Alternative zu einer GCC-Portierung, dass man LCC
> verwendet:
> http://www.cs.princeton.edu/software/lcc/
> Es handelt sich um einen ANSI C-Compiler, der irgendeinen Zwischencode
> erzeugt (der wahrscheinlich ähnlich wie ein Bytecode ist), aus dem man
> sich dann selbst einen für eine Maschine passenden Code erzeugen kann.
> Es werden zwar Register unterstützt, aber ich gehe davon aus, dass man
> genau so Code für eine Stack-Maschine erzeugen kann. Vielleicht wäre die
> Einarbeitungszeit in LCC geringer als wenn man GCC portieren will ?

Dazu kann ich nichts sagen, den LCC hab ich noch nicht portiert; auch 
keine anderen retargetable compiler ausser GCC.

> Vice habe ich noch nicht ausprobiert, aber CCS64. Vor längerem (d.h. zur
> Zeit von Intel 486 Prozessoren) hatte ich mein Spiel mal auf einem C64
> Emulator ausgeführt (dessen Name ich nicht mehr weiß). In dem Programm
> werden Zufallszahlen erzeugt, indem durch den SID-Soundchip "weißes"
> Rauschen bei ausgeschaltetem Ton erzeugt wird und jeweils ein aktuelles
> Sample gelesen wird. Das konnte der Emulator außerst schlecht, d.h. die
> vom Emulator erzeugten Zufallszahlen waren ziemlich schlecht.

VICE find ich klasse, es ist haargenau das C64 guck-und-fühl :-)

> Ein anderes Projekt, dass ich nun vielleicht machen werde, ist ein Jump
> and Run Spiel mit einem LMG6401PLG Display und einem ATMega644. Ich habe
> solch ein Spiel schon mal unter MS DOS programmiert (siehe google:
> gamble94). Wenn man eine Landschaft aus vielen "Zeichen" (z.B. 8x8
> Blöcke) aufbaut, kann man mit begrenztem Speicher relativ große
> Spielflächen erzeugen. Mit dem LMG6401PLG Display kann man vertikal
> pixelweise scrollen, aber horizontal leider nur zeichenweise.

Da geht's dann eher um Geschwindigkeit, schätz ich. Wobei Codegröße ist 
immer ein Thema auf AVR, denn unnötige Instruktionen vertrödeln eben 
Zeit.

Was mit schleierhaft ist, wie man auf einem ATmega168 sowas 
implementieren kann:
   http://www.youtube.com/watch?v=-mMdc-rNNtU

Da steht was von 5 Cycles/Pixel, was heisst das überhaupt? Kann der AVR 
neben dem Game auch das PAL-Signal selbst erzeugen?

Johann

von Florian K. (led)


Lesenswert?

> Da geht's dann eher um Geschwindigkeit, schätz ich. Wobei Codegröße ist
> immer ein Thema auf AVR, denn unnötige Instruktionen vertrödeln eben
> Zeit.

Sicher, bei solch einem Spiel ist ein Bytecode-Interpreter absolut fehl 
am Platz. Vielleicht muss ich dazu sogar einige 
Low-Level-Grafikfunktionen in Assembler schreiben, obwohl ich mich dazu 
erst einarbeiten muss.

> Wie gesagt: wenn du dir mal anschaust, wie avr-gcc zwei 64-Bit Werte
> addiert... da wird einem echt übel :o/

Ich kenne zwar kein Avr-Assembler, da ich aber Assembelr von anderen 
Prozessoren kenne, kann ich schon sehen, dass avr-gcc recht 
umständlichen Code z.B. für 64-Bit-Additionen erzeugt. Wenn man dies 
verbessern würde (eventuell auch mit kleinen Hilfsfunktionen, um 
Speicher zu sparen), würde sich ein Bytecode-Interpreter eventuell sogar 
erübrigen, weil dies den Speicher wohl deutlich reduzieren würde. Und es 
hätte den Vorteil, dass es deutlich schneller als ein 
Bytecode-Interpreter wäre.

von MThomas (Gast)


Lesenswert?

Zwei "C-Interpreter"

1. a)  Gforth uses GCC to compile a fast threaded Forth.
http://www.jwdt.com/~paysan/gforth.html
(GForth ist ein in C geschriebenes Forth, so dass es sich auch als 
C-Interpreter miss/ge-brauchen lässt.)
 b)  Forum: GCC --   Fragen zu ...., AVR-GCC,


2.  Embedded Ch allows you to embed or plugin Ch, a powerful script 
engine, into your C/C++ application programs and hardware. Your C/C++ 
binary applications can call back into the Ch programs, Ch script 
functions, or access Ch global variables, etc.
http://www.softintegration.com/solution/embedded/

 (Ch ist ein C-Interpreter und embedded Ch eben ein in C einbedbarer 
C-Interpreter)

Tipp: Zum privaten gebrauch ist die Studenten-Edition von Ch kostenlos, 
nicht aber die ebedded-edition.

von Florian K. (led)


Lesenswert?

Ich habe mal Ch heruntergeladen. Es scheint gar kein Problem zu sein, 
ein C Programm auf dem PC mit Ch interpretieren zu lassen, aber geht das 
auch auf einem ATMega ? Auf die Schnelle habe ich nirgendwo Hinweise 
darauf gefunden. Und falls der Interpreter auch auf einem ATMega läuft, 
wäre die Frage, wie viel Speicherplatz er braucht und wie schnell die 
Programme laufen. Ich könnte mir vorstellen, dass Ch auf größeren 
Controllern wie z.B. Cortex M3 oder STM32 läuft, aber bei diesen 
Controllern hat man sowieso mehr Speicher, und da wäre der Einsatz eines 
Interpreters allein aus Speichergründen in vielen Fällen wohl nicht 
gerechtfertigt.

Hast Du selbst Erfahrung mit Ch bzw. Gforth ?

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.