[self-interest] embedding based prototype languages

Jecel Assumpcao Jr jecel at merlintec.com
Wed Nov 6 23:48:56 UTC 2002


| claimed a 1 clock dispatch for an embedded Self (recently renamed 
"Oliver Smalltalk" in an effort to make other Smalltalkers see that we 
are really a part of their team).

On Wednesday 06 November 2002 18:26, James McCartney wrote:
> OK I've been curious about this ever since you posted it. If it is
> not a secret, I wish you would tell more about it. I assume you are
> flattening parents into a single object.

There are several tricks that work together. A simple masking function 
is used to convert an object's pointer into a pointer for its map, so 
we don't have to look that up in the object's first word or something. 
The drawback is that sometimes an object has to change maps after a 
programming change (added or removed a slot) and that means changing 
the object's pointer, which means scanning the whole memory to update 
any references to the old pointer.

This is a 16 bit machine so I can't afford to have many objects. So the 
sources for all of an object's slots are gathered into one string which 
is stored in the object's map (so it doesn't take up an object pointer 
of its own). The object's "outliner" gives you a more Self-like view, 
so this implementation detail is hidden. When you make any programming 
changes the new string is used to build a new map, which is then 
compiled and finally any of the object's children are recompiled.

Besides the source string, a map contains a table and machine language 
code. This table includes a two word entry for every single selector 
defined in the system. There can be up to 4096 selectors, so this per 
map table can grow to be as much as 16KB!! But like I said, I have 
memory to spare. The tables for all maps are always exactly the same 
size and they grow as you define new selectors.

Most instructions (except for "send") are only 5 bits long, so up to 
three of them can fit in a 16 bit word. With two words we can have up 
to 6 instructions or 3 instructions plus a 16 bit literal. When we 
compile a map, for each possible selector we fill in its two word entry 
with the right code. If it is a constant slot, we do something like

              pushLiteral  42 ; return

while if it is a data slot we might do

             read 3 ; return

or an assignment slot

            write 3 ; return

and if it is a method slot we can simply stuff the code into those two 
words if it will fit, or else put the code at the end of the map and 
put a jump to that code in the two words.

Note that most entries would have nothing defined for them in a given 
object, so we simple compile code to call the debugger with a "message 
not found" error. These entries are considered "empty" in what is 
described next.

Before actually compiling a map, we copy the table and extra code from 
its first parent (which already includes everything from its parents), 
then everything from the second parent and so on. Then we compile the 
map overwritting anything copied from the parents.

So at runtime, given the pointer to an object and a selector of a 
message to send to it, we can

  1) find the object's map with no memory accesses
  2) fetch from 1 the first word from the entry corresponding to the 
selector
  3) execute that word

> Do you also flatten the
> instance's own slots along with the parents?

If you mean that you can't tell what came from the object and what came 
from the parent, you are right.

What I described above won't work in some cases: selectors that access 
data slots in the parent and resends. In the first case, code is 
compiled to explicitly delegate to the parent. It is like adding this 
local slot:

              buttonColor: c = (globals ui preferences buttonColor: c)

I actually got rid of the lobby and globals so this sort of thing 
wouldn't  be so common (it adds a lot of overhead to every single 
map!).

And about the resends, the compiler simply inlines the parent's source 
for the method into the spot where the resend was.

> It would seem like you'd
> need to hash in that case, but obviously you're not since you say 1
> clock. But I'm not even sure what that means since even an array
> lookup is not 1 clock.

If we are executing a method in objA and the current top of stack points 
to objB, we can have these accesses via the cache (which needs a 16 
object pointer and a 16 bit field offset to find the 16 bit word we 
want):

clock N:    cache ( map(objA), PCa ) ===> "send selector23"
clock N+1:   cache ( map(objB), 23<<1 ) ===> "read 3 ; return"
clock N+2:   cache ( objB, 3 ) ===> data slot 3's contents
clock N+3:    ----
clock N+4:   cache ( map(objA), PCa+1) ===> "selfsend selector92"

It is annoying that the return takes 2 clocks (N+3 and N+4). With a 
little more hardware that can be reduced to 1 clock or even, when the 
instruction in N+2 doesn't do a memory access (which "read 3" does, but 
most don't), 0 clocks.

Anyway, what I mean by "1 clock dispatch" is that in clock N we fetched 
a send instruction and in clock N+1 we were already fetching the first 
instruction from the right method. This is possible because the 
hardware is a simple stack machine which isn't pipelined.

-- Jecel



More information about the Self-interest mailing list