Front page | perl.perl6.internals |
Postings from March 2004
Re: newbie question....
From: Dan Sugalski
March 12, 2004 09:00
Re: newbie question....
Message ID: firstname.lastname@example.org
At 6:06 PM -0500 3/11/04, Matt Greenwood wrote:
> I have a newbie question. If the answer exists in a doc, just
>point the way (I browsed the docs directory). What is the design
>rationale for so many opcodes in parrot? What are the criteria for
Whether we have a lot or not actually depends on how you count. (Last
time I checked the x86 still beat us, but that was a while back) In
absolute, unique op numbers we have more than pretty much any other
processor, but that is in part because we have *no* runtime op
For example, if you look you'll see we have 28 binary "add" ops.
.NET, on the other hand, only has one, and most hardware CPUs have a
few, two or three. However... for us each of those add ops has a very
specific, fixed, and invariant parameter list. The .NET version, on
the other hand, is specified to be fully general, and has to take the
two parameters off the stack and do whatever the right thing is,
regardless of whether they're platform ints, floats, objects, or a
mix of these. With most hardware CPUs you'll find that several bits
in each parameter are dedicated to identifying the type of the
parameter (int constant, register number, indirect offset from a
register). In both cases (.NET and hardware) the engine needs to
figure out *at runtime* what kind of parameters its been given.
Parrot, on the other hand, figures out *at compiletime*.
Now, for hardware this isn't a huge deal--it's a well-known problem,
they've a lot of transistors (and massive parallelism) to throw at
it, and it only takes a single pipeline stage to go from the raw to
the decoded form. .NET does essentially the same thing, decoding the
parameter types and getting specific, when it JITs the code. (And
runs pretty darned slowly when running without a JIT, though .NET was
designed to have a JIT always available)
Parrot doesn't have massive parallelism, nor are we counting on
having a JIT everywhere or in all circumstances. We could waste a
bunch of bits encoding type information in the parameters and figure
it all out at runtime, but... why bother? Since we *know* with
certainty at compile (or assemble) time what the parameter types are,
there's no reason to not take advantage of it. So we do.
It's also important to note that there's no less code involved (or,
for the hardware, complexity) doing it our way or the
decode-at-runtime way--all the code is still there in every case,
since we all have to do the same things (add a mix of ints, floats,
and objects, with a variety of ways of finding them) so there's no
real penalty to doing it our way. It actually simplifies the JIT some
(no need to puzzle out the parameter types), so in that we get a win
over other platforms since JIT expenses are paid by the user every
run, while our form of decoding's only paid when you compile.
Finally, there's the big "does it matter, and to whom?" question. As
someone actually writing parrot assembly, it looks like parrot only
has one "add" op--when emitting pasm or pir you use the "add"
mnemonic. That it gets qualified and assembles down to one variant or
another based on the (fixed at assemble time) parameters is just an
implementation detail. For those of us writing op bodies, it just
looks like we've got an engine with full signature-based dispatching
(which, really, we do--it's just a static variant), so rather than
having to have a big switch statement or chain of ifs at the
beginning of the add op we just write the specific variants
identified by function prototype and leave it to the engine to choose
the right variant.
Heck, we could, if we chose, switch over to a system with a single
add op with tagged parameter types and do runtime decoding without
changing the source for the ops at all--the op preprocessor could
glom them all together and autogenerate the big switch/if ladder at
the head of the function. (We're not going to, of course, but we
could. Heck, it might be worth doing if someone wanted to translate
parrot's interpreter engine to hardware, though it'd have bytecode
that wasn't compatible with the software engine)
As for what the rationale is... well, it's a combination of whim and
necessity for adding them, and brutal reality for deleting them.
Our ops fall into two basic categories. The first, like add, are just
basic operations that any engine has to perform. The second, like
time, are low-level library functions. (Where the object ops fall is
a matter of some opinion, though I'd put most of them in the "basic
For something like hardware, splitting standard library from the CPU
makes sense--often the library requires resources that the hardware
doesn't have handy. (I wouldn't, for example, want to contemplate
implementing time functions with cross-timezone and leap-second
calculations with a mass 'o transistors. The System/360 architecture
has a data-formatting instruction that I figure had to tie up a good
10-15% of the total CPU transistors when it was first introduced)
Hardware is also often bit-limited--opcodes need to fit in 8 or 9
For things like the JVM or .NET, opcodes are also bit-limited (though
there's much less of a real reason to do so) since they only allocate
a byte for their opcode number. Whether that's a good idea or not
depends on the assumptions underlying the design of their engines--a
lot of very good people at Sun and Microsoft were involved in the
design and I fully expect the engines met their design goals.
Parrot, on the other hand, *isn't* bit-limited, since our ops are 32
bits. (A more efficient design on RISC systems where byte-access is
expensive) That opens things up a bunch.
If you think about it, the core opcode functions and the core
low-level libraries are *always* available. Always. The library
functions also have a very fixed parameter list. Fixed parameter
list, guaranteed availability... looks like an opcode function to me.
So they are. We could make them library functions instead, but all
that'd mean would be that they'd be more expensive to call (our
sub/method call is a bit heavyweight) and that you'd have to do more
work to find and call the functions. Seemed silly.
Or, I suppose, you could think of it as if we had *no* opcodes at all
other than end and loadoplib. Heck, we've a loadable opcode
system--it'd not be too much of a stretch to consider all the opcode
functions other than those two as just functions with a fast-path
calling system. The fact that a while bunch of 'em are available when
you start up's just a convenience for you.
So, there ya go. We've either got two, a reasonable number the same
as pretty much everyone else, an insane number of them, or the
question itself is meaningless. Take your pick, they're all true. :)
--------------------------------------"it's like this"-------------------
Dan Sugalski even samurai
email@example.com have teddy bears and even
teddy bears get drunk