我知道 8086 Assembly,现在我正在通过阅读MIPS Assembly Language Programming和See MIPS Run 这两本书来学习 MIPS Assembly ,但我从未停下来思考汇编的编码标准/最佳实践。我每天都想把自己变成一个更好的开发人员,然后想知道这一点来提高自己。如何了解有关汇编编码标准和最佳实践的更多信息?
2 回答
最佳实践是一种社会现象,取决于您将工作的社会,因此您最好的答案是从您希望与之交互的任何环境中读取现有的 MIPS asm 代码。
从我自己的世界想到的例子是 Linux 内核的汇编器部分、来自 GCC 的 MIPS 启动代码或 glibc 的 MIPS 端口的汇编器片段。
如果您主要与其他项目进行交互,最好吸收和模仿该社区的编码实践。
良好的 asm 风格在 ISA 中非常普遍(以及同一 CPU 的不同 asm 方言)。编译器输出(如 gcc/clang)通常会完成我在下面提到的所有事情,因此是一个很好的指南。(而 C 编译器的输出通常是优化小函数的良好起点。)
通常将指令缩进比标签和汇编指令更深一层。
将操作数缩进到一致的列(因此不同长度的助记符不会让您的代码参差不齐,并且很容易向下扫描一个块并将每条指令的目标寄存器视为第一个操作数)1。
将指令行上的注释缩进到右侧一致的列中,远远超过操作数以避免视觉噪音。
将相关指令块组合在一起,用空行分隔它们。(或者,如果您正在通过调度指令来优化有序 CPU,那么您实际上无法做到这一点,并且需要使用注释来跟踪每条指令正在处理的问题的哪一部分。使用不同级别的缩进那么评论可能会有所帮助)
脚注 1:
除了 MIPS 存储指令,例如sw $t0, 1234($t1)
第一个操作数实际上是源;他们选择让 asm 源对加载和存储使用相同的操作数顺序,可能是因为它们都是机器代码中的 I 型指令。不过,这是 RISC 加载/存储架构的典型 asm,因此习惯于来自mov eax, [rdi]
一个加载和mov [rdi], eax
存储的 CISC。add [rdi], eax
两者兼而有之。
示例:无符号整数的atoi
函数,用于具有分支延迟槽的真实 MIPS。但不是 MIPS I,没有加载延迟槽。尽管无论如何我都试图避免负载使用停顿。(C 版本的 Godbolt)
# unsigned decimal ASCII string to integer
# inputs: char* in $a0 - ASCII string that ends with a non-digit character
# outputs: integer in $v0
# clobbers: $t0, $t1
atoi:
# peel the first iteration to avoid a 0 * 10 multiply
lbu $v0, 0($a0)
addiu $v0, $v0, -'0' # digit = *p - '0'
sltu $t0, $v0, 10
bnez $t0, .Lloop_entry # if unsigned (! digit<10)
nop # doing work for the next iteration here hurts ILP for in-order CPUs
#addu $t2, $v0, $v0 # total * 2 (branch delay slot)
# invalid non-digit input
jr $ra # return 0
move $v0, $zero
.Lloop: # do {
addu $v0, $v0, $v0 # total *= 2
addu $t0, $t0, $t1 # total*8 + digit
addu $v0, $v0, $t0 # total*10 + digit = total*2 + (total*8 + digit)
.Lloop_entry:
lbu $t0, 1($a0)
addui $a0, $a0, 1 # t0 = *(p++ + 1)
addiu $t0, $t0, -'0' # t0 = digit
sltu $t1, $t0, 10
bnez $t1, .Lloop # while(digit<10);
sll $t1, $v0, 3
jr $ra
nop
这对于任何特定的 MIPS 实现可能都不是最佳的;一个有序的超标量可能会受益于在负载和分支之间放置更多的移位/添加,即使这意味着在最后一次迭代中完成了更多的冗余工作。对于像 r10k 这样的 OoO 执行人员来说,这可能是件好事。现代 MIPS32r6 将用于lsa
左移累加,就像 gcc 对 所做的那样-march=mips32r6
,并将使用分支指令的无分支延迟版本。
不过,这在早期的标量 MIPS 上可能相当不错。指针增量在加载后填充槽,避免循环内的停顿。(立即偏移 1 是因为我们避免了剥离的第一次迭代中的增量)。
如果我们想在主循环内部之后为下一次迭代计算更多的东西,那么填充启动分支的延迟槽.Lloop_entry
是可能的。但这需要依赖 ,这会损害超标量有序 CPU 的 ILP。(目前top to指令可以并行运行,那么生成新的total指令就可以并行运行了。)addu $v0, $v0, $t0
$v0
addu
addu
lbu
在标量有序(如 MIPS I / MIPS II)或无序 CPU 上都可以。
(虽然我不确定当条件分支从前一个 ALU 指令读取其输入时,早期 MIPS 是否需要停止;分支决策处于 ID 阶段,比EX早1 个周期。但可能不是因为我实际上没有 MIPS RAW 危险的管道互锁;这就是它有一个加载延迟槽的原因。)