Hallo Besucher, der Thread wurde 32k mal aufgerufen und enthält 170 Antworten

letzter Beitrag von EgonOlsen71 am

BASIC V2 Interpreter/Compiler in/für Java

  • Was mir durch flüchtige Blicke auf VMs usw., auch durch den anderen Basic-Thread, aufgefallen ist: Stack-Maschinen sind da unheimlich beliebt. Paar Kommandos, um Werte auf "einen" Stack zu legen oder runter zu holen, aber die Hauptarbeit wird von Kommandos mit Bezug auf die letzten Stackeinträge erledigt.
    Aber warum? Sind es nur die Einfachheit der Befehle und die einfache Übergabe von Parametern? Eventuelle Vorteile beim echten Compilieren werden vermutlich kein Grund sein, all die VMs sind ja quasi als Interpreter losgelaufen.


    Sind Stackmaschinen auch in Hardware verbreitet? Ich kenne jetzt nur die alten Prozessoren, die alles im Speicher gemacht haben, und die Microprozessoren, die Register mögen.


    Was den Stack anbelangt, so gibt es drei Möglichkeiten, diesen auf dem C64 zu gestalten:
    1.) Du definierst eine maximale Tiefe von 8-12, was meiner Erfahrung nach für die meisten Berechnungen ausreicht. Dafür legst Du im Speicher 6 kleine Tabellen an,

    Den Teil hab ich jetzt nicht verstanden. Woher die 6? 6 Stacks für verschiedene Zwecke wie Gosub/Return, For/Next, Formeln berechnen u.Ä.? Oder weil der komplizierteste Datentyp auf dem Stack 6 Bytes belegen kann?

  • Aber warum? Sind es nur die Einfachheit der Befehle und die einfache Übergabe von Parametern?

    Es ist ein recht einfach zu verstehendes Konzept und im Gegensatz zu registerbasierten Maschinen kann man sich die die Implementierung einer Registerallokation sparen.


    Zitat

    Eventuelle Vorteile beim echten Compilieren werden vermutlich kein Grund sein, all die VMs sind ja quasi als Interpreter losgelaufen.

    Java verwendet zB ebenfalls eine Registermaschine als Bytecode.


    Zitat

    Sind Stackmaschinen auch in Hardware verbreitet?

    Kaum. Der Nachteil von Stackmaschinen in echter Hardware ist, dass sie ziemlich viele Speicherzugriffe erzeugen, auch wenn man das etwas abfangen kann, indem die obersten Stackeinträge in der CPU statt in externem Speicher abgelegt werden. Jenseits einiger Forth- und Java-Obskuritäten fällt mir spontan eigentlich nur die ZPU ein, das ist ein für minimalen FPGA-Platzbedarf optimierter Prozessor mit reiner Stack-Architektur. Glücklicherweise gibt es einen gcc dafür, direkt in dessen Assemblercode zu programmieren fühlt sich doch reichlich ungewohnt an.

  • Java verwendet zB ebenfalls eine Registermaschine als Bytecode.


    Grad Java sieht für mich sehr nach Stack aus (link), LLVM nach irgendwas mit unendlichen Registern (link). Aber ich hab das auch nur sehr oberflächlich betrachtet.


    Der Nachteil von Stackmaschinen in echter Hardware ist, dass sie ziemlich viele Speicherzugriffe erzeugen, auch wenn man das etwas abfangen kann, indem die obersten Stackeinträge in der CPU statt in externem Speicher abgelegt werden. Jenseits einiger Forth- und Java-Obskuritäten fällt mir spontan eigentlich nur die ZPU ein, das ist ein für minimalen FPGA-Platzbedarf optimierter Prozessor mit reiner Stack-Architektur. Glücklicherweise gibt es einen gcc dafür, direkt in dessen Assemblercode zu programmieren fühlt sich doch reichlich ungewohnt an.

    Ist ja irgendwie komisch. Auch Register sind irgendwann mal aufgebraucht und müssen gerettet werden, und dann kommen die Speicherzugriffe doch, am besten noch über den Umweg eines Stacks... Da scheint mir ein Stack geeigneter, solche Zugriffe zu bündeln und vielleicht die nötigen Speicherzugriffe versteckt zu erledigen.

  • Grad Java sieht für mich sehr nach Stack aus (link)

    Stimmt, da stand ursprünglich "eine Stack- statt Registermaschine" und das wurde dann falsch zusammengestrichen.


    Zitat

    LLVM nach irgendwas mit unendlichen Registern (link). Aber ich hab das auch nur sehr oberflächlich betrachtet.

    Könnte man so nennen, allerdings kann jedes dieser "Register" nur einmal beschrieben werden und ändert sich dann nie wieder.


    Zitat

    Ist ja irgendwie komisch. Auch Register sind irgendwann mal aufgebraucht und müssen gerettet werden, und dann kommen die Speicherzugriffe doch, am besten noch über den Umweg eines Stacks... Da scheint mir ein Stack geeigneter, solche Zugriffe zu bündeln und vielleicht die nötigen Speicherzugriffe versteckt zu erledigen.

    Manchmal werden Register auch als "Level 0-Cache"(*) bezeichnet - es sind die schnellsten Speicher, die im Prozessor verfügbar sind. Natürlich muss man irgendwann deren Inhalt retten, aber je länger man es herauszögern kann, um so weniger fallen die Kosten dafür im Mittel an.


    (*) Disclaimer: Es gibt inzwischen einige Nicht-Register-Konzepte, die den Namen eher verdient haben, zB Microop-Caches

  • Den Teil hab ich jetzt nicht verstanden. Woher die 6? 6 Stacks für verschiedene Zwecke wie Gosub/Return, For/Next, Formeln berechnen u.Ä.? Oder weil der komplizierteste Datentyp auf dem Stack 6 Bytes belegen kann?

    Letzteres. Fließkommazahlen brauchen (entpackt) 6 Bytes. Anstelle diese 6 Bytes nacheinander auf den Stack zu legen

    Code
    1. lda facc
    2. sta stack, x
    3. inx
    4. lda facc + 1
    5. sta stack, x
    6. inx
    7. ...
    8. lda facc + 5
    9. sta stack, x
    10. inx

    splittet man den Stack in 6 Teile, was auch gleich die maximale Anzahl an Einträgen erhöht.

    Code
    1. lda facc
    2. sta stack_0, x
    3. lda facc + 1
    4. sta stack_1, x
    5. ...
    6. lda facc + 5
    7. sta stack_5, x
    8. inx

    Der Nachteil von Stackmaschinen in echter Hardware ist, dass sie ziemlich viele Speicherzugriffe erzeugen, auch wenn man das etwas abfangen kann, indem die obersten Stackeinträge in der CPU statt in externem Speicher abgelegt werden.

    Man muß dabei unterscheiden zwischen Stackmaschinen mit nur einem Stack für Variablen, Returnadresse und Termberechnung und solchen mit zwei Stapeln: einen für Variablen und Returnadresse und einen Evaluationsstack für die Termberechnung. Letzterer braucht, wie beschrieben, bloß aus ein paar Einträgen zu bestehen, nicht mehr als ein Registersatz eines Prozessors. Dadurch werden alle Rechenoperationen wie bei einem Prozessor im "Level 0-Cache" ausgeführt. Das reduziert die Speicherzugriffe und die Cachebelegung (Level 1..) deutlich, was die Unterschiede zwischen einer Stackmaschine und einer Registermaschine mindert.

    nur die Einfachheit der Befehle

    Gerade auf dem C64 (bzw. auf dem 6502) ergeben sich dadurch, daß Stackmaschinen über einfache Befehle verfügen, mehrere Vorteile.
    1.) Die Befehle sind deutlich kürzer, da keine Angaben gemacht werden müssen, welche Register bei der Operation zur Anwendung kommen sollen. Dadurch werden nicht nur die Programme kürzer (Stackmaschinencode ist sehr kompakt und eignet sich daher besonders gut für kleinen Speicher), auch die Ausführungszeit wird schneller als bei der Emulation einer Registermaschine, da die Befehlsdekodierung einfacher ist.
    2.) Einfache Befehle bedeuten auch, daß der Interpreter relativ kurz ist, was wiederum Platz im Rechner spart.

    Eventuelle Vorteile beim echten Compilieren werden vermutlich kein Grund sein

    Oh doch. Wie Unseen schon schrieb, ist die Registerallokation sehr aufwendig. Hinzukommen auch die ganzen Komplikationen bei nicht-orthogonalen Befehlssätzen (wie beim x86: Integer-Multiplikation nur über EAX...). Außerdem können komplizierte Vorgänge sehr leicht in einem Befehl gebündelt werden, z. B. eine Stringaddition durchführen oder Speicher kopieren. Kurz: Es ist wesentlich leichter, ein Backend zu schreiben für eine Stackmaschine als für eine Registermaschine.


    Nachtrag:
    Gerade bei der Kompilierung vom ansonsten interpretierten BASIC-Code, bei dem die Bedeutung des NEXT (oder auch RETURN) laufzeitabhängig ist, wäre es sinnvoll, eine Stackmaschine zu verwenden mit einem Sonderbefehl "Next-Ausführen". Ebenso kann man eigene Befehle einbauen für PRINT, READ und anderen Standardbefehlen. Der Unterschied zwischen dem BASIC-Tokencode und dem Stackmaschinencode ist dann nicht mehr so groß.

  • Vielleicht eine kleine Anmerkung, was Java und Stackmachines angeht: Das stimmt für die Desktop-VMs, aber z.B. nicht für Dalvik/ART auf Android. Die nehmen wiederum den stackbasierten Bytecode und konvertieren ihn in etwas registerbasiertes, um dann darauf zu arbeiten.

  • Oh doch. Wie Unseen schon schrieb, ist die Registerallokation sehr aufwendig. Hinzukommen auch die ganzen Komplikationen bei nicht-orthogonalen Befehlssätzen (wie beim x86: Integer-Multiplikation nur über EAX...). Außerdem können komplizierte Vorgänge sehr leicht in einem Befehl gebündelt werden, z. B. eine Stringaddition durchführen oder Speicher kopieren. Kurz: Es ist wesentlich leichter, ein Backend zu schreiben für eine Stackmaschine als für eine Registermaschine.

    Ja, wenn man es bei einem Interpreter für die VM lasen will, dann sehe ich das auch schnell ein. Aber wenn das Ergebnis ein richtiges Kompilat sein soll, dann war der Code für die VM nur ein Zwischenschritt, und dann kommen halt zu dieser späten Stelle die alten Probleme wieder hoch. Ich glaub nicht so recht, dass man beim Design der JVM schon einen späteren JIT-Compiler im Auge hatte.

    Vielleicht eine kleine Anmerkung, was Java und Stackmachines angeht: Das stimmt für die Desktop-VMs, aber z.B. nicht für Dalvik/ART auf Android. Die nehmen wiederum den stackbasierten Bytecode und konvertieren ihn in etwas registerbasiertes, um dann darauf zu arbeiten.

    Darüber hab ich interessanten Lesestoff gefunden: Stack vs. Register
    Comparison of VMs

  • Darüber hab ich interessanten Lesestoff gefunden: Stack vs. RegisterComparison of VMs

    Wobei der Performancevergleich hier nicht eine stackbasierte VM mit einer registerbasierten VM des gleichen Kalibers vergleicht und von daher meiner Ansicht nach fast keine Aussagekraft bzgl. Stack vs. Register hat.
    Der JIT bei Dalvik war nie besonders gut, er war ursprünglich auch überhaupt nicht angedacht. Die ersten Aussagen über Dalvik gingen in die Richtung: "JIT? Brauchen wir nicht, machen wir nicht!"
    Mit ART haben sie dann erst komplett auf AOT-Kompilierung umgeschwenkt, um jetzt in der letzten Fassung einen Mix aus allen drei Optionen (Interpreter, JIT und AOT (in zwei Ausprägungen, einmal "schnell" und einmal "gut")) zu bieten. Von daher ist die aktuelle Android-VM am ehesten mit Hotspot vergleichbar...aber da kenne ich leider keine vergleichenden Tests.

  • Update: Ein weiterer kleiner Meilenstein ist erreicht. Das Teil kann jetzt mein gerne als Testfall benutztes Programm Lyric 3.0 (aus einer alten 64er, meine ich) in lauffähigen Pseudo-Assembler-Code compilieren (bis auf OPEN, CLOSE und PRINT#...das unterstütze ich bisher aus Faulheit noch nicht).


    BASIC-Version:


    ...und das Kompilat:


  • Also diese ZPU ist "Interessant", von Natur aus sind kompliziertere Befehle darauf ausgelegt durch ein Programm der einfachen Befehle emuliert zu werden.
    Aber ich frage mich, wie man aus diesem Befehlssatz einen Branch oder LSR herbeizaubert...
    Der Code ist verfügbar, und obwohl ich die Sprache nicht spreche ist er doch recht verständlich. Geholfen hat's mir aber nicht.

  • Aber ich frage mich, wie man aus diesem Befehlssatz einen Branch oder LSR herbeizaubert...

    Unter der Befehlssatzbeschreibung findet sich der Hinweis "Code points 33 to 63 may be emulated by code in vectors 2 through 32:" gefolgt mit einer Liste zusätzlicher Befehle, darunter auch LSR ("LSHIFTRIGHT") und Branch ("EQBRANCH, NEQBRANCH"). Ich finde es allerdings merkwürdig, daß die Bitkombination %00000001 nicht aufgelistet ist. Was der Befehl FLIP im Befehlssatz zu suchen hat, weiß ich auch nicht, genauso wenig wie NOP. Auch einfache Schleifen ohne NOP verplempern bereits ausreichend Zeit, und notfalls könnte man ein NOP immer noch durch zweimal SWAP emulieren.


    Persönlich würde ich diesen Befehlssatz aus folgenden Gründen als für ein Kompilat auf dem C64 nicht geeignet bezeichnen:
    1.) Wie der Artikel sagt, ist die Codeausführung sehr langsam. Für den C64 würde dies zwar bedeuten, daß ein für die ZPU kompiliertes Basicprogramm voraussichtlich immer noch schneller wäre als die interpretierte Basicversion, jedoch langsamer als das Kompilat herkömmlicher Basic-Compiler.
    2.) Es gibt im Befehlssatz (auch in der Erweiterung) keine Unterteilung in Integer- (für Ganzzahlen, Zeichen oder Aufzählungstypen) und Fließkommaoperationen. Befehle zur Stringbearbeitung (String kopieren, String addieren, String vergleichen) fehlen vollständig und müßten über SYSCALL emuliert werden. Auch weitere Basicbefehle wie NEXT, LEFT$ oder READ müßten über SYSCALL eingebunden werden.
    3.) Ungeregelt sind bei der Beschreibung auf Wikipedia auch die erwähnten Branchbefehle (relativ? absolut? Offset bzw. Adressgröße?) oder Unterprogrammaufrufe (CALLPCREL: welcher Offset?). Wählt man die Offsets zu klein, muß man zusätzliche JMPs in den Code einfügen (wie beim 6502). Gestaltet man sie zu groß, geht es zu Lasten der Codedichte.
    Nebenbei: Was andere Hochsprachen anbelangt, dürfte die Codegröße nur bei einfach gehaltenen Programmen minimal bleiben. Bei z. B. Zugriffen auf structs (structzeiger->offset) oder dem Kopieren von Verbunddatentypen wird der erzeugte Code recht schnell aufgebläht, wodurch die ZPU ihren Vorteil der Codedichte ebenso schnell verliert.
    Schwierig wird es (nach meinem jetzigen Kenntnisstand) bei Zugriffen auf globale Daten. Die ZPU verwendet viele Bitkombinationen für die Implementierung der Befehle STORESP_x und LOADSP_x, die jedoch nur einen relativen Zugriff, d. h. auf lokale Variablen ermöglichen. Bei globalen Zugriffen müßte man zunächst die absolute Adresse auf den Stack pushen und anschließend mit LOAD den Wert laden. Damit macht man einen Vorteil der Stackmaschinen zunichte: Code- und Datenbereich sind relokatibel. Üblicherweise existieren für globale Zugriffe ein gesonderter Zeiger auf globale Daten und eigens Befehle, die diesen Zeiger verwenden. Da beim C64 die globalen Daten in einem Bereich oberhalb des Programms angesiedelt wären (unterhalb kommen ja $0..$ff = Zeropage, $100..$1ff = 6502-Stack usw.), müßten für globale Zugriffe also 16-Bit-Adreßwerte auf den Stack geladen werden, was ingesamt pro Zugriff 4 Bytes veranschlagen würde. Zumindest, hätten die Designer das nicht bemerkt und daher die Befehle LOADH und SAVEH schnell mitaufgenommen, die aber - wie erwähnt - auf festen Adressen arbeiten.


    Summa summarum: Wahrscheinlich fährt EgonOlsen71 am besten, wenn er für sein Projekt eine eigene Zielmaschine mit speziell angepaßtem Befehlssatz entwirft.

  • Oh Sorry, das war nicht als praktische Frage für eine Implementierung am C64 gedacht (wobei mir das Emulieren schon sehr nützlich erscheint) :schande:
    Der Befehlssatz ist sehr heftig gekürzt, muss aber wohl reichen, um damit alles machen zu können.
    Das ist schon ein bisschen herausfordernd...
    Innerhalb eines Programms erscheint mir ein Branch schon kompliziert genug. Ich würde wohl den TOS X nach links schieben, den PC addieren, mit PullPC ein weiteres push/PullPC überspringen.
    Emuliert mit allen Parametern auf dem Stack, der anschließend sauber sein sollte, bestimmt noch komplizierter.
    Aber ein LSR?

  • Aber ich frage mich, wie man aus diesem Befehlssatz einen Branch oder LSR herbeizaubert...

    Auf den zweiten Blick..


    Sofern man die Zusatzbefehle nicht emuliert, sondern nur durch eine feste Befehlsfolge ersetzen will, die bei Verwendung des Zusatzbefehls automatisch aufgerufen wird, macht der Befehle FLIP durchaus Sinn, da er dazu benutzt wird, um besagte LSR-Verschiebung zu ermöglichen.
    Ein LSL erreicht man bekanntlich durch Addition des Operanden auf sich selbst mittels PUSH und ADD: 7 (0111) + 7 (0111) => 14 (1110). Für eine Rechtsverschiebung werden nun die Bits vertauscht, dann PUSH und ADD ausgeführt und anschließend mit FLIP wieder in die originäre Form gebracht.
    Bei Branchbefehlen wird TOS geladen mit der Zieladresse und dann per POPPC in den PC übertragen. Bei relativen Sprüngen/Verzweigungen müßte man auf weitere Zusatzbefehle wie PUSHPC usw. zurückgreifen, was aus einem Zusatzbefehl gleich eine ganze lange Kette von weiteren Befehlen macht und das Prädikat "langsam" mehr als rechtfertigt.
    Diese Form der Befehlsausführung, d. h. die Emulation der Befehle durch die Emulation, ist dermaßen langsam, daß man an eine Umsetzung auf dem C64 besser gar nicht erst nachdenkt.


    Edit (Hab's erst gerade gelesen):

    Das ist schon ein bisschen herausfordernd...

    Ja, die Ersetzung der komplizierteren Befehle durch die erste Gruppe ist schon eine Herausforderung an sich. Da könnte man für eine optimierte Version schon einige Abende dran verschwenden. ^^

  • So...ich habe mal wieder ein paar Abende in das Projekt investiert und der Compiler erzeugt jetzt aus dem Zwischencode echten 6502-Assemblercode (den ich dann wieder in den zum Projekt gehörenden Assembler kippe, um was Ausführbares zu erhalten). Unterstützt werden dabei momentan REAL- und INTEGER-Variablen, Schleifen, IF-THEN, GOTO, GOSUB, Vergleiche, PEEK und POKE und ein paar Grundrechenarten. Arrays, Strings und Ausgaben fehlen noch. Egal, ich kann damit dieses (unoptimierte und in der Grundform von irgendwo her kopierte) Fraktal-Programm übersetzen:




    Der erzeugte Zwischencode sieht so aus:



    und der 6502-Assemblercode (er enthält auch die vom Compiler erzeugten Kommentare sowie solche, die der anschließende Optimizerlauf dann noch einfügt) dann so:


    Hier ist noch ein Video, wo sich das Kompillat ein Wettrennen mit dem BASIC BOSS liefert (meiner links, BB rechts):

  • Code
    1. 623 tf%=int(2^(7-(((i+1)/8-int((i+1)/8))*8)))
    2. 625 if cc%=3 then pokep,peek(p) or int(2^(7-((i/8-int(i/8))*8))) or tf%:return
    3. 630 if cc%=2 then pokep,peek(p) or int(2^(7-((i/8-int(i/8))*8))):return

    Da wird mir schlecht vor lauter Klammer-Labyrinth und dem wohlfeilen Einsatz des extrem schnellen (nicht!) Potenz-Operators.


    Das kann schon in interpretiertem BASIC nicht schnell sein und da hilft auch Kompilieren nicht mehr. Ist so wie wenn man gleichzeitig auf Bremse und Gaspedal tritt.


    Also: die innere Schleife vom Mandelbrot läßt sich unter Gebrauch der Arithmetik-Routinen des Interpreters etwa um den Faktor 3 beschleunigen. Da fällt dann der ganze Overhead mit "Baum der Ausdrucksauswertung auf- und wieder abbauen", "Variablen suchen" und "Zahlnumerale umrechnen" komplett weg. Diese Schleife wird dann während >95% der Laufzeit abgearbeitet.


    Den ganzen Rest kann man gerne in interpretiertem BASIC lassen. Evtl. grad noch die Grafik-Befehle in eine BASIC-Erweiterung auslagern. Aber doch nicht so wie in diesem Programm! :cry:

  • Ich hoffe Du bist nicht böse, wenn ich nicht alles durchgelesen habe :saint:


    Sehr interessanter Video-Vergleich. BBoss läuft zunächst deutlich schneller los, wird dann aber nach 1 Zeile überholt - und danach laufen beide scheinbar gleich schnell? Schnelles loslaufen und langsamer werden könnte ich mir ja durch dynamisch verwaltete Variablen erklären, aber so find ich das Ergebnis doch ungewöhnlich...


    xl=-2:


    wurde zu:
    MOV X,#-2.0{REAL}
    MOV XL{REAL},X
    => D.H. Deine Maschine nutzt Befehle mit 2 Parametern, eines immer Register? Ein MOV XL{REAL},#-2.0{REAL} ist nicht vorgesehen?


    Zeilen 16-31
    Der Compiler hat dann recht viele Bewegungen draus gemacht
    -Speicher "Const_0" zum Speicher "X_Reg"
    -Speicher "X_Reg" in den FAC
    -FAC nach Var_XL
    Wie ist denn der Umweg über den FAC da reingeraten?


    Ansonsten find ich es absolut geil, dass Du da was funktionierendes hingezaubert hast :love:

  • Da wird mir schlecht vor lauter Klammer-Labyrinth und dem wohlfeilen Einsatz des extrem schnellen (nicht!) Potenz-Operators.
    Das kann schon in interpretiertem BASIC nicht schnell sein und da hilft auch Kompilieren nicht mehr. Ist so wie wenn man gleichzeitig auf Bremse und Gaspedal tritt.

    Naja, das will ich gar nicht bestreiten. Aber darum geht es nicht. Das Programm ist einfach irgendein Beispiel und es ist immer besser, wenn es was grafisches ist, weil Menschen eben gerne was sehen. Ich jedenfalls. Als Test für den Compiler ist es völlig unerheblich, wie optimal die BASIC-Version ist. Und das Klammerlabyrinth ist sogar ein ganz besonders guter Test, denn einfache Ausdrücke auswerten kann jeder, da geht selten was schief.
    Zur Info: Im Interpreter braucht das 6h, als Kompilat 2h 20min.

  • Wie Hoogo schon schrub: es ist in der Tat bewundernswert, daß dein Compiler diesen Code schluckt. Das hätte ich durchaus mit obigem Beitrag schon mitschicken können. :)


    Immerhin ist meine Zeitschätzung in Richtung Faktor 3:

    Zur Info: Im Interpreter braucht das 6h, als Kompilat 2h 20min.

    doch gar nicht mal so schlecht gewesen. ;)

  • BB läuft schneller los, weil ich es eher gestartet habe. Musste erst in das andere Fenster wechseln und einmal Enter drücken, daher der Zeitversatz.


    Und ja, meine Befehle in der Zwischensprache sind immer min. mit einem Register.


    Der FAC landet da oft drin. Das liegt an der Art, wie ich den Zwischencode in 6502 überführe. Würde man direkt 6502 erzeugen, wäre das vermutlich seltener der Fall, aber dann wäre das Ding auch weniger flexibel (und auch nicht zwingend schneller, wie man im.Video ja sieht). Bei der Umsetzung entstehen einige ineffiziente Muster, aber da die sich immer wiederholen, geht hinterher ein Optimizer über den 6502-Code und versucht, diese Sachen wieder glätten. Mit dem Ergebnis bin ich bisher recht zufrieden.