【问题标题】:Assembly Coding Standards / Best Practices装配编码标准/最佳实践
【发布时间】:2010-01-31 14:35:05
【问题描述】:

我知道 8086 汇编,现在我正在通过阅读 MIPS Assembly Language ProgrammingSee MIPS Run 这两本书来学习 MIPS 汇编,但我从未停下来思考汇编的编码标准/最佳实践。我每天都想把自己变成一个更好的开发人员,然后想知道这一点来提高自己。如何了解有关汇编编码标准和最佳实践的更多信息?

【问题讨论】:

  • 好目标...你能说出你想回答的问题吗?

标签: assembly coding-style mips


【解决方案1】:

最佳实践是一种社会现象,取决于您将要工作的社会,因此您最好的答案是从您希望与之交互的任何环境中读取现有的 MIPS asm 代码。

从我自己的世界中想到的示例是 Linux 内核的汇编器部分、来自 GCC 的 MIPS 启动代码或 glibc 的 MIPS 端口的汇编器片段。

如果您主要与其他项目互动,最好吸收和模仿该社区的编码实践。

【讨论】:

    【解决方案2】:

    良好的 asm 风格在 ISA(以及同一 CPU 的不同 asm 方言)中非常普遍。编译器输出(如 gcc/clang)通常会完成我在下面提到的所有事情,因此是一个很好的指南。 (而 C 编译器的输出通常是优化小函数的良好起点。)

    通常将指令缩进比标签和汇编指令更深一层。

    将操作数缩进到一致的列(因此不同长度的助记符不会让您的代码参差不齐,并且很容易向下扫描一个块并将每条指令的目标寄存器视为第一个操作数)1支持>.

    将指令行上的 cmets 缩进到右侧一致的列,远远超过操作数以避免视觉噪音。

    将相关指令块组合在一起,用空行分隔它们。 (或者,如果您通过调度指令来优化有序 CPU,那么您实际上无法做到这一点,并且需要使用 cmets 来跟踪每条指令正在处理的问题的哪一部分。使用不同级别的缩进那么 cmets 可能会有所帮助)


    脚注 1:
    除了 MIPS 存储指令,例如 sw $t0, 1234($t1) 其中第一个操作数实际上是源;他们选择让 asm 源对加载和存储使用相同的操作数顺序,可能是因为它们都是机器代码中的 I 型指令。不过,这是 RISC 加载/存储架构的典型 asm,因此要习惯于来自 CISC,其中mov eax, [rdi] 是负载,mov [rdi], eax 是存储。而add [rdi], eax 两者兼而有之。


    示例: 用于无符号整数的 atoi 函数,用于具有分支延迟槽的真实 MIPS。但不是 MIPS I,没有加载延迟槽。尽管无论如何我都试图避免负载使用停顿。 (Godbolt for a C version)

    # 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 是因为我们避免了剥离的第一次迭代中的增量)。

    如果我们想在主循环内的addu $v0, $v0, $t0 之后 为下一次迭代计算更多的东西,那么将启动分支的延迟槽填充到.Lloop_entry 是可能的。但这需要依赖$v0,这会损害超标量有序 CPU 的 ILP。 (目前顶到addu的指令可以并行运行,然后addu产生新的总可以与lbu并行运行。)

    在标量有序(如 MIPS I / MIPS II)或无序 CPU 上都可以。

    (虽然我不确定当条件分支从前一个 ALU 指令读取其输入时早期 MIPS 是否需要停止;分支决策在 ID 阶段,1 个周期 before EX。但是可能不是因为 MIPS 我确实没有针对 RAW 危险的管道互锁;这就是它有一个加载延迟槽的原因。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-31
      • 1970-01-01
      • 2011-12-08
      • 1970-01-01
      相关资源
      最近更新 更多