【问题标题】:Advantages of arrays of structures over parallel arrays in 6502 assembly language?6502汇编语言中结构数组相对于并行数组的优势?
【发布时间】:2017-08-14 04:49:04
【问题描述】:

我当时写了很多 6502,我的理解是并行数组比存储数据的结构更好。

想象一下,你想要一张怪物统计数据表,在 C 语言中会这样定义

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

您可以将其存储为结构数组

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

或者您可以将其存储为并行数组(通常使用宏或工具来生成)。注意:我在 C 中显示代码,但请想象它是 6502 程序集。

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, 
};

在 6502 中,给定一个 itemNdx,您可以使用这样的并行数组访问所有字段

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

如果你使用结构而不是并行数组,它就变成了

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

结构版本必须计算每个结构的偏移量。在上述情况下,与并行阵列版本相比,它多了 5 条指令。最重要的是,计算偏移量的数学是手工编码的,这意味着如果结构发生变化,则必须在任何时候重写它的大小。最重要的是,您只能拥有一张256 / sizeof(Monster) 大的表格。如果您有更多字段(20 到 30 个并不少见),这意味着您的表只能有 8 到 12 个条目,而与并行数组一样,您可以有 256 个条目。如果您想遍历表,还有一个优势。使用并行数组,您只需增加 x inx,一条指令。对于结构,您必须添加 sizeof(monster) ,其中添加仅适用于 a 将是

 txa
 clc
 adc #sizeof(Monster)
 tax

这比并行数组版本多 3 条指令。

看起来并行数组是 6502 汇编语言的客观胜利,但 John Carmack 来自his plan file 的这条模糊评论

...实际上,一直到理解 Apple II 汇编语言中结构相对于并行数组的优点... ...

有谁知道这些优点是什么?

我能想到的唯一优点是使用结构数组分配动态数组更容易,但大多数游戏在 6502 天内没有分配任何东西。他们硬编码修复了大小的内存数组,所以看起来不可能是这样。 6502 也没有缓存,所以没有缓存优势。

此外,如果您使用完整的指针,则您可以处理超过 256 个项目,但完整的指针比上面显示的任何一种方法要慢得多,并且需要 更多的代码因此,它们通常是最后的选择。

; 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

您可以通过创建一个指向每个项目的并行指针数组来摆脱乘法。您仍然有 2 行并行数组不需要的设置,并且您仍然会使其余代码变得更慢和更大。每次访问 8 个周期 vs 5 和 5 字节每次访问 vs 3。

基本上,您只会在绝对必要时才使用指针。如果您可以选择并行数组,那么似乎您应该始终选择它们。

【问题讨论】:

  • 问题是X和Y寄存器都是8位的,不能作为结构元素的索引。
  • 这个 Q 对我来说听起来太笼统了。还有更多选项可以将某些技巧硬编码到其中,如果您的任务允许的话,不幸的是我不熟悉 6502,并且 Z80+x86 差异太大,但有时我们会在内存中交错结构,所以每个元素都是在 +256 地址处(通过仅增加 ptr 的高 8 位来移动指针),或者元素具有固定大小,并且在处理过程中指针部分增加以越过每个字段,并且结束指向下一个元素的开头,或者固定大小元素到 2 的幂,所以乘法只是移位,等等......
  • 当我上次为 C64 实现光栅 IRQ 表时,我为下一个光栅线使用了一组结构化数据来设置 IRQ 和要跳转到的 ISR 有效负载。这更简单并且没有任何缺点,因为数据仅以线性顺序使用。也就是说,我想不出结构化数据的一般优势。这真的取决于用例。

标签: assembly 6502


【解决方案1】:

并行数组在使用绝对寻址的一组固定参数中工作得非常快。但是,当您超出此范围并不得不使用零页索引时,情况就变了。

;  Assuming MONSTER_PTR is zp, set to the start of the current structure
ldy  #Monster.hitPoints
lda  (MONSTER_PTR),y
...
ldy  #Monster.damage
lda  (MONSTER_PTR),y

对于超过单个页面限制的并行数组,必须为每个数组重置一个指针。此外,一旦使用指针,长索引计算可以用预先计算的指针的简单移动或对索引指针表的单次移位来代替。

鉴于优势(至少在他使用的灵活性方面),动态分配项目的能力是免费赠品。他的写作并不清楚,但这似乎是他的意思。

【讨论】:

  • 这仍然是净亏损。设置MONSTER_PTR 是几条指令,太糟糕了。 lda (xxx),ylda xxx,y 慢,所以更糟。而且,必须为每个字段设置ldy #offset 比根本不需要加载偏移量要慢,所以这更糟,所以我不了解这里的优势。另外,在只有 64k 内存的 6502 上,在一个表中需要超过 256 个结构的情况很少见。当然,如果您真的需要,那么您只需按照上述方式进行操作,但这并不是真正的优势,而是必要的。
  • 我首先提到了并行数组的优点,因为直接模式总是比索引更灵活(因为在 6502 中没有 (ptr+offset),idx 等价物。然而,当你开始不得不使用指针的那一刻,这个优势就失去了,结构变得更有效率,因为给定的原因.想想你必须做什么才能在并行数组上使用指针 - 我认为,这就是他得到的在。
  • 当然可以,但是当我们讨论优点与缺点时,讨论应该可以说是“使用最有效的方法,直到你不能使用”,这基本上意味着“喜欢并行数组,直到你不能使用他们”。否则,如果建议是“在并行数组上使用指针和结构数组”,那么 20 次中有 19 次您编写的代码更多而效率更低。
  • 那个,我完全同意。
【解决方案2】:

唯一的一点是,并行数组比结构访问更好(这需要在每次访问时计算当前怪物的地址),如果你有指向零页中每个数组的现成指针:

; set up zero page, done only once per level
; (per level, per stage, per dungeon, whatever)

lda #<monster_HP_list_for_this_level
sta $f0
lda #>monster_HP_list_for_this_level
sta $f1

lda #<monster_damage_list_for_this_level
sta $f2
lda #>monster_damage_list_for_this_level
sta $f3

lda #<monster_shield_list_for_this_level
sta $f4
lda #>monster_shield_list_for_this_level
sta $f5

...

; here you'd have instant access to the monster's values :
lda ($F0),y   ; monster Nr. y's HP
; or 
lda ($F4),y   ; monster Nr. y's Shield level

但是: 你可能只有 256 个怪物(还有很多免费的零页地址,这应该不是什么大问题)

在每一种新情况下(例如,您到达新地图、进入新地牢等),零页指针可能会更新,以指向新关卡的怪物

【讨论】:

  • lda (zeropagedamgelist),yload nonzeropagedamagelist,x 好多少?后者是5个周期,前者是6个,后者不需要设置,前者需要。我想我不明白你的回答。也许您假设数据在运行时加载到任意位置?但这不是人们编写大多数 6502 应用程序的方式。
  • 你可以为你到达的每个“级别”,你进入的每个新地牢等加载新的零页指针,但这是一个好点,我会相应地改变我的答案
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-18
  • 1970-01-01
相关资源
最近更新 更多