汇编 MIPS:嵌套循环

Assembly MIPS: Nested loops

当我尝试编写一些代码来打印五行星号符号时,每行打印 4 次,这肯定有点棘手。

****
****
****
****

所以我认为嵌套循环可以挽救局面。 天哪,我错了。

所以我为星号做了一个内循环,为空格做了一个外循环,如下所示:

.text
.globl main
main:
add $t0, $zero, $zero   #i counter for the inner loop
add $t2, $zero, $zero   #j counter for the outer loop

outerloop:

    innerloop:

        slti    $t1, $t0, 4     #while (i<4)
        beq     $t1, $zero, innerexit

        li      $v0, 11         #printf("*");
        la      $a0, '*'
        syscall

        addiu   $t0, $t0, 1     #i++

    j innerloop
    innerexit:

slti    $t3, $t2, 5     #while (j<5)
beq     $t3, $zero, outerexit

li      $v0, 11         #printf("\n");
la      $a0, '\n'
syscall

addiu   $t2, $t2, 1     #j++

j outerloop
outerexit:

li  $v0, 10
syscall

但是输出只给我一行:

****

外环怎么了?

将第 23 行的 5(slti $t3, $t2, 5) 更改为 4,因为循环从 0 开始,并在第 28 行的系统调用之后添加 ( add $t0, $zero, $zero) 到重新初始化我的值。

.text
.globl main
main:
add $t0, $zero, $zero   #i counter for the inner loop
add $t2, $zero, $zero   #j counter for the outer loop

outerloop:

    innerloop:

        slti    $t1, $t0, 4     #while (i<4)
        beq     $t1, $zero, innerexit

        li      $v0, 11         #printf("*");
        la      $a0, '*'
        syscall

        addiu   $t0, $t0, 1     #i++

    j innerloop
    innerexit:

slti    $t3, $t2, 4     #while (j<5)
beq     $t3, $zero, outerexit

li      $v0, 11         #printf("\n");
la      $a0, '\n'
syscall
add $t0, $zero, $zero
addiu   $t2, $t2, 1     #j++

j outerloop
outerexit:

li  $v0, 10
syscall

最简单的方法是使用非嵌套循环的写入字符串系统调用 N 次。 (好吧,可以说,制作一个包含所有行的长字符串会更“简单”,但可维护性较差,并且不利于 N 较大的程序的大小)。

注意递减计数器的使用,向零递减计数,因此我们可以 bne 反对 $zero。这对于 asm 和 来说是惯用的。特别是对于任何你知道行程计数保证至少为 1 的循环。(如果不是这种情况,你通常会在需要时使用循环外的分支来跳过它。)

## Tested, works in MARS 4.5
.data
line: .asciiz "****\n"

.text
.globl main
main:
   li  $t0, 4     # row counter
   li  $v0, 4     # print string call number
   la  $a0, line

 .printloop:
   syscall           # print_string has no return value, doesn't modify v0
   addiu  $t0, $t0, -1
   bnez   $t0,  .printloop           # shorthand for BNE $t0, $zero, .printloop

   li  $v0, 10       # exit
   syscall

您可以在临时缓冲区中生成字符串,并在打印循环之前的单独循环中使用来自寄存器的计数。因此,您仍然可以支持 运行 时间可变的行和列计数,使用两个顺序循环而不是嵌套。

使用按 4 对齐的缓冲区,我们可以一次存储 4 个字符的整个单词,这样循环就不必 运行 多次迭代。 (li reg, 0x2a2a2a2a 需要 2 条指令,但 li reg, 0x2a2a 只需要一条指令,因此使用 sh 进行 2 条指令会使代码更小)。

.text
.globl main
main:
.eqv    WIDTH, 5
.eqv    ROWS, 4

   addiu  $sp, $sp, -32         # reserve some stack space.    (WIDTH&-4) + 8   would be plenty, but MARS doesn't do constant expressions.
   move   $t0, $sp
   
   addiu  $t1, $sp, WIDTH       # pointer to end of buf = buf + line length., could be a register
   li     $t2, 0x2a2a2a2a           # MARS doesn't allow '****' or even '*' << 8 | '*'
 .makerow:                  # do{
   sw     $t2, ($t0)          # store 4 characters
   addiu  $t0, $t0, 4         # p+=4
   sltu   $t7, $t0, $t1
   bnez   $t7, .makerow     # }while(p < endp);
# overshoot is fine; we reserved enough space to do whole word stores
   li     $t2, '\n'
   sb     $t2, ($t1)
   sb     $zero, 1($t1)     # terminating 0 after newline.  Unfortunately an sh halfword store to do both at once might be unaligned

   move   $a0, $sp
   li     $t0, ROWS
   li     $v0, 4             # print string call number

 .printloop:
   syscall                   # print_string has no return value, doesn't modify v0
   addiu  $t0, $t0, -1
   bnez   $t0,  .printloop           # shorthand for BNE $t0, $zero, .printloop.      # }while(--t != 0)


## If you were going to return instead of exit, you'd restore SP:
#  addiu $sp, $sp, 32

   li  $v0, 10       # exit
   syscall

正如预期的那样,每行打印 5 个星号。

*****
*****
*****
*****

通常(在实际系统中)系统调用比普通指令昂贵,因此准备一个大缓冲区多个换行符实际上是有意义的。 (系统调用的开销使写入 1 字节与 5 字节甚至 20 字节之间的差异相形见绌,因此即使调用 print_string 而不是 print_char 是一种隐藏在系统调用内部的工作,但这是合理的。 )

在那种情况下,您可能需要嵌套循环,但使用 sb / addiu $reg, $reg, 1 指针增量而不是 syscall。最后只做一次系统调用

或者一个循环来一次存储所有 * 个字符 4(对于 ROWS * COLS / 4 向上舍入迭代),然后另一个循环将 \n' 换行符插入它们所属的位置。这使您可以使用比一次按 1 个字节的顺序执行所有操作更少的指令将所有数据放入内存。 (对于非常大的 row*col 计数,您可能会将缓冲区大小限制为 4 或 8 kiB 或其他大小,因此当内核的系统调用处理程序读取数据以将其复制到需要的位置时,您的数据仍在缓存中。)


顺便说一句,在 C 术语中,print char 系统调用更像是 putchar('*'),而不是 printf("*")。请注意,您是按值传递一个字符,而不是指向以 0 结尾的字符串的指针。