以您熟悉的语言来考虑您的设计。最好是 C,因为 C 中的指针和值是直接映射到 asm 的概念。
假设您想通过存储指向头元素的指针来跟踪您的链表。
#include <stdint.h> // for int8_t
struct node {
int8_t next; // array index. More commonly, you'd use struct node *next;
// negative values for .next are a sentinel, like a NULL pointer, marking the end of the list
int8_t val;
};
struct node storage[5]; // .next field indexes into this array
uint8_t free_position = 0; // when you need a new node, take index = free_position++;
int8_t head = -1; // start with an empty list
有一些技巧可以减少极端情况,例如让列表头成为一个完整节点,而不仅仅是一个引用(指针或索引)。您可以将其视为第一个元素,而不必到处检查空列表情况。
无论如何,给定一个节点引用 int8_t p(其中 p 是指向列表节点的指针的标准变量名,在链表代码中),下一个节点是 storage[p.next]。下一个节点的val 是storage[p.next].val。
让我们看看这在 asm 中是什么样子的。 NASM manual 讨论了它的宏系统如何帮助您使用全局结构使代码更具可读性,但我还没有为此做任何宏的工作。你可以为NEXT 和VAL 或其他东西定义宏,用0 和1,所以你可以说[storage + rdx*2 + NEXT]。甚至是一个带有参数的宏,所以你可以说[NEXT(rdx*2)]。但是,如果您不小心,您可能会得到更多难以阅读的代码。
section .bss
storage: resw 5 ;; reserve 5 words of zero-initialized space
free_position: db 0 ;; uint8_t free_position = 0;
section .data
head: db -1 ;; int8_t head = -1;
section .text
; p is stored in rdx. It's an integer index into storage
; We'll access storage directly, without loading it into a register.
; (normally you'd have it in a reg, since it would be space you got from malloc/realloc)
; lea rsi, [rel storage] ;; If you want RIP-relative addressing.
;; There is no [RIP+offset + scale*index] addressing mode, because global arrays are for tiny / toy programs.
test edx, edx
js .err_empty_list ;; check for p=empty list (sign-bit means negative)
movsx eax, byte [storage + 2*rdx] ;; load p.next into eax, with sign-extension
test eax, eax
js .err_empty_list ;; check that there is a next element
movsx eax, byte [storage + 2*rax + 1] ;; load storage[p.next].val, sign extended into eax
;; The final +1 in the effective address is because the val byte is 2nd.
;; you could have used a 3rd register if you wanted to keep p.next around for future use
ret ;; or not, if this is just the middle of some larger function
.err_empty_list: ; .symbol is a local symbol, doesn't have to be unique for the whole file
ud2 ; TODO: report an error instead of running an invalid insns
请注意,我们通过将符号扩展到 32 位 reg 而不是完整的 64 位 rax 来避免更短的指令编码。如果该值为负数,我们将不会使用rax 作为地址的一部分。我们只是使用movsx 将寄存器的其余部分归零,因为mov al, [storage + 2*rdx] 会将rax 的高56 位保留为旧内容。
另一种方法是使用movzx eax, byte [...] / test al, al,因为 8 位 test 的编码和执行速度与 32 位 test 指令一样快。此外,在 AMD Bulldozer 系列 CPU 上,movzx 作为负载的延迟比movsx 低一个周期(尽管它们仍然采用整数执行单元,不像英特尔,movsx/zx 完全由负载端口处理)。
无论哪种方式,movsx 或 movzx 都是加载 8 位数据的好方法,因为您可以避免在写入部分 reg 后读取完整 reg 和/或错误依赖(在前面reg 的高位内容,即使 你 知道你已经将它归零,CPU 硬件仍然必须跟踪它)。除非您知道您没有针对 Intel pre-Haswell 进行优化,否则您不必担心部分寄存器写入。 Haswell 进行双重簿记或其他操作,以避免在阅读时将部分值与旧的完整值合并。 AMD CPU、P4 和 Silvermont 不会将部分注册与完整注册分开跟踪,因此您只需要担心错误的依赖关系。
还请注意,您可以将next 和val 一起加载,就像
.search_loop:
movzx eax, word [storage + rdx*2] ; next in al, val in ah
test ah, ah
jz .found_a_zero_val
movzx edx, al ; use .next for the next iteration
test al, al
jns .search_loop
;; if we get here, we didn't find a zero val
ret
.found_a_zero_val:
;; do something with the element referred to by `rdx`
请注意无论如何我们必须使用movzx,因为有效地址中的所有寄存器必须具有相同的大小。 (所以word [storage + al*2] 不起作用。)
这可能更有用,在将next 转换为al 并将val 转换为ah 之后,将节点的两个字段存储在一个单独的存储中,例如mov [storage + rdx*2], ax 或其他东西,可能来自不同的来源。 (在这种情况下,如果您还没有将它放在另一个寄存器中,您可能希望使用常规字节加载,而不是 movzx)。这没什么大不了的:不要为了避免进行两个字节存储而使您的代码难以阅读或更复杂。至少,直到您发现存储端口 uops 是某个循环中的瓶颈。
使用数组的索引而不是指针可以节省大量空间,尤其是。在指针占用 8 个字节的 64 位系统上。如果您不需要释放单个节点(即数据结构只会增长,或者在删除时立即全部删除),那么新节点的分配器是微不足道的:只需将它们粘贴在数组的末尾,和realloc(3)。或者使用 c++ std::vector。
有了这些构建块,您应该已经准备好实现通常的链表算法了。只需使用 mov [storage + rdx*2], al 或其他方式存储字节即可。
如果您需要有关如何使用干净算法实现链表的想法,以处理尽可能少的分支的所有特殊情况,请查看this Codereview question。它适用于 Java,但我的回答非常 C 风格。其他答案也有一些不错的技巧,其中一些是我借来的。 (例如,使用虚拟节点避免分支来处理插入作为新头的特殊情况)。