June 18, 2005

WWIT: Calling conventions

If you've looked you might have noticed that Parrot's calling conventions are somewhat... heavyweight. Not particularly bad as these things go (they're actually very similar to the conventions you see on systems with lots of registers such as the Alpha or PPC) but still, heavier than folks used to only stack-based systems are used to.

As a recap for those not intimately familiar with parrot's calling conventions (as they stood a while ago at least -- things may have changed) the first eleven of each type of argument (PMC, String, Integer, and Float) go into registers 5-15 of the appropriate register type. The count of parameters in each register type go into integer registers 1-4, Int register 0 gets a true/false value noting whether this is a prototyped call or not (meaning that non-PMC parameters are being passed in basically), P0 gets the sub PMC being invoked put in it, P1 holds the return continuation (this can be filled in automatically for some invocation ops), P2 holds the object the method's being invoked on (if this is a method call), P3 holds an array with any extra PMC parameters if there are more than 11, and S0 holds the name of the sub you're calling (since subs may have multiple names)

Seems complex, doesn't it?

Let's think for a moment before we go any further. When calling a function, what do you need to have? Of course, you need the parameters. You need to have a place to return to. There has to be some indication of how many parameters you're passing in. (At least with perl-like languages, where the parameter list is generally variable-length) You need some handle on the thing you're calling into. Per introspection requirements perl imposes, you need to know the name of the function you're calling, since a function may have several names you need to know which name you're using when making the call, and if it's a method call you need the name of the method you're calling so you can look it up. If you're calling a method on an object you need the object. (And you thought this was going to be simple...)

The only required elements for a sub call are the count of PMC parameters, the prototyped indicator (which you would, in this case, set to unprototyped), the sub PMC, and the sub name. The parameters themselves aren't required since you don't actually have to have any. The return continuation can be autogenerated for you if you so choose, so it's not on the list.

So. Sub name, Sub PMC, prototype indicator, and parameter count. Not exactly onerous, and unfortunately required. No way around that. The biggest expense you're going to have is shuffling some pointers and constants around. (And, while I admit I resent burning time, it's hard to get too worked up about four platform natural sized integer moves per sub call, one of which, the sub PMC, can potentially be skipped if you fetch it out of the global store into the right spot in the first place)

The extras are just that -- extras. If you choose to do a prototyped call you need to fill in the counts for the other arg types. If you choose to not take advantage of automatic return continuation creation you need to create one and stick it in the right spot. If you've got way too many parameters, you need to put them into the overflow array. That's it, though.

The first thing anyone does when they look at this is want to start chopping things out. The problem is that there's really nothing to cut out. You can't chop out the object for method calls, that's kinda needed. You can't chop out the PMC for the sub being called, since you need a place to go. You can't skip using PMCs for subs for a number of reasons, which warrant their own topic, so I'll put that in a separate WWIT entry. You can skip the parameter count if you have functions with fixed parameter signatures (which we don't) or if you use a container that keeps count for you, which just pushes the cost off somewhere else (and ultimately makes calling more expensive, since you then need to move parameters out of the container and into registers). You could skip the whole prototyped thing, but in that case you either always use parameter counts or lose the ability to have non-PMC parameters. You can't chop the sub name out, since then you can't properly introspect up the stack to find the caller names (as any particular sub PMC could have multiple names) You can't chop out the return continuation since you need a place to return to when you're done. You can't chop out... well, we've run out of things to consider chopping out, and the best we've managed is to potentially change how the actual parameters are passed, but that doesn't make things cheaper or easier, it just shifts the cost and adds a little extra overhead.

Aren't engineering trade-offs fun?

Oh, and you can't even count on the sub you're calling being singly or multiply dispatched, so you have to leave the dispatching entirely up to the sub/method PMC being invoked. The HLL compilers can't emit code that assumes one or the other dispatching method. ('Specially since the method may change from invocation to invocation of a subroutine, as code elsewhere screws around with the definition of a sub)

Posted by Dan at June 18, 2005 09:42 PM | TrackBack (0)

Incidentally, this is in the process of being changed, because Chip and Leo think the old conventions are too hard to get working with continuations. The new system has a bank of argument registers and a bank of return registers; normal registers are assigned into these special ones through an opcode and retrieved back into normal registers through another op (or maybe metadata on the sub, it's not quite clear yet). The system supports transparent flattening/folding and argument type translation, neatly dancing around the prototyped/unprototyped distinction.

But I'm not sure you care anymore, so...

Posted by: Brent Dax at June 18, 2005 11:24 PM
There has to be some indication of how many parameters you're passing in. (At least with perl-like languages, where the parameter list is generally variable-length)

This reminded me of one of my favourite lesser known languages Pop-11. It had an open stack - anybody could push/pop things off the stack at any point. Argument parsing for subroutines was just syntactic sugar over push/pop on the global stack.

However, if you wanted, you could push more values than the subroutine needed, or pop less than a subroutine output (stack based VM if you hadn't already guessed :-)

This led to some interesting (and occasionally even useful) programming styles (e.g. using the stack rather than intermediate data structures when doing a chain of operations on a number of objects.)

Posted by: Adrian Howard at June 19, 2005 09:18 AM

I knew they were going to change things in the calling conventions -- that was one of the last straws that lead me to bail completely on parrot. I can't really think of any reason that continuations and the calling conventions would clash, since they didn't when I designed everything together and I honestly still can't see why they would. I could find out what they're thinking (they've not, so far, asked what I was thinking) but, as you mentioned, I don't much care any more.

Guess this is a "whatever" moment. Not my software, not my problem.

Posted by: Dan at June 19, 2005 09:39 AM

The are a lot simpler...just a couple of opcodes.
Anyway autrijus was strongly supporting them (presumably makes better encapsulaton and easier register allocation).

Posted by: Pawel M. at July 12, 2005 03:49 AM