在MIPS中调用类似于二叉树的递归过程

时间:2017-02-18 18:18:42

标签: recursion assembly tree mips

我正在完成一项任务,并且很难理解如何在C中正确编码以下问题。

int choose(int n, int k){
    if (k == 0) {
        return 1;
    } else if (n == k) {
        return 1;
    } else if (n < k) {
        return 0;
    } else {
        return choose(n-1, k-1) + choose(n-1, k);
    }
}

我的想法是使用三个寄存器将值存储到堆栈中,每次调用$s0, $s1, $s2,其中$s0将包含更新的n的值; $s1会维持k的价值;并且$s2会在第二个k中保留choose(n-1, k)的值,因为该值只会在父级调用更改时减少。我选择这个的原因是因为k的值没有从这个中的每次调用中减去,它应该是相同的,直到父级在前一次调用中减少它。

这是我正在尝试的Choose程序。问题是我当然没有得到正确的答案。

Choose:
    #store current values onto stack
    addi $sp, $sp, -16
    sw $ra, 0($sp)
    sw $s0, 4($sp)
    sw $s1, 8($sp)
    sw $s2, 12($sp)

    #check values meet criteria to add to $v0       
    beq $s1, $0, one
    beq $s0, $s1, one
    blt $s0, $s1, zero
    beq $s2, $0, one

    #no branches passed so decrement values of n and k
    subi $s0, $s0, 1
    subi $s1, $s1, 1

    #move values of registers to $a's for argument passing
    move $a0, $s0
    move $a1, $s1

    jal Choose  #call procedure again

    #this is where I'm having my problems
    #not sure how to loop the procedure to get
    #the second half of the equation Choose(n-1,k)
    #which is the reason for the next 2 lines of code
    move $a2, $s2
    jal Choose

    add $v0, $v0, $v1
    j Choose_Exit


#add one to $v1 from branch passed
one:
    addi $v1, $v1, 1
    j Choose_Exit

#branch returns 0   
zero:
    addi $v1, $v1, 0
    j Choose_Exit

#return values to caller from stack
Choose_Exit:
    lw $s2, 12($sp)
    lw $s1, 8($sp)
    lw $s0, 4($sp)
    lw $ra, 0($sp)
    addi $sp, $sp, 16
    jr $ra

所以我在理解如何正确实现此递归过程两次以将它们添加到一起时遇到问题。我可以理解如何在MIPS中创建递归过程来执行阶乘,因为这始终是任何语言的递归定义。但是使用不同参数的递归然后将它们全部加在一起让我感到困惑。

当写在纸上时,我知道这个程序可以用父母和孩子的二叉树来表示。父项是单个函数Choose(n,k),子项是Choose(n-1, k-1) + Choose(n-1, k),一旦叶子项中的一个从if语句分支,它会将一个数字传递给将等待另一个的父项添加的被调用者部分返回其值等等

任何帮助指出我正确的方向,我的方法是错误的将是伟大的。我理解一开始,我理解结束,只需要一些帮助来帮助理解中间最重要的部分。

2 个答案:

答案 0 :(得分:2)

你非常接近。

您建立了包含四个单词的堆栈框架:返回地址,arg1,arg2,并保存为返回值。

你的主要障碍是,在第一次调用你的函数之后,你必须将$v0保存到堆栈中[如上面提到的Margaret]。

这里有一些我认为可行的代码。它与你的非常相似,但我是从头开始编写的。它有正确的&#34; push&#34; /&#34; pop&#34;第一个电话的返回值。

我确实为早期转义[非递归]情况添加了一个小优化:它们省略了创建堆栈框架。

无论如何,这是:

#@+
#   int
#   choose(int n, int k)
#   {
#
#       if (k == 0)
#           return 1;
#
#       if (n == k)
#           return 1;
#
#       if (n < k)
#           return 0;
#
#       return choose(n - 1,k - 1) + choose(n - 1,k);
#   }
#@-

    .text

