On Sat, Jul 20, 2002 at 11:51:49AM +0200, Marcus Denker wrote:
All "modern" systems seem to work somewhat like that: They have a *very* fast compiler (very much like J3, maybe even generating worse code) that is called if a method is run the first time. This compiler adds profiling code and uses PICs (Polymorphic Inline Caches) that allow to collect information about what types are actually used at runtime. Then a slow, highly optimizing compiler is called for all hotspots, that is methods that are run very often. This compiler can use the typeinfos collected in the PIC to do very clever optimizations that are even not possible with a static (e.g. C++) compiler.
Also, once a method is compiled to machine code, why would there be any need to keep the bytecodes around (as Tim descibes)? Can't you just throw away the old bytecodes at that point? Ah...but perhaps it's so that the image can remain portable to different architectures...is that it?
Bytecodes... I don't like bytecodes. Actually they are a bad idea if you are generating native code: A modern jit-compiler needs a representation of the code that is exactly the one that the smalltalk-compiler had just before it generated the bytecodes. So the first thing a Jit does is decompiling... it has to spend time to regenerate stuff we allready had. So IMHO it would be interesting to look at other representations for compiled methods than bytecodes: Something very simple, no optimizations (like ifTrue: inlining, special sends, eg...) Just a serialised AST.
Bytecode is GREAT for WHAT IT IS GREAT FOR. :) Rather, bytecode is awesome at being memory efficient and fast to interpret. Due to its compactness and the fact that a proper bytecode encapsulates very complex operations into very little space, it can actually end up running faster than non-optimized native code. You really need BOTH syntax trees (or some other graph-like representation) and bytecode to make a great dynamic compilation system. Bytecode will take care of 90% of the code that just needs to be run infrequently and take up inconspicuous amounts of space, while the other representation will allow you to optimize when necessary.
But Squeak of course needs an interpreter: Why not treat the Interpreter as just another target for the JIT-Compiler? Then every new optimization we implement for the compiler would even help to make squeak running faster in interpreted mode (actually, all most optimizations could work for an interpreter, even Inline Caches, Inlining of sends, eg...).
This is a great idea. I've been designing a dynamic compiler for a new language I am currently working on and it incorporates this very idea. Firstly, code is simply adaptively recompiled based on the stored non-bytecode representation, and initially only uses bytecode. After a certain amount of recompilation it will invest the time to switch over to native code, with the already heavily recompiled bytecode/syntax tree available to work from. The net effect is that bytecode is adaptively optimized, which gains signifigant speedups without having to invest all the time to go to native code until it is truely needed.
This would essentially allow us to "late bind" the real-world Interpreter and change the bytecodes used from one release to another.
The clean and simple representation for code in compiledmethods could maybe used for other things, too: Maybe this could be a nice thing for Alan's ideas of a very clean Squeak Kernel. (An interpreter for that would be very simple, but very slow). And the eToy and omniuser tiles could be generated from this representation... maybe we could even think about trowing away the source (:-)).
just some strange ideas...
Marcus
-- Marcus Denker marcus@ira.uka.de -- Squeak! http://squeakland.org
Lee