Advantages of arrays of structures over parallel arrays in 6502 assembly language?

Viewed 2535

My understanding from writing a lot of 6502 back in the day is that parallel arrays are better than structures for storing data.

Imagine you want to have a table of monster stats that in C would be defined something like this

struct Monster {
  unsigned char hitPoints;
  unsigned char damage;
  unsigned char shieldLevel;
  char* name;
};

You could store it as an array of structures

static Monster s_monsters[] = {
  { 5,   1, 0, "orc", },
  { 50, 10, 5, "dragon", },
  { 10,  3, 1, "goblin", },
};

Or you could store it as parallel arrays (usually using macros or tools to generate). Note: I'm showing the code in C but please imagine it's 6502 assembly.

unsigned char Monster_hitPoints[] = { 5, 50, 10, };
unsigned char Monster_damage[] = { 1, 10, 3, },
unsigned char Monster_sheildLevel[] = { 0, 5, 1, };
unsigned char Monster_nameLow[] = { 
   &m_orc_name & 0xFF, 
   &m_dragon_name & 0xFF,
   &m_goblin_name & 0xFF, 
};
unsigned char Monster_nameHigh[] = { 
   &m_orc_name >> 8 , 
   &m_dragon_name >> 8,
   &m_goblin_name >> 8, 
};

In 6502, given an itemNdx you can access all the fields with parallel arrays like this

ldx itemNdx
lda Monster_hitPoints,x   ; access hitpoints
...
lda Monster_damage,x      ; access damage
...
lda Monster_shieldLevel,x ; access shieldLevel
...
lda Monster_nameLow,x     ; access name
sta pageZeroStringPointer
lda Monster_nameHigh,x
sta pageZeroStringPointer + 1
ldy #0
lda (pageZeroStringPointer),y

Where as if you use a struct instead of parallel arrays it becomes

lda itemNdx
clc          ; have to compute offset
asl a        ; a = itemNdx * 2   
asl a        ; a = itemNdx * 4
adc itemNdx  ; a = itemNdx * 5
tax          ; x = itemNdx * 5 

lda s_monsters+Monster.hitPoints,x   ; access hitpoints
...
lda s_monsters+Monster.damage,x      ; access damage
...
lda s_monsters+Monster.shieldLevel,x ; access shieldLevel
...
lda s_monsters+Monster.name,x        ; access name
sta pageZeroStringPointer
lda s_monsters+Monster.name+1,x
sta pageZeroStringPointer + 1
ldy #0
lda (pageZeroStringPointer),y        ; a is now first char of name

The structure version has to compute the offset to each structure. In the case above that's 5 more instructions vs the parallel array version. On top of which the math for computing the offset is hand coded, meaning it has to be re-written anytime the size if a structure changes. On top of that you can only have a table of 256 / sizeof(Monster) large. If you have more fields (20 to 30 is not uncommon), that would mean your tables could only have 8 to 12 entries where as with parallel arrays you can have 256 entries. One more advantage if if you want to iterate over the table. With parallel arrays you just increment x inx, one instruction. With structures you'd have to add sizeof(monster) which add only works with a would be

 txa
 clc
 adc #sizeof(Monster)
 tax

That's 3 more instructions than the parallel arrays version.

That seems like parallel arrays are an objective win for 6502 assembly language but there's this obscure comment by John Carmack from his plan file

... actually, all the way back to understanding the virtues of structures over parallel arrays in apple II assembly language.. ...

Does anyone know what those advantages are?

The only advantages I can think of off the top of my head is that it's easier to allocate a dynamic array with arrays of structs but most games didn't allocate anything back in 6502 days. They hardcoded fixes sized arrays of memory so that doesn't seem like that can be it. 6502 didn't have a cache either so there's no cache advantage.

Also you can address more than 256 items if you go full on pointers but full on pointers are much slower and require much more code than either of the methods shown above therefore they are usually options of last resort.

; setup pointer
lda itemNdx
ldx #sizeof(Monster)
jsr multiplyAX       ; 8->16 bit multiply is around 70 cycles result in A(low), X(high)
clc
adc #s_monster && 0xFF
sta POINTER
txa
adc #s_monster >> 8
sta POINTER + 1

ldy #Monster.hitPoints   ; access hitpoints
lda (POINTER),y   
...
ldy #Monster.damage      ; access damage
lda (POINTER),y 
...
ldy #Monster.shieldLevel ; access shieldLevel
lda (POINTER),y 
...
ldy #Monster.name       ; access name
lda (POINTER),y
sta pageZeroStringPointer
ldy #Monster.name+1    
lda (POINTER),y
sta pageZeroStringPointer + 1
ldy #0
lda (pageZeroStringPointer),y        ; a is now first char of name

You could get rid of the multiply by making a parallel array of pointers to each item. You'd still have 2 lines of setup that are not needed for parallel arrays and you'd still be making the rest of the code slower and larger. 8 cycles per access vs 5 and 5 bytes per access vs 3.

Basically you would only use pointers if you absolutely have to. If you can choose parallel arrays then it seems like you should always choose them.

2 Answers
Related