# choose -- choose
#
# RETURNS:
#   v0 -- return value
#
# arguments:
#   a0 -- n
#   a1 -- k
#
# registers:
#   t0 -- temp for 1st return value
choose:
    beqz    $a1,choose_one          # k == 0? if yes, fly
    beq     $a0,$a1,choose_one      # n == k? if yes, fly
    blt     $a0,$a1,choose_zero     # n < k? if yes, fly

    # establish stack frame (preserve ra/a0/a1 + space for v0)
    sub     $sp,$sp,16
    sw      $ra,12($sp)
    sw      $a0,8($sp)
    sw      $a1,4($sp)

    addi    $a0,$a0,-1              # get n - 1 (common to both calls)

    # choose(n - 1,k - 1)
    addi    $a1,$a1,-1              # get k - 1
    jal     choose
    sw      $v0,0($sp)              # save 1st return value (on _stack_)

    # choose(n - 1,k)
    addi    $a1,$a1,1               # get k (from k - 1)
    jal     choose

    lw      $t0,0($sp)              # "pop" first return value from stack
    add     $v0,$t0,$v0             # sum 1st and 2nd values

    # restore from stack frame
    lw      $ra,12($sp)
    lw      $a0,8($sp)
    lw      $a1,4($sp)
    add     $sp,$sp,16
    jr      $ra                     # return

choose_one:
    li      $v0,1
    jr      $ra

choose_zero:
    li      $v0,0
    jr      $ra

<强>更新

  

首先,我喜欢你在调用它之前如何记录过程。我要去偷那个!

做我的客人!这是多年写作asm的原因。有关如何写好asm的想法的入门读物,请参阅我的回答:MIPS linked list

  

我试过这个并且它有效。我需要尝试使用你的代码来理解为什么堆栈被操作(总是认为它必须在proc的开头和结尾)。

通常,堆栈帧在proc启动时建立并从proc端恢复。您处理&#34;快速逃脱的代码&#34;基于已经建立框架,[非递归]案例 是正确的。

这只是一个小优化。但是,它来自这样一个事实:因为mips有很多寄存器,对于小功能,我们甚至不需要堆栈帧,特别是如果函数是一个&#34; leaf&#34;或&#34;尾巴&#34; (即它不会调用任何其他功能)。

对于较小的[非递归]函数,有时我们可以使用仅保留$ra的单字堆栈帧(例如):fncA调用fncB,但{{} 1}}是一片叶子。 fncB需要一个框架,但fncA 。事实上,如果我们控制这两个函数并且知道 fncB 返回地址而不是在fncB中创建堆栈帧:

$t9

通常情况下,我们无法依赖fncA保留fncA: move $t9,$ra # preserve return address jal fncB # call fncB jr $t9 # return fncB: # do stuff ... jr $ra # return ,因为根据mip ABI,fncB可以随意修改/删除任何注册 $t9fncB。但是,如果我们制定功能,我们认为$sp是&#34;私人&#34;到$s0-$s7(例如只有fncB可以访问的C fncA函数),我们可以 我们想要的任何

信不信由你,以上的static符合 ABI。

