Lowlevel
Lowlevel => OS-Design => Thema gestartet von: SSJ7Gohan am 14. September 2005, 16:24
-
Hallo,
ich wollte hier mal mein OS vorstellen, und bitte um Kritik. Ich würde mich ausserdem über jeden freuen, der sich an dem OS beteiligen möchte, da es ein recht großes Projekt werden sollte, wenn ich alle meine geplanten Features umsetzen will.^^
Der Name blubbOS hat sich einfach daraus entwickelt, dass mir kein besserer eingefallen ist.^^;;;
Das Konzept von blubbOS ist es, ein flexibles Betriebsystem auf der Basis eines kleinen, sicheren und schnellen Kernels aufzubauen. Wegen dem flexibelen Aufbau ist es sowohl möglich, eine POSIX kompatibele API sowie eine moderne objekt-orientierte API zu unterstützen. POSIX wird darbei unterstützt, um Anwendungen wie GCC benutzen zu können. Die API von blubbOS ist jedoch nur ein unabhängiges Interface, das eigentlich nur die richtigen Treiber aufruft.
Hier eine Skizze über den Aufbau von blubbOS:
KERNEL HARDWARE
| |
TREIBER
| |
POSIX BLUBBOS API
| |
POSIX-APPS NATIVE BLUBBOS PROGRAMME
blubbOS hat eine Exokernel ähnliche Architektur. Es unterstützt Treiber sowohl als Module, als auch als Prozesse, wie in einem Microkernel. Im Unterschied zu monolitischen Kernel, laufen Treiber aber immer im Userlevel. Prozesse können auch ohne Treiber auf die Hardware zugreifen, falls sie Rechte dafür besitzen. In einem normalen System wird dieses Feature aber nicht oder nur bei bestimmter Hardware benutzt.
Hardware Abstraktions Modell:
HARDWARE
| |
| |
| | KERNEL
| | | |
| MODULE |
| | |
| | |
| | |
PROZESSE / THREADS
Der Kernel von blubbOS ist nur sehr klein gehalten und verwaltet nur folgende Bereiche:
- Speicher (Prozesse können pageweise Speicher anvordern; Shared-Memory)
- Prozesse (Prozesse sind in blubbOS die Resourcen, die ein Programm besitzt, in Form von Speicher, Filehandles, usw. (sofern das vom Treiber so implementiert wird, der Kernel verwaltet keine Files)
- Threads (Threads sind einem Prozess zugeordnet und haben einen eigenen Stack)
- Module (Module können geladen, und in alle Prozesse gemappet werden, so dass schnelle Treiberaufrufe über normale JMPs möglich sind. Als Module sollten möglichst nur die geschwindigkeitskritischen Codeteile geladen werden.)
- Interrupts (Sobald ein Interrupt auftritt wird an den Thread, der ihn handlet ein Signal gesendet. Eventuell wird der Thread auch direkt aufgerufen.)
- I/O (Der Kernel kann den Threads den Zugriff verschiedene Hardwareteile ermöglichen. Er greift jedoch nicht selbst auf die Hardware zu (mit Ausnahme von eingebauten Debugfunktionen und Speicher). Ein Thread darf nur auf einen Hardwareteil gleichzeitig zugreifen, und normalerweise ist nur Treibern der Zugriff auf die Hardware gestattet.)
- Eventuell sollte noch der V8086-Mode unterstützt werden, um das BIOS aufrufen zu können. (Noch nicht implementiert)
Die native API von blubbOS besteht aus verschiedenen Objekten. Ãhnlich wie bei CORBA, unterscheidet die API zwischen Client und Servercode. Ein API-Aufruf besteht eigentlich nur aus einem kleinen Codestück, das den Aufruf an einen Server weiterleitet. Der Server führt die angeforderte Aktion aus, und leitet die Rückgabe an den Client weiter. Die Kommunikation zwischen Client und Server basiert darbei nicht auf einem bestimmten Protokol, kann also über Sockets, Pipes oder auch ganze normale Funktionsaufrufe (sofern der Server auf dem gleichen Rechner befindet, wie der Client.). Die API soll möglichst alle von den Anwendungen benötigten Funktionen abdecken, so dass die erstellten Programme eine große API benutzen, statt auf viele verstreute Module zuzugreifen. Das umfasst File-IO, IO mit verschiedenen Geräten, GUI, Netzwerk, Datenverarbeitung (z.B. XML), Sound, Grafik, verschiedene Funktionen wie REGEXP usw.
Eventuell sollte auf dieser Basis auch noch eine .NET ähnliche VM bereitgestellt werden, um eine noch größere Sicherheit zu gewähren. Mit einem JIT Compiler sollte die Geschwindigkeit ja auch nicht sooo stark dem nativen Code hinterherhinken.
Der Kernel ist in C geschrieben, die Sprache für die anderen Teile steht noch offen, wobei ich auch stark zu C tendieren würde, da dass einfach am schnellsten ist.
Die Enduser Programme würden dann wohl die VM benutzen, nicht blubOS-Native Anwendungen sollten aber auch ohne die VM laufen, und POSIX benutzen können.
Der Kernel unterstützt bereits alle oben beschriebenen Features. Die nächsten Schritte werden sein, grundlegende Treiber zu entwickeln (VFS, HD/FD Treiber, Grafiktreiber). Danach sollte die POSIX API (zumindest die grundlegenden Teile, sodass GCC und evt. Bash läuft) umgesetzt werden. Danach kann man sich an die native blubbOS API machen. Sobald die soweit ist, kann man dann eine VM und einen JIT Compiler programmieren.
Bei der Lizenz hatte ich an GPL gedacht, ich denke, für dieses Projekt hat sie eigentlich garkeine Nachteile. Das Ãnderungen veröffentlicht werden müssen ist ja wünschenswert, und die Treiber usw. müssen ja eh nicht GPL sein, da sie ja keinen Code direkt verwenden und sich nicht im Kernel befinden.
Den Kernel werde ich wohl in einigen Tagen mal online stellen.^^
-
da hast dir aber viel vorgenommen! naja die idee is cool, und besonders das mit der vm find is gut... wollte schon immer mal nen kompiler, interpreter und oder vm schreiben :) (würde also helfen)
-
Das Design hat sehr grosse Parallelen zu meinen Ideen. Von daher finde ich es sehr gut!
Und es stimmt, da du ja explizit alle Treiber in den User Mode "verbannst" sollte die GPL dir keine Schwierigkeiten bereiten.
Aber am Namen musst auch du (wie viele andere) noch arbeiten. ;)
-
Name hin oder her: Da gebe ich dir mein lieber SSJ7Gohan vollkommen recht, das ist erstmal das unwichtigste. Viele planen wie wild und geben ihrem OS die hoffnungsvollsten Namen, aber es kommt nie zu gutem Code.
Und wenn ich mir anschaue, was du so bisher geschafft hast: Hut ab! Mir würde es allerdings reichen, erstmal ein Disk-Image zu erhalten, um mir einen ersten Eindruck zu verschaffen und erst später den Kernel.
Aber ehrlich: Mach weiter so, klingt großartig! Das mit der VM haben viele vor, ich hoffe, dass du es schaffst (auch wenn ich selbst Java vor .Net bevorzugen würde, aber das ist wohl Geschmacksache)!
-
Ich hab mal eine (simple) erste Version einer Java VM in nen Kernel verfrachtet und das ganze laufen lassen! ;)
EDIT: Oh, ich habe vergessen zu erwähnen das mit den virtuellen Maschinen die rechtliche Sache evtl. nicht mehr ganz so einfach sein könnte - ich meine mal eine Diskussion gelesen zu haben ob es legal sei, auf einer freien VM eine unfreie Anwendung laufen zu lassen - bei der Klassenbibliothek ist das sowieso klar, da die Anwendung ja auch dagegen gelinkt wird, wie es auch mit nativem Code passiert.
-
Hm, dann müsste ich mir mal erkundigen, wie das mit unfreiem Code auf einer freien VM ist. Falls das bedenktlich ist, sollte die VM evt. unter die LGPL gestellt werden.
Als Sprache für die VM würde ich am liebsten eine C++ ähnliche Sprache verweden.
@Kevin
Es würde mich sehr freuen, wenn du mich bei der VM Programmierung unterstützen würdest.
-
Ich bin vom Design her auch auf ein Virtualisiertes OS gekommen. Bzw. dass nur die eigentlichen Grundstrukturen als Binäri vorliegen.
Der Grund dafür war ursprünglich die Nichtbenutzung der Protected Funktionen. Also wenn die eigentlichen Programme selber keine direkten Zugriff auf den Speicher haben, dann kann auch alles im Ring 0 laufen und man erspart sich "teure" Sprünge zwischen den System- und Benutzrebenen.
Der Vorteil bei einem hier eingesetzen JIT ist, dass alle benötigten Module zu einer einheit zusammengeschmolzen werden, also dass man alle Module als Einheit sehen kann und es in der Optimierung keine wirklichen Grenzen zwischen diesen gibt.
Bei mir würden sich Treiber von Diensten und Programmen nur in ihrer Berechtigung für I/O-Schnitstellen (Ports, IRQs, spezifische Speicherbereiche)
unterscheiden. Also auch dei Treiber werden als Virtualbinäri geschrieben.
Kernel:
Verwaltung von Speicher, I/O-Ports und IRQs
Module mit Hardwarerechten:
Treibermodule
Module ohne Hardwarerechte:
Dienste
Programme
Die grundlegende API, die das OS bereitstellt ist NUR die Verwaltung der Systemresourcen und das Ausführen von Modulen.
Ich würde aber nicht versuchen zu irgendwelchen Standards kompatibel zu sein.
Ein großes Augenmerk habe ich auf die Programmiersprache gerichtet. Ich habe vor eine eigene Sprache zu entwickeln, wobei der Programmierer sich nicht mehr auf die Geschwindigkeitsoptimierung des Quellcodes konzentrieren müssen. Die Sprache soll so strukturierd werden, dass sie mit einem Compiler ohne Probleme optimiert werden kann.
Ist zwar ein bisschen schwieriger, aber ich arbeite dran...
BTW: Diese Abgränzung zwischen einer Kernelbasis(PORTS, IRQs, MEM-Bereiche) und Module könnte der OS-Dev Zene endlich zu Standarformaten für Treiber verhelfen. Man müsste sich nur auf ein Virtualbinärformat einigen. Wie das ganze dann erstellt wird oder wie ein Kernel es interpretiert kann dann der Programmierer entscheiden.
-
Ich habe mein Konzept seit dem letzten Post hier auch etwas geändert, das Konzept sieht jetzt so aus:
Das OS besteht aus einem kleinen Kernel der Prozesse, Thread, Speicher und Rechte für IO/Speicher verwaltet. Alle Anwendungen, die in einer sicheren Sprache geschrieben sind, laufen in Ring0. Anwendungen in unsicheren Sprache laufen in Ring3. Für die Beschleunigung des Ring0 Codes sorgen ein oder mehrere JIT Compiler. Die Treiber unterscheiden sich bei mir nicht von Anwendungen, mit der ausnahme, das wichtige Treiber in alle Prozesse gemappt werden _können_. Der Kernel ist in C implementiert und bereits fertig. Ein Java JIT, der selbst auch in Java geschrieben ist, ist auch in der Entwicklung (viel mehr als ein Hello World kriegt er nicht hin, aber alle Grundlagen (Methodenaufrufe, Klassen, Arrays usw.) sind komplett fertig, es fehlen nur noch Instructions die in 5-6 Zeilen Code implementiert werden können, wie ADD, SUB und so weiter^^. Allerdings kann der Compiler im Moment nur AOT kompilieren, da ich noch keinen JIT-Linker habe, und daher ld verwende, um den Code zu linken. Wenn du eine eigene Sprache schreibst, könntest du dich mal mit mir in Kontakt setzen, und wir könnten uns auf Standarts für die JITs einigen, so das sie portabel auf mehreren OS und mit dem gleichen Linker laufen.
-
Naja was heißt unsichere Sprache? Wenn du es durch deinen eigenen JIT durchlaufen lässt hast du auch die endgültige kontrolle über das Programm.
Ich für meien Teil habe mir vorgenommen NUR mein (noch nicht exestirendes) VBin Format zu benutzen.
Und das Problem von puren x86 Code leigt ganz einfach darin, dass ich nur im Ring 0 arbeiten will, und ich x86 Code erst paranoit überprüfen/modifizieren müsste um die Systemsicherheit zu erhalten.
Zur eigenen Progsprache:
Ich habe noch nichts konkretes in der Hand. Momentan bin ich noch dabei die Grundlagen der Informatik auseinanderzunehmen.
Dabei habe ich schon große Fortschritte gemacht bin jetzt aber bei solchen Sachen wie Platformunabhängigkeit, Template, Datentypen, Reduktionismuss, ... noch stark am grübeln. :-k
Aber für alle die ihren eigene Sprache schreieben wollen, es geht eigentlich nur um:
Ein-/Ausgabe, Arithmetik (auch Binärlogik und Vergleiche), Verzweigungen und Variablen.
Übrigens: kann mir jemand was über Variablen ohne genaue Datentypen erzählen? Gibt es ja auch bei Net oder Java. Und ich meine nich nur bei Templates.
-
Eine sichere Sprache, ist eine Sprache, die halt vom Design her sicherstellt, das Programme nicht wild im Speicher rumschreiben/auf die Hardware zugreifen können, sondern kontrolliert werden. Das macht ja in meinem Fall der JIT, wie du schon gesagt hast.
Naja, die Sprache muss ja nur Arithmetik, Kontrollfluss, und Variablen/Objekte verwalten können. Für die I/O Sachen kann man native Methoden benutzen, oder Treiber aufrufen, die dann native Methoden nutzen.^^
-
Sichere Sprache ist meiner Meinung nach eine schlechte Definition. Nur die Ausführung muss halt sicher sein. Denn im Endefekt machst du aus einem Quellcode einer Sprache (sicher oder nicht) eine "Binärdatei", welche aber nicht vor Veränderungen geschützt ist oder halt anders erstellt wurde.
Man könnte als besser "sichere Ausführungsschicht" sagen.
Und wie ruft man diese externen Methoden auf? Über I/O Funktionen der Programmiersprache!
Ich meinte auch nicht die Hardware-I/O sondern Funktionsaufrufe zu externen quellen. Die Module müssen ja auch irgendwie meiteinander und mit dem Kernel kommunizieren. Das Eingabe-Verarbeitung-Ausgabe Prinzip dürfte dir ja bakannt sein.
Und was genau meinst du mit Kontrollfluss?
-
Jumps (Gotos), Vergleiche und sowas.
-
Ja, eine sichere Sprache ist natürlich nur sicher wenn man den Code vor dem ausführen überprüfen kann. Also sicherstellen das der z.B. bei der JVM der Bytecode wirklich zu einem Java-Programm gehören kann.
Wenn du aus Javacode x86-Instruktionen erzeugst kannst du diese natürlich nicht mehr zur Laufzeit validieren, das stimmt schon. Andererseits kannst du z.B. C/C++ Code der noch gar nicht kompiliert wurde auch nicht so validieren das man sagen könnte das man den Code ohne Speicherschutz laufen lassen könnte, was bei Java-Quellcode jedoch gegeben ist.
Deshalb ist das ganze natürlich nur richtig sicher mit fremdem Code wenn wenn, diese "sichere Ausführungsschicht" vorhanden ist, aber ohne sichere Sprache wirst du diese gar nicht erst programmieren können.
Natürlich stellt sich auch die Frage ob man innerhalb der Klasse von sicheren Sprachen noch so einschneidende, aber potenziell lohnenswerte Veränderungen machen kann. Wahrscheinlich geht das, jedoch kann man damit schon ein eigenes Projekt füllen und besonders in der aktuellen Situation mit den verschiedenen virtuellen Maschinen finde ich es doof sich auf eine zu beschränken.
Sagen wir mal so, ich hatte mir auch ein Konzept überlegt, wo mehrere virtuelle Maschinen zum Einsatz kommen können, wobei ich selber keine neue erfinden wollte weil ich mit Java und C# eigentlich ganz zufrieden bin. Jedoch hielt mein Konzept auch den Weg zur Integration von "Eigenkreationen" frei ...
-
@SSJ7Gohan:
Vergleiche habe ich bei Arithmetik untergeordnet. Das ist mein momentaner Weg alles auf wenige Grundlagen zu reduzieren. Z. B. in C (C++?) ist der Wert 0=false und 0<>true. Daher bekommen bedingte Sprünge eigentlich nur einen Zahlenwert.
2 == 2 ist 1
2 == 3 ist 0
2 <> 2 ist 0
2 <> 3 ist 1
@Legend:
Den einzigen großen Unterschied von C++ und Jave, den ich kenn ist der Verzicht auf direkte Pointer in Java. Was gibt es da sonst noch? Habe von Jave leider keine Ahnung.
-
Von der Sprache her:
-> Keine Mehrfachvererbung, dafür gibt es Interfaces, die jede Klasse implementieren kann, die eingentlich nur abstarkte Klassen sind.
-> Kein Operatoroverloading
-> (Leider) keine unsigned Variablen
-> Bessere, umfangreichere Standardlib.
-> Es fehlt sicher noch sehr viel, das was hier steht ist mir grad mal so eingefallen.
Vom Design her:
-> Wird zu Bytecode kompiliert.
-
Von der Sprache her:
-> Keine Mehrfachvererbung, dafür gibt es Interfaces, die jede Klasse implementieren kann, die eingentlich nur abstarkte Klassen sind.
Da mit kenne ich micht nicht so gut aus.
-> Kein Operatoroverloading
Ich wollte in dem Sinne auch eine recht undynamische Sprache machen, die nichts der gleichen unterstützt.
-> (Leider) keine unsigned Variablen
Warum nicht? Ich weiß jetzt wirklich nicht warum unsigned nicht benutzt werden sollten/können...
-> Bessere, umfangreichere Standardlib.
Damit sind wahrscheinlich die ganzen API aufrufe gemeint, die nicht vom Programm erzeugbare Funktionen zur verfügung stellen.
-> Es fehlt sicher noch sehr viel, das was hier steht ist mir grad mal so eingefallen.
Hat mir dennoch einen tieferen Einblicke gegeben.
Vom Design her:
-> Wird zu Bytecode kompiliert.
OK, das war klar.
-
Nun ja, sagen wir mal so, der Verzicht auf Pointer verhindert erstmal genau das man wild im Speicher rumschreiben kann, weil ich immer mal +X rechnen können muss mit dem Pointerkonzept. Ohne Alternativen würde dieser Verzicht natürlich auch sinnvolle Programme verhindern. ;)
Deswegen gibt es bei Java Referenzen auf Objekt und Arrays mit Bounds-Checking als Ersatz, so dass man auch keine Pointer braucht.
Mit diesen Möglichkeiten kann man kein Bit Speicher das ausserhalb des dem Programm zugeteiltem Bereich liegt erreichen.
Dadurch kann man Java Programme unbesorgt in den selben Addressraum packen, solange nirgendwo ein Bug ist können die sich nicht gegenseitig überschreiben. Hierzu muss man noch sicherstellen das es Java Programme sind, zum Bytecode ist in der VM Spec der JVM beschrieben wie man das macht.
Auch bei syntaktisch korrekten C Programme kann man so eine Aussage nicht machen. char* ptr = 0xABCDEF; memset (ptr, ...); ? ;)
-
Ich habe mir jetzt schon viele Gedanken über die möglichen Programmierstrukturen und das Ausgabeformat gemacht.
Wie kann man beides voneinander unabhängig entwickeln?
Hört sich seltsam an?
Dass soll einfach nur heißen: wie kann man die eigentliche Programmiersprache weiterentwickeln und mit neuen Programmiertechniken versehen ohne gleich ein neues Ausgabeformat zu benötigt.
Dafür muss allerdings erst ein solides Ausgabeformat her. Mit diesem Format könnte man dann auch getrente Wege gehen und jeweils eigene Compiler entwickeln.
Was ich bis jetzt vom Inhalt dieses Formates sagen kann ist:
-sollte möglichst schnell und einfach von einem Programm zu lesen sein.
-Baumstrukturen für Abhänigkeiten von Variablen oder Verzweigungen
-Eventuell eine aufteilung in Initalisierungsteil und Laufzeitteil
Also besonders solche Sachen die eine JIT Optimierung benutzen kann.
Bei den Abhänigkeitsstrukturen kann das noch ein bisschen schwerer werden. Als kleines Beispiel:X = sin(y)/pi+pi*360
if Bedingung1 then A = X
kann vom Compiler ganz einfach inif Bedingung1 then A = sin(y)/pi+pi*360
umgewandelt werden.
Aber was ist in einem solchen Fall:X = sin(y)/pi+pi*360
if Bedingung1 then A = X
if Bedingung2 then B = X
Wenn nur eine der Bedingungen ausgeführ wird soll sin(y)/pi+pi*360 gerechnet werden. Sind jedoch beide Bedingungen erfüllt soll sin(y)/pi+pi*360 dennoch nur einmal berechnet werden.
Es gibt viele Dinge die man beachten muss, z.B. ob man die Dateien grundsätzlich in little-endian abspeichert und damit zukünftige Platformen wie PPC benachteiligt.
Ich möchte dem JIT halt soviel wie möglich vorkauen um eine optimale Geschwindigkeit zu erreichen.
-
Naja, ich würde erstmal die Sprache selbst festlegen, bevor ich das Ausgabeformat festlege.
Die VM würde ich auf einer virtuellen Maschine mit beliebig vielen Registern aufbauen und nicht auf einem Stack wie in Java oder .NET. Das hat den Vorteil, das man einige Optimierungen besser vornehmen kann, wie z.B. Typchecks zur Compilierzeit. Die Register kann man dann teilweise 1:1 auf die realen Register übertragen und muss nicht den Umweg über Stackzugriffe nehmen.
Ob du Little oder Big Endian benutzt ist nicht wirklich wichtig, der JIT muss dann zwar eventuell beim Laden der Anwendung etwas rumshiften, aber das solle keinen bemerkbaren Performanceunterschied geben.
-
@Osbios: Für den 2. Fall hast du wahrscheinlich die optimale Möglichkeit (ohne das man irgendwelche Verteilungen vom Eintreten der Bedingungen kennt) schon hingeschrieben! ;)
Du hast aber einen grundlegenden Unterschied schon mal zum Java Bytecode drin der schon mal ein potenzieller Grund für ein neues Format darstellt: Du willst alles für maximales Tempo drin. Der Java Bytecode sollte in erster Linie kompakt sein, damit die Programme schnell übers Internet übertragen werden können.
Allerdings ist deine Forderung nach einem halbwegs universellem, sicherem Bytecode ähnlich der Anforderungen des Bytecodes von .NET.
@SSJ7Gohan: Ja, das mit der Maschine mit unendlich vielen Register kam mir auch mal in den Sinn. Jedoch hatte ich mal gelesen das die stack-basierten Maschinen ein Stück mehr erforscht sind als die register-basierten. Jedoch verwendet GCC an einer Stelle in seinem Kompilierprozess so eine Register-Maschine! Jedoch brauchst du immer noch einen Algorithmus zur Register Allokation, ausser der Code braucht weniger Register als die CPU hat. ;)
Nun ja, vieles kann man zur Ladezeit machen. Programme die Daten über das Internet verschicken wollen werden aber sich aber darauf verlassen müssen das die Daten die sie zur Laufzeit erzeugen die richtige Reihenfolge haben.
Ich denke optimales Tempo kann man wahrscheinlich erreichen indem man Instruktionen die möglichst nahe an klassichen CPUs liegen anbietet, jedoch ohne die Sicherheit zu gefährden, sonst kann man sich ne VM auch praktisch sparen! ;)
-
X = sin(y)/pi+pi*360
if Bedingung1 then A = X
kann vom Compiler ganz einfach in
Code:
if Bedingung1 then A = sin(y)/pi+pi*360
umgewandelt werden.
was machst du mit folgendem?
X = sin(y)/pi+pi*360
if Bedingung1 then A = X
print(X)
und diesem hier?
X = sin(y)/pi+pi*360
if X < 100 then A = X
und woher weisst du dass sin(y) nicht vielleicht mehr macht, als nur einen wert zurückzugeben? vielleicht schreibt es was auf den monitor oder so.
und was machst du hiermit:
X = 1/sin(y)
if Bedingung1 then A = X
unter bestimmten bedingungen wird sin(y) gleich 0 und es gibt eine divide-by-zero-exceptionen. was, wenn das programm in diesem fall auch genau eine exception braucht?
if Bedingung1 then A = 1/sin(y)
dieser code kann nur dann eine divide-by-zero-exception auslösen, wenn auch Bedingung1 erfüllt ist. die beiden code abschnitte sind also nicht gleichwertig.
Aber was ist in einem solchen Fall:X = sin(y)/pi+pi*360
if Bedingung1 then A = X
if Bedingung2 then B = X
if Bedingung1 or Bedingung2 then X = sin(y)/pi+pi*360
if Bedingung1 then A = X
if Bedingung2 then B = X
oder besser:
if Bedingung1 or Bedingung2 then
X = sin(y)/pi+pi*360
if Bedingung1 then A = X
if Bedingung2 then B = X
end if
aber was wenn Bedingung1 oder Bedingung2 sehr kompliziert sind? z.B. Bedingung1 ist "(x < 500 * y - sin(alpha) + cos(beta))" und Bedingung2 ist "(customfunction(a, b, c) == d)"
if (x < 500 * y - sin(alpha) + cos(beta)) or (customfunction(a, b, c) == d) then X = sin(y)/pi+pi*360
if (x < 500 * y - sin(alpha) + cos(beta))then A = X
if (customfunction(a, b, c) == d) then B = X
optimiert sähe das so aus:
bool TempBedingung1 = (x < 500 * y - sin(alpha) + cos(beta))
bool TempBedingung2 = (customfunction(a, b, c) == d)
if TempBedingung1 or TempBedingung2 then
X = sin(y)/pi+pi*360
if TempBedingung1 then A = X
if TempBedingung2 then B = X
end if
oder noch besser:
if (x < 500 * y - sin(alpha) + cos(beta)) then
A = sin(y)/pi+pi*360
if (customfunction(a, b, c) == d) then B = A
else if (customfunction(a, b, c) == d) then B = A
B = sin(y)/pi+pi*360
end if
was aber tust du bei folgendem code (ich nehm den weil er simpler als die anderen ist):
X = customfunction(y)*sin(y)/pi+pi*360
if Bedingung1 then A = X
wenn du den so optimierst:
if Bedingung1 then A = customfunction(y)*sin(y)/pi+pi*360
wird customfunction() nur noch aufgerufen, wenn Bedingung1 erfüllt ist. was ist wenn customfunction() mehr macht als nur den rückgabewert zu schreiben?
sowas?
if Bedingung1 then A = customfunction(y)*sin(y)/pi+pi*360 else (void)customfunction(y)
oder das?
temp = customfunction(y)
if Bedingung1 then A = temp*sin(y)/pi+pi*360
wie du siehst gibt es milliarden von sonderfällen. du hast ein paar erwischt, die vielleicht optimierbar sind. aber wirst du die auch in der echten welt an den stellen, die wirklich performancekritisch sind, antreffen?
ich habe mir auch ein paar gedanken gemacht, wie ich einen (normalen) compiler optimieren könnte, und bin zu dem entschluss gekommen, dass meine ideen höchstens den code von unfähigen programmieren optimieren könnten (wenn überhaupt) und _wirkliche_ optimierungen nicht vollbringen werden können. ich habe mich nur soweitgehend damit beschäftigt, bis ich davon überzeugt war, dass sowas anderen zu überlassen und mich mit interessanteren dingen zu beschäftigen sinnvoller ist. vielleicht hast du ja bessere ideen und mehr ausdauer als ich. mit deinen beispielen wirst du allerdings nicht sehr weit kommen. sie beschränken sich auf ein paar mathematische formeln. die wenigsten programme bestehen nur aus formeln. es gibt noch schleifen, globale variablen, funktionsübergreifene strukturen (einfachstes beispiel: parameter), rekursionen, etc. diese sachen sind deutlich schwerer zu optimieren. richtig spaßig wirds auch erst, wenn du für multicore-cpus optmierst. ;)
-
Naja, ich würde erstmal die Sprache selbst festlegen, bevor ich das Ausgabeformat festlege.
Ach, und bei x86 Prozessoeren hat man auch erst die Sprachen festgelgt und dann die CPU entwickelt? :wink:
Nein kommt nicht in Frage, zuerst ein solides Ausgabeformat nach dem sich dann die Sprachen zu richten haben!
Die VM würde ich auf einer virtuellen Maschine mit beliebig vielen Registern aufbauen und nicht auf einem Stack wie in Java oder .NET. Das hat den Vorteil, das man einige Optimierungen besser vornehmen kann, wie z.B. Typchecks zur Compilierzeit. Die Register kann man dann teilweise 1:1 auf die realen Register übertragen und muss nicht den Umweg über Stackzugriffe nehmen.
Ich denke mehr an eine System das ohne beides auskommt.
Ob du Little oder Big Endian benutzt ist nicht wirklich wichtig, der JIT muss dann zwar eventuell beim Laden der Anwendung etwas rumshiften, aber das solle keinen bemerkbaren Performanceunterschied geben.
Hmm ok, dass dürfte eigentlich nicht soviel Zeit benötigen.
@Osbios: Für den 2. Fall hast du wahrscheinlich die optimale Möglichkeit (ohne das man irgendwelche Verteilungen vom Eintreten der Bedingungen kennt) schon hingeschrieben!
Nein habe ich nicht, aber sie währe (meine Meinung nach):if Bedingung1 then
{
X = sin(y)/pi+pi*360
A = X
if Bedingung2 then B = X
}
else
if Bedingung2 then B = sin(y)/pi+pi*360
Du hast aber einen grundlegenden Unterschied schon mal zum Java Bytecode drin der schon mal ein potenzieller Grund für ein neues Format darstellt: Du willst alles für maximales Tempo drin. Der Java Bytecode sollte in erster Linie kompakt sein, damit die Programme schnell übers Internet übertragen werden können.
Allerdings ist deine Forderung nach einem halbwegs universellem, sicherem Bytecode ähnlich der Anforderungen des Bytecodes von .NET.
Nunja, ich will damit auch Treiber Schreiben und die im Ring 0 laufen lassen. Da muss schon etwas Geschwindigkeit und Sicherhheit vorhanden sein.
Nun ja, vieles kann man zur Ladezeit machen. Programme die Daten über das Internet verschicken wollen werden aber sich aber darauf verlassen müssen das die Daten die sie zur Laufzeit erzeugen die richtige Reihenfolge haben.
Dafür gibt es in standard Netzerk Libs, Systemunabhängige Funktionen, die dafür sorgen, dass für bestimmte dinge nur eine Bytereihenfolge im Netzwerk benutzt wird.
Ich denke optimales Tempo kann man wahrscheinlich erreichen indem man Instruktionen die möglichst nahe an klassichen CPUs liegen anbietet, jedoch ohne die Sicherheit zu gefährden, sonst kann man sich ne VM auch praktisch sparen!
Das mit der Sicherheit dürfte uns allen klar sein, aber ich bin nicht der Meinung, dass man führ eine hohe Geschwindigkeit, CPU nahe Instruktionen nutzen sollte.
@PorkChicken:X = sin(y)/pi+pi*360
if Bedingung1 then A = X
print(X)
X = sin(y)/pi+pi*360
if X < 100 then A = X
Bei diesen beiden Varianten wird X explizit von einer folgenden Berechnung benötigt, daher spielt es keine Rolle ob sie woanders eventuell benötigt oder nicht benötigt wird.
und woher weisst du dass sin(y) nicht vielleicht mehr macht, als nur einen wert zurückzugeben? vielleicht schreibt es was auf den monitor oder so.
Ich weiß es weil ich alles, bis auf die Kernelfunktionen, in Module unterbringen werde. Man kann daher relativ einfach ein neues Modul den den schon vorhandenen Modulen verschmelzen.
Als Beispiel: mal angenommen man hätte 2 Module, wobei das eine auf die angebotenen Funkionen des anderen zugreift.
X = 8663
Funktion XY
{
return X
}
A = 1337
B = A + XY
Hierbei kann man erkennen das XY nur eine Konstante zurück gibt und man dann zwei Konstandten addieren kann:
B = 10000
Das ist natürlich ein sehr einfaches Beispiel, es soll auch nur zeigen, dass man sehr wohl wissen kann was die einzelnen Funktionen aufrufen wenn man ein ganzes System darauf aufbaut.
und was machst du hiermit:
X = 1/sin(y)
if Bedingung1 then A = X
unter bestimmten bedingungen wird sin(y) gleich 0 und es gibt eine divide-by-zero-exceptionen. was, wenn das programm in diesem fall auch genau eine exception braucht?
if Bedingung1 then A = 1/sin(y)
dieser code kann nur dann eine divide-by-zero-exception auslösen, wenn auch Bedingung1 erfüllt ist. die beiden code abschnitte sind also nicht gleichwertig.
Ein Programm das eine exception brauch? Ich würde vorschreiben das man Teiler auf <> 0 Testen müsste um unvorhergesehene "Porgrammeigenschaften" zu vermeiden. Nein, ehrlich gesagt weiß ich noch nicht wie man X/0 handhaben sollte.
if Bedingung1 or Bedingung2 then
X = sin(y)/pi+pi*360
if Bedingung1 then A = X
if Bedingung2 then B = X
end if
Ich würde die doppelten if then vermeiden. Hab ich zwar schon weiter oben hingeschrieben, aber hier nochmal:if Bedingung1 then
{
X = sin(y)/pi+pi*360
A = X
if Bedingung2 then B = X
}
else
if Bedingung2 then B = sin(y)/pi+pi*360
Ich hoffe das mit dem Modlen, als System und Optimierungsbasis, ist rübergekommen.
Nochmal in Kurzform:
Es gibt nur ganz wenige Funktionen die direkt vom JIT bzw. Kernel zur Verfügung gestellt werden. Alle anderen Funktionen werden von Modulen bereitgestellt.
Der JIT kann auf die Module übergreifende Optimierungen vornehmen.
Wieder ein kleines Beispiel anhand eines Treibermodules und eines Programmmodules (das keine I/O Rechte besitzt).
Wenn das Programm auf eine Funktion des Treibermodules zugreift, die auf einen Port schreibt, kann es sein, dass der JIT zur Optimierung den CALL-Aufruf umgeht indem er einfach den entsprächenden Code des Treibers im Laufzeitcode des Programms einfügt.
Dann ist im Laufzeitcode des Programms ein I/O Zugrif obwohl das Programm garkeine I/O berechtigung hat.
-
Was für eine VM hast du denn im Sinn, die nicht Register oder Stackbasiert ist?
-
Eine Standard Lib, na ja, Little und Big Endian in den Datenpaketen selber, welche von der Anwendung generiert werden, wird damit aber schon schwierig.
Mit CPU-fernen Instruktionen wirst du mehr nachbauen müssen und das ganze wird daher wohl auch langsamer. Instruktionen die komplexe Operationen in einem Schritt machen kann man auch nicht bei Bedarf aufteilen und anpassen.
-
Eine Standard Lib, na ja, Little und Big Endian in den Datenpaketen selber, welche von der Anwendung generiert werden, wird damit aber schon schwierig.
Mit CPU-fernen Instruktionen wirst du mehr nachbauen müssen und das ganze wird daher wohl auch langsamer. Instruktionen die komplexe Operationen in einem Schritt machen kann man auch nicht bei Bedarf aufteilen und anpassen.
Das ist der nachteil von Cisc.
-
Durchaus, auch wenn manche Operation durchaus schneller in Hardware gegossen werden kann als aus kleineren zusammen gebaut werden kann. Da muss man halt ne Balance finden.
Der Java Compiler kaut dem JIT eigentlich nichts vor, er optimiert kaum - es gibt auch kaum was zu optimieren. Das was der ausspuckt ist so unspezifisch in der Art "Erstelle Objekt", "Rufe Funktion XY virtuell auf" das es da auch nichts zur Kompilierzeit gibt was man tun könnte.
-
So, mein Java-Compiler ist nun fast vollständig (von den Instructions her, Optimierungen fehlen noch), bis auf einige Einschränkungen:
-> lookupswitch und tableswitch Instructions, die lassen sich aber auch sehr leicht einbauen.
-> monitorenter und monitorleave, solle auch nicht schwer sein.
-> Double, Float und Long Datentyp. Ich weiß nicht wie ich 64 Bit Longs dividieren soll, siehe Thread in Lowlevel Coding.
-> Kein Classfile Verifizierer.
-> Kann im Moment nur AOT nasm kompatiblen ASM-Code ausgeben, und nicht JIT x86-Code ausgeben
-> Wie gesagt keine Optimierungen, der Code den er im Moment generiert sieht etwa so aus:
public static void main (String[] args) {
int zahl = 500;
zahl -= 50;
zahl /= 10;
System.out.println("Hello World");
System.out.println(zahl);
}
Wird durch meinen JIT zu (nachdem es von javac von Sun zu einem classfile kompiliert wurde und dann von meinem "JIT" zu ASM kompiliert wurde):
[extern java_new] ;macht das gleiche wie malloc
[global vmtest_47test__class]
vmtest_47test__class:
dd vmtest_47test__clist ;pointer auf superclass-liste
dd vmtest_47test__ilist ;pointer auf interface-liste
dd vmtest_47test__fields ;pointer auf statische felder
vmtest_47test__vtable: ;virtual method table
[extern java_47lang_47Object__m0]
dd java_47lang_47Object__m0
dd vmtest_47test__m0
dd vmtest_47test__m1
vmtest_47test__clist:
dd 2 ;anzahl der klassen, die diese klasse ableitet
[extern java_47lang_47Object__class]
dd vmtest_47test__class ;pointer auf die klasse
dd java_47lang_47Object__class
vmtest_47test__ilist:
dd 0 ;anzahl der interfaces die diese klasse implementiert
vmtest_47test__fields: ;statische felder
;der string "Hello World" als UTF8 Array
vmtest_47test__c23:
dd 11 ;anzahl der elemente in einem array
dw 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100 ;elemente
;funktion no.0 der klasse vmtest.test (Konstruktor)
[global vmtest_47test__m0]
vmtest_47test__m0:
; stacksize: 4 bytes
push ebp
mov ebp, esp
sub esp, 4
m0__i0:
; instLoad
;lokale variable auf den stack pushen
;parameter0 ist immer der this-pointer
mov eax, [ebp + 8]
push eax
m0__i1:
; instInvoke
[extern java_47lang_47Object__class]
.invoke:
mov eax, [esp + 0] ;pointer auf objekt holen
mov ebx, [eax] ;dword an offset 0 des objekts = pointer zu klasse holen
mov ecx, [ebx] ;anzahl elemente in der klassenliste holen (offset 0 in der klasse = pointer zu klassenliste; offset 0 in der klassenliste = anzahl klassen)
.search:
mov eax, [ecx * 4 + ebx] ;gucken ob klasse an der position die gesuchte ist
cmp eax, java_47lang_47Object__class
jne .next ;wenn nicht, dann nächste klasse überprüfen
call [eax + 12] ;sprung in die vtable der klasse
jmp .done ;fertig
.next:
loop .search ;nächste klasse
.unresolvable: ;hier sollte noch ne exception hin
.done:
add esp, 4 ;elemente vom stack popen
m0__i4:
; instReturn
mov esp, ebp
pop ebp
ret
; methode no.1 der klasse (main)
[global vmtest_47test__m1]
vmtest_47test__m1:
; stacksize: 8 bytes
push ebp
mov ebp, esp
sub esp, 4
m1__i0:
; instConst
mov eax, 500
push eax
m1__i3:
; instStore
pop eax
mov [ebp - 4], eax
m1__i4:
; instIncrement
mov eax, [ebp - 4]
add eax, -50
mov [ebp - 4], eax
m1__i7:
; instLoad
mov eax, [ebp - 4]
push eax
m1__i8:
; instConst
mov eax, 10
push eax
m1__i10:
; instArithmetic
pop ebx
pop eax
xor edx, edx
idiv ebx
push eax
m1__i11:
; instStore
pop eax
mov [ebp - 4], eax
m1__i12:
; instFieldGet
;statisches feld holen
[extern java_47lang_47System__f0]
mov eax, [java_47lang_47System__f0]
push eax
m1__i15:
; instPop
add esp, 4
m1__i16:
; instNew
;speicher allokieren
;anzahl bytes in eax
mov eax, 8
call java_new
mov dword [eax], java_47lang_47String__class
push eax
; instDuplicate
mov eax, [esp]
push eax
; instConst
mov eax, vmtest_47test__c23
push eax
;Konstruktor aufrufen, siehe oben
; instInvoke
[extern java_47lang_47String__class]
.invoke:
mov eax, [esp + 4]
mov ebx, [eax]
mov ecx, [ebx]
.search:
mov eax, [ecx * 4 + ebx]
cmp eax, java_47lang_47String__class
jne .next
call [eax + 20]
jmp .done
.next:
loop .search
.unresolvable:
.done:
add esp, 8
m1__i18:
;statische funktion aufrufen
; instInvoke
[extern java_47io_47PrintStream__m1]
call java_47io_47PrintStream__m1
add esp, 4
m1__i21:
;statisches feld holen und auf den stack pushen
;der javac hat den code hier so generiert, man könnte ihn auch weglassen, da im Moment System.out.println eine statische Funktion ist, und man den Pointer auf System.out nicht braucht.
; instFieldGet
[extern java_47lang_47System__f0]
mov eax, [java_47lang_47System__f0]
push eax
m1__i24:
; instPop
add esp, 4
m1__i25:
; instLoad
mov eax, [ebp - 4]
push eax
m1__i26:
; instInvoke
;statische funktion aufrufen
;und einen parameter vom stack popen
[extern java_47io_47PrintStream__m2]
call java_47io_47PrintStream__m2
add esp, 4
m1__i29:
; instReturn
mov esp, ebp
pop ebp
ret
Ich hab den Code mal etwas kommentiert.
Einige Instructions überprüfen auch nicht auf Null-Pointer, das muss ich noch einbauen.
Die Geschwindigkeit eines so simplen JITs hat mich schon beeindruckt, er ist auch ohne Optimierung nur 5-15% langsammer als Suns Hotspot VM, und hat einen erheblich kleineren Speicherbedarf.^^
Ich werden in der nächsten Zeit (ich denke so in einer Woche) mal den schon vorhandenen Code unter der GPL online stellen und hoffen, das sich hier jemand an dem Projekt mitbeteiligen will :)
Ich werde dann mit der Programmierung der Treiber mit Java beginnen, und irgentwann auch x86-code output in den kompiler einbauen und einen JIT-Linker schreiben.
-
Durchaus, auch wenn manche Operation durchaus schneller in Hardware gegossen werden kann als aus kleineren zusammen gebaut werden kann. Da muss man halt ne Balance finden.
Das ist falsch.
http://de.wikipedia.org/wiki/CISC
Der Java Compiler kaut dem JIT eigentlich nichts vor, er optimiert kaum - es gibt auch kaum was zu optimieren. Das was der ausspuckt ist so unspezifisch in der Art "Erstelle Objekt", "Rufe Funktion XY virtuell auf" das es da auch nichts zur Kompilierzeit gibt was man tun könnte.
Langsam bekomme ich auch ein besseres Bild von Java und kann nur sagen, dass sich meine Philosophie stark davon unterscheidet. Ich tendiere immer mehr in die Richtung komplette Erzeugung von x86 Code aus den Modulen, da Optimierungen wie Hotspot den CPU Cache zerschießen, was wiederum einiges an Performanceverlust nach sich zieht.
@SSJ7Gohan:
Nur 5-15 % langsammer als dieses Hotspotteil? Oo
Das muss ich mir dann auch mal angucken.
-
Bei VMs ist aber ein komplexes Instructionsset besser, auf RISC Prozessoren kann man dann ja mit dem JIT die komplexen Instructions zerlegen und der JIT kann für prozessorspezifische Instructions wie MMX, SSE usw. optimieren.
-
Nun ja, Osbios, ganz so falsch ist das nicht. Der extreme CISC Ansatz ist zwar veraltet, der extreme RISC Ansatz auch. Wenn man es mit RISC übertreibt bestehen die Programme nachher aus so vielen Instruktionen das auch eine höhere MISP Leistung da nicht gegensteuern kann. Da brauch man ne Balance. Ein Beispiel wär der Extremfall das eine Maschine nur eine Instruktion kann, da gibt es eine mit der man eigentlich alles programmieren kann. Aber dann brauchen die einfachsten Sachen schon ellenlange Codelistings.
@SSJ7Gohan: In der Theorie ja. Du lässt den JIT die Arbeit übernehmen, weil mit einem Instruktionsset wie bei Java der javac eigentlich auch gar nichts machen kann. Du musst nur aufpassen das dein JIT dann nachher nicht so viel tun muss das er mehr Zeit bräuchte als Code mit einem einfacherem Instruktionsset. Das ist zumindestens meine Meinung, ob da irgendwas bewiesen ist in der Theorie weiss ich ehrlich gesagt nicht.
Ich glaube das dürfte ein Problem von Hotspot sein. Das misst ja verschiedene Performancedaten zur Laufzeit, das klingt wie ein Lauf mit nem Profiler. Den kann Hotspot natürlich effizienter einbauen als man dies bei C++ Code wohl machen kann, aber ich weiss nicht ob dies nicht evtl. stark auf die Performance gehen kann. ;)
-
Ja stimmt scho, sonst kann man auch gleich mit Brainfuck anfangen. ](*,)
Ein Beispiel wär der Extremfall das eine Maschine nur eine Instruktion kann, da gibt es eine mit der man eigentlich alles programmieren kann.
Nein das glaube ich nicht. :P
Min. 2 braucht man schon!
-
Also mit Substract and branch on less then zero konnte man schon multiplizieren und schleifen bauen. if, addieren usw. geht auch ... ;)
-
Also mit Substract and branch on less then zero konnte man schon multiplizieren und schleifen bauen. if, addieren usw. geht auch ... ;)
und es lässt sich mathematisch beweisen, dass so eine one-instruction-maschine turing-vollständig ist. also alles machen kann, was andere cpus auch können.
-
Bloß glaube ich das man mit ein paar mehr Instruktionen ne schnellere CPU bauen kann ...
-
So, ich hab jetzt den JIT-Compiler nochmal überarbeitet.
Er hat jetzt einen Classfile Verifizierer, der die Typchecks wärend Compilierens vornimmt. Da der Compiler wegen des Verifizierers nun weiß, auf welche Elemente des Stacks er zugreift, kann der Stack auf eine Maschiene mit unendlich vielen Registern gemappt werden.
Beispiel:
Java Code zum addieren von zwei Ints und zurückgeben des Ergebnisses:
// bei java werden die parameter einer methode als die ersten paar lokalen variablen übergeben.
// bei nicht-statischen Methoden enthällt die lokale Variable 0 eine Referenz auf das Object (this-pointer)
// ich stelle lokale vars mal als v<n> da.
iload v1 //pusht lokale int variable 1 auf den stack
iload v2 //pusht lokale int variable 2 auf den stack
iadd // popt 2 int-operanden vom stack, addiert sie und pusht das ergebniss
ireturn // popt einen intwert und gibt ihn zurück
Mein Compiler wandelt das um in:
// r<n> sind hier die register
move int v1, r0 //lokale variable 1 in register 0
move int v2, r1 //lokale variable 2 in register 1
add int r0, r0, r1 //addiere r0 und r1, ergebniss in r0
return int r0 // gebe r0 zurück
Dieser Zwischencode kann natürlich viel besser optimiert werden als der Java-Bytecode, und es können fast alle herkömmlichen Optimierungen angewandt werden. Eventuell könnte man auch sowas wie HotSpot einbauen und langsame Optimierungen nur bei Bedarf vornehmen. Dann müsste man aber für das Onstack-Replacement extream viele Informationen speichern, und man sieht ja wie viel Speicher die HotSpot VM verbraucht.