6

Programming languages like C, where any function has the potential to be called recursively, are effectively designed on the assumption that it's okay for all local variables to go on the stack.

On the 6502, this is not a good assumption. The stack is of limited size, it's awkward to access things on it, and you miss out on zero page addressing; if you really put everything on the stack, your code will be a lot slower, apart from the risk of running out of stack space.

To what extent is this also true on the Z80? It doesn't have zero page addressing, and does make it easier to access stuff on the stack using an index register, but I think variables at absolute memory addresses are still more efficient to access?

12

TL;DR;

How costly is it to put things on the stack with the Z80?

Very costly as there are no neat instructions to access stack at all, only 8 bit instructions to access indexed memory and only accessing the stack pointer allone is a already mess.

When going into details the 6502 almost looks good in comparsion. For both a seperate parameter stack may be a better solution.


The Long Read

(Caveat: I did look up the timing for the instructions here, but I didn't look up the deatails, so there might be cases and side effects I missed out)

I assume your question is about accessing data on the stack, not push or pop operations (*1). Looking at the basic load instruction and its timing should give a good overview how indexed memory access can be done:

Loading from memory

LD A,(nn)   13
LD HL,(nn)  16
LD rr,(nn)  20
LD r,(rr)   07
LD r,(ix+n) 19

Storing to memory

LD (nn),A     13
LD (nn),HL    16
LD (nn),rr    20
LD (rr),r     07
LD (ix+n),r   19

Several issues can be noted:

  • There are no instruction to load or store 16 bit values (pointers) from an address pointed to by a register pair
  • There is no way to load an index register from an indexed location, it needs to be loaded bytewise.
  • Using the index registers is extremely slow (*2)
  • Indexed access by register pairs don't allow the use of an offset.

Additional 'quirks' in the instruction set are degrading the use of index registers in general, which influences the use to access stack based data a lot:

  • There is no way to just move SP to any other register
  • Accessing SP can only be done by adding it to HL or IX/IY.
  • There is no way to copy register pairs then pushing/poping them or use two 8 bit register moves
  • There is no way to combine HL and IX or IY in one instruction (*3)
  • There is no way to include IX and IY in one instruction (*3)
  • There is no way to load IXH (IYH) addressed by IX (IY) (*3)
  • There is no way to load H or L from IXH/IXL or vice versa. (*3)

It's further important to keep the stack structure in mind:

  • The only instruction directly accessing the stack are PUSH and POP
  • The stack is 16 bit 'wide'
  • Only register pairs or index registers can be pushed/poped (*1)

So loading a 16 bit value (pointer) from stack into DE (*4), for further addressing, requires:

  • Clearing one of the index registers (4B/14C)
  • Adding SP to it (2B/15C)
  • Loading D and E with bytewise loads (3B/19C each)

That adds up to 6B/19C setup and 6B/38C for each pointer thereafter.

Loading DE by using HL requires.

  • Moving the offest into HL (3B/10C)
  • Adding SP to HL (1B/11C)
  • Loading the first byte (1B/7C),
  • Incrementing HL (1B/6C)
  • Loading the second (1B/7C)

Now we need 4B/21C for setup and 3B/20C to load the pointer.

Not using the index registers is already on the first word faster (-16C) and more compact (-4B) than doing so. And it gets worse when more parameters need to be accessed.

As usual, YMMV depending on the tast, just not much, as the underlaying problem is the way the index registers have been implemented. A great idea at first sight, but (more often than not) degrading in real world applications.


Interesting sidenote:

The undocumented 8085 instructions would have been way more useful here. Most notably LDSI which in just 10 clocks loads DE with SP and adds an 8 bit constant.


*1 - Push register pair is 10 clocks, poping it is 11, while the same operation for index registers takes an additional 4 clocks (to read the prefix)

*2 - They all have at least one prefix byte accessed in 4 cycles, plus a displaccement byte to be read (if needed), adding 4 more.

*3 - IX and IY related opcodes are implemented by overriding HL in an instruction via a prefix byte into IX or IY. Thus there are no opcodes that can include both

*4 - It has to be a register pair, as none of the index registers can be loaded using the other - and using itself can't work due the bytewise nature.

  • I think you should add push pop timings for comparison too ... push bc/de/hl/af 11T and push ix/iy 15T and pop bc/de/hl/af 10T and pop ix/iy 14T – Spektre Nov 5 '18 at 11:08
  • @Spektre I left them out to keep it clear, as they where not part of the question,. Let me add a footnote. – Raffzahn Nov 5 '18 at 13:40
10

Putting automatic objects on the stack is a horribly inefficient way of handling them on the 8080 and Z80 unless it is necessary to support recursion or reentrancy. Copying the stack pointer to IX on function entry and then using IX to access automatic objects is somewhat reasonable from a code-size perspective, but the Z80's 4-bit ALU makes it very slow to process address arithmetic.

If a function does not need to support reentrancy, performance can generally be massively improved by having the function start by popping the return address and parameters and storing them into registers or statically-assigned addresses. If one can use the Pascal calling convention (called function cleans up the stack), simply leave the arguments off the stack. Otherwise, either save the stack pointer someplace on and restore it after the function returns, or else push some dummy values on the stack. Either approach will be wasteful compared to using the Pascal convention, considering that the caller will then have to spend effort removing the junk the called function just put back onto the stack.

If a function takes no arguments, it can simply use a RET to return, for a cost of 10 cycles. The function prologues and epilogues necessary for functions that use arguments are much more expensive, especially if arguments need to be left on the stack when a function returns. If one can use the Pascal calling convention where a called function cleans up arguments, then a prologue/epilogue for a function that takes e.g. two words of arguments could be:

pop HL ; Return address
pop DE ; Arg #1
ld  (arg1),DE
ex  (sp),HL   ; Fetch arg#2 and put return address back on stack
ld  (arg2),HL
...
ret

So in addition to the 10 for the ret, add 20 for the two pops, 19 for the ex, and 32 for the two LD, so an extra 71 cycles plus an extra 26 cycles for each additional argument. That's pretty nasty, but cheap compared with the approach used for code that must support re-entrancy. If local variables are required:

push ix
ld   ix,numBytes
add  ix,sp
ld   sp,ix   ; If function needs any additional local variables
...
pop  ix
ret

That's 29 for the push/pop, 14 and 8 for the ld, and 15 for the add (even if numBytes would be zero, there's no way to copy sp to ix in less than 29 cycles). So 66 plus the ret. If no local variables are required, one can omit the second load, reducing the cost to 58, but the six-byte 29-cycle sequence "ld ix,nn / add ix,sp" is required even when nn is zero.

While it may seem like an extra 58 cycles for prologue/epilogue would be cheaper than the Pascal approach, trying to actually work with arguments on the stack is much more expensive than using statically-allocated objects. For example, if "x" and "y" are 16-bit values, "x=y;" would be 6 bytes and 32 cycles if x and y are statically allocated (ld hl,(y) / ld (x),hl. If x and y are on the stack accessed via ix, however, then ld l,(ix+y) / ld h,(ix+y+1) / ld (ix+x),l, / ld (ix+x+1),l will cost 12 bytes and 76 cycles. If there are a lot of arguments but a function never uses them, the prologue/epilogue that sets up ix may be cheaper than popping arguments and copying them to statically-allocated space, but if a function uses its arguments even once, any savings in the prologue/epilogue will be wiped out by the extra cost of accessing things through IX.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.