4 ms·
As a longtime 6502 programmer I was impressed by the Z80 after reading the data sheet - but actually trying to program it left me nonplussed. The main omissions
by to3m 11y ago
As a longtime 6502 programmer I was impressed by the Z80 after reading the data sheet - but actually trying to program it left me nonplussed. The main omissions as I recall were: lack of immediate addressing for the 16-bit arithmetic instructions; immediate instructions generally are more expensive; (IX+n)/(IY+n) addressing modes are heinously slow; no indirect-with-register-offset addressing mode. These all seemed to eat away at the Z80's apparent advantages.
As a somewhat representative example - suppose you have a bunch of objects that you want to work on. On the Z80 you'd probably adopt a modern-style struct system, whereby each object is represented by a block of memory. Offset 0 is X coordinate, offset 1 is Y coordinate, offset 2 is flags, blah de blah. Suppose each object is 8 bytes, and you want to set a flag for each object that's on screen.
LD DE,(MAX_X<<8)|MIN_X ; 10 10
LD IY,8 ; 14 24
LD IX,OBJECT0 ; 14 38
LD B,NUMOBJECTS ; 7 45
.LOOP
LD A,(IX+0) ; 19 19
CP D ; 4 23
JP C,NOTVISIBLE ; 10 33
CP E ; 4 37
JP NC,NOTVISIBLE ; 10 47
.VISIBLE
LD A,(IX+2) ; 19 19
OR VISFLAG ; 7 26
LD (IX+2),A ; 19 45
ADD IX,IY ; 15 60
DJNZ LOOP ; 13 73
JP DONE ; 10
.NOTVISIBLE
LD A,(IX+2) ; 19 19
AND ~VISFLAG ; 7 26
LD (IX+2),A ; 19 45
ADD IX,IY ; 15 60
DJNZ LOOP ; 13 73
.DONE
(For readers born after 1985 :) - numbers in the comments are cycle counts: instruction's count, then cumulative total for this block)
So, for N objects: (45 + N * 120). Plus 10 if the last one was visible.
For the 6502 you'd probably adopt a striped layout, so rather than having each object as a block of data, you'd have an X table, a Y table, a flags table, and so on. So the same again:
LDX NUMOBJECTS ; 3 3
.LOOP
LDA XS-1,X ; 4 4
CMP MINX ; 3 7
BCC NOTVISIBLE ; 3 10
CMP MAXX ; 3 13
BCS NOTVISIBLE ; 3 16
.VISIBLE
LDA FLAGS-1,X ; 4 4
ORA #VISFLAG ; 2 6
STA FLAGS-1,X ; 5 11
DEX ; 2 13
BNE LOOP ; 3 16
BEQ DONE ; 3
.NOTVISIBLE
LDA FLAGS-1,X ; 4 4
AND #~VISFLAG ; 2 6
STA FLAGS-1,X ; 5 11
DEX ; 2 13
BNE LOOP ; 3 16
.DONE
So, for N objects: (3 + N * 32). Plus 3 if the last one was visible. (I've been a bit scrappy with the cycle counts here, by counting each branch as taken, even though that makes the totals invalid. This is done to favour the Z80, which doesn't appear to execute untaken branches any quicker.)
That's pretty much the 4:1 improvement that's commonly claimed. 1MHz 6502 will beat 3.5MHz Z80; 2MHz 6502 will beat 4MHz Z80.
This might seem like a synthetic benchmark, designed to make the 6502 look better, but this sort of thing crops up fairly often. (I noticed it initially after noting how crappy-looking the code was for a couple of Z80 games I was disassembling; I only realised why after trying to rewrite the snippets in question myself!)
One thing to note in particular here is that in the Z80 case you're two registers down - because IY has been used for the array stride (no immediate 16-bit addition...), and DE has been used for the min/max constants (immediate instructions are more expensive).
And this is all very well as the code is presented here, but in practice, in many cases you'll probably need to use DE in your code. You can work around this by using EXX and having your constants in the shadow register bank, but you're losing 8 T-states per iteration (EXX = 4 T-states). For many purposes you'd be better off using self-modifying code - but now you lose 3 T-states per addition (immediate instructions are more expensive).
So a bit of a disappointment for me. Initial excitement at seeing just how much crazy stuff the Z80 can do rapidly turned into disillusionment as I realised just how long it takes to do anything. It's like the 68000 in this respect.
Good luck beating LDIR with a 6502 at quarter the clock rate, though...
Z80 code is also generally much easier to follow...
Z80 has a 16-bit stack pointer too...
- rdc12 11y agoWhy would you favour a AOS (Array of Structures) or the SOA (Structure of Arrays), for the different architectures?
- to3m 11y agoThe basic operation for accessing a value from a structure is to take the address of the structure, add a compile-time constant offset to it, and use that address. There's an addressing mode specifically for this in the Z80: you put the struct address in a register, and have the constant offset as part of the instruction. So you can do stuff like this: (numbers in comments are: <bytes for this instruction> <bytes total> / <cycles for this instruction> <cycles total>) <<put address in IX>> LD A,(IX+0) ; A=SELF.X 3 3 / 19 19 ADD A,(IX+2) ; A=SELF.X+SELF.DX 3 6 / 19 38 LD (IX+0),A ; SELF.X=SELF.X+SELF.DX 3 9 / 19 57 XOR A ; A=0 1 10 / 4 61 LD (IX+2),A ; SELF.DX=0 3 13 / 19 80 The 6502 doesn't have anything exactly like this. You do have an indirect addressing mode, but the offset goes in a register. So for every access, you need to have the offset into the index register. (There's no point adding the offset to the address; the offset is mandatory, so you'd then just have to load zero into the index register every time.) So the above in 6502: <<put address in $70>> LDY #2 2 2 / 2 2 LDA ($70),Y ; A=SELF.DX 2 4 / 5 7 LDY #0 2 6 / 2 9 CLC 1 7 / 2 11 ADC ($70),Y ; A=SELF.DX+SELF.X 2 9 / 5 16 STA ($70),Y ; SELF.X=SELF.X+SELF.DX 2 11 / 6 22 TYA 1 12 / 2 24 LDY #2 2 14 / 2 26 STA ($70),Y ; SELF.DX=0 2 16 / 6 32 Which is rather unwieldy, takes up more space, and is a bit slow (the 4:1 ratio I mentioned in my original past is here closer to 2:1) - even after I rearranged the code a bit so the Y value didn't need reloading so much. You also have the issue that the struct address is a 16-bit quantity, something that is inconvenient to handle on the 6502. There are no instructions that operate on 16-bit data. One thing the 6502 does have though is an addressing mode where the operand's address is a fixed 16-bit value (from the instruction) plus an 8-bit index register. So, with this in mind, what you could do is have a separate table for each struct field. So this switches things around, and instead of a runtime 16-bit address (of the struct) and a compile-time 8-bit offset (of the field), you have a compile-time 16-bit address (of the field table) and a runtime 8-bit offset (for the row in the table). Which is a perfect fit. Best of all, the indexing is free if you arrange things correctly, which is not that hard to do. Then the code would look more like this: <<put row in X>> CLC ; 1 1 / 2 2 LDA XS,X ; A = SELF.X 3 4 / 4 6 ADC DXS,X ; A = SELF.X+SELF.DX 3 7 / 4 10 STA XS,X ; SELF.X=SELF.X+SELF.DX 3 10 / 4 14 LDA #0 ; 2 12 / 2 16 STA DXS,X ; SELF.DX=0 3 15 / 4 20 And there's that 4:1 ratio again... Each object's "address" is now an 8-bit quantity (it's just an index into the tables), so much easier to deal with. And moving from one object to the next is very quick, since it's just incrementing a register. There doesn't appear to be anything analogous to this alternative approach on the Z80.