给定的被调用者(例如fncA需要保留fncA为其来电者< / em>,仅为自己。而且,重要的是fncA内的,而不是特定的寄存器。被调用者只需要保留$ra,确保$ra在退出时具有与条目相同的值,并且它返回到调用者[{1}}所执行的正确地址 - 因为如果调用了$s0-$s7,则它具有$sp中的

  

我喜欢你使用temp寄存器。

额外的寄存器是必需的,因为在mips中,我们可以从内存操作数执行算术运算。 mips只能jr $t9。 (即) no 这样的事情:

$ra

我使用fncA来简化/清晰,因为当您需要临时注册时,lw/sw是常用的。代码&#34;读得更好&#34;使用 add $v0,$v0,0($sp) 时。但是,这只是 约定。

在mip ABI中,$t0可以修改,$t0-$t9也可以修改,因为只需要保留$t0。而且,&#34;修改&#34;意味着它们可用于保留任何价值或用于任何目的。

在上面的链接中,请注意$a0-$a3直接递增$v1以查找字符串的结尾。它使用$s0-$s7作为一个有用的目的,但就其来说,strlen正在被删除&#34; [由$a0]。此用法符合 ABI。

$a0中,我本可以使用任何注册:$a0strlen而不是choose。实际上,在$v1中的特定点,不再需要$a2-$a3,因此可以使用它代替$t0。虽然对于choose,我们 -ABI符合(因为我们保存/恢复$a0),这可以在$t0中使用,因为我们会恢复原始值来自函数epilog [stack frame pop]的choose,保留了函数的递归性质。

正如我所说,$a0-$a1是用于临时空间的常用寄存器。但是,我已经编写了使用所有10个函数的函数,并且仍然需要更多函数(例如使用Bresenham圆算法绘制到帧缓冲区中)。 choose$a0可用作临时注册以获得额外的6.如果需要,$t0-$t9可以保留在堆栈框架中,仅用于释放它们以用作更多临时注册表

答案 1 :(得分:0)

免责声明:我重写汇编代码没有检查它。

  1. 有一些延迟槽可以更好地考虑使用它们并使它们明确以避免在分支指令之后聚合隐式nop指令。
  2. 反转select(n - 1,k - 1)和select(n - 1,k)之间的调用顺序,以便更智能地使用$ a0和$ a1以及堆栈。
  3. 限制堆栈使用仅用于调用choose(n - 1,k)并使用尾调用来调用choose(n - 1,k - 1)。
  4. 堆叠a0-1而不是a0的值更有意义。
  5. 我们将所有内容累积到$ v0而不是堆叠它。我们保留$ t0作为本地结果添加到$ v0,因为它很便宜,而我们可以通过直接用$ v0来丢弃它。
  6. 因此,总体变化应该会让更少的指令更快乐,而且更快,更少堆栈空间。
  7. 汇编代码:

    #@+
    #   int
    #   choose(int n, int k)
    #   {
    #
    #       if (k == 0)
    #           return 1;
    #
    #       if (n == k)
    #           return 1;
    #
    #       if (n < k)
    #           return 0;
    #
    #       return choose(n - 1,k - 1) + choose(n - 1,k);
    #   }
    #@-
    
        .text
    
    # choose -- choose
    #
    # RETURNS:
    #   v0 -- return value
    #
    # arguments:
    #   a0 -- n
    #   a1 -- k
    #
    # registers:
    #   t0 -- temp for local result to accumulate into v0
    choose:
        j       choose_rec
        addui   $v0, $zr, 0             # initialize $v0 to 0 before calling
    choose_rec:
        beqz    $a1,choose_one          # k == 0? if yes, fly
        addui   $t0,$zr,1               # branch delay-slot with $t0 = 1
        beq     $a0,$a1,choose_one      # n == k? if yes, fly
        nop                             # branch delay-slot with $t0 = 1 already done
        blt     $a0,$a1,choose_zero     # n < k? if yes, fly
        addui   $t0,$zr,0               # branch delay-slot with $t0 = 0
    
        # establish stack frame (preserve ra/a0/a1)
        sub     $sp,$sp,12
        addui   $a0,$a0,-1              # get n - 1 (common to both calls)
        sw      $ra,8($sp)
        sw      $a0,4($sp)          
        jal     choose_rec              # choose(n - 1,k)
        sw      $a1,0($sp)
    
        # restore from stack frame
        lw      $ra,8($sp)
        lw      $a0,4($sp)
        lw      $a1,0($sp)
        add     $sp,$sp,12
    
        # choose(n - 1,k - 1)
        j       choose_rec              # tail call: jump instead of call
        addi    $a1,$a1,-1              # get k - 1
    
    choose_one:
    choose_zero:
        jr      $ra                     # return   
        addui   $v0,$v0,$t0             # branch delay-slot with $v0 += $t0