跳转至

Lab4-1实验报告

理解简单RISC-V程序

  1. acc是如何获得函数参数的,又是如何返回函数返回值的?

    1. a0,a1实际上是参数寄存器,用于在函数调用过程中依次保存第一个和第二个参数,以及在函数返回时传递返回值。

    2. 通过预处理,将a的值写入a0,将b的值写入a1,通过这两个寄存器实现传值

    3. 结束时将结果res存入a0,实现函数返回值。

  2. acc函数中s0寄存器的作用是什么,为什么在函数入口处需要执行sd s0, 40(sp)这条指令,而在这条指令之后的addi s0, sp, 48这条指令的目的是什么?

    1. s0和s1是保存寄存器,被调用函数需要保证这些寄存器的值在函数返回后仍然维持函数调用之前的原值。

    2. 通过在40(sp)位置预先储存原先s0的值,后续再将此处的值返回到s0实现维持函数调用前后s0寄存器的值不变。

    3. 由于sp是被调用函数用于指向栈顶的指针,s0是指向栈底的指针,进入函数后的sp是进入函数前栈帧的栈顶,进入函数后这个栈顶的值应该传给用于指向新栈帧的栈底的s0,再通过addi s0, sp, 48这条指令主要就是用于给新栈帧的栈底指针传值。

  3. acc函数的栈帧(stack frame)的大小是多少?

    48字节

  4. acc函数栈帧中存储的值有哪些,它们分别存储在哪(相对于sp或s0来说)?

    1. 栈帧储存了a,b,res,i以及调用函数前s0的值

    2. a在-48(s0),b在-40(s0),res在-32(s0),i在-24(s0),原s0在-8(s0)

  5. 请简要解释acc函数中的for循环是如何在汇编代码中实现的。

    1. for循环的初始赋值语句 long long i = a 通过

    2. ld a5,-40(s0)

    3. sd a5,-24(s0) 完成

    4. 循环条件判断通过

    5. ld a4,-24(s0)

    6. ld a5,-48(s0)

    7. ble a4,a5,.L3 完成

    8. 循环变量递增通过

    9. ld a5,-24(s0)

    10. addi a5,a5,1

    11. sd a5,-24(s0) 完成

    12. 通过调整.L3和.L2之间的前后顺序来实现循环语句的执行逻辑

    13. 主要循环体在.L3内部实现 

  6. 请查阅资料简要描述编译选项-O0和-O2的区别。

  7. -O0: 不做任何优化,这是默认的编译选项。

  8. -O2: 做出一定优化,包括延迟栈弹出时间等等。

  9. 请简要讨论src/lab4-1/acc_opt.ssrc/lab4-1/acc_plain.s的优劣。

  10. Opt多处使用伪代码,减少代码长度,优化阅读体验。

  11. Opt多用寄存器内部计算,减少了栈的使用,节省存储空间;减少与储存器之间的数据交换,加快代码运行速度。但是会导致不够稳定,如遇突然断电,ram内的数据会消失。

  12. Opt使用更少的寄存器,让 \(a_0\) 在给i赋完值后用于储存res,释放更多内存空间,但是会增加理解代码难度。

理解递归汇编程序

  1. 为什么 src/lab4-1/factor_plain.s 中factor函数的入口处需要执行 sd ra, 24(sp) 指令,而 src/lab4-1/acc_plain.s 中的 acc 函数并没有执行该指令?

    1. 在进入函数前,ra 已经存储改函数的返回地址。

    2. Acc 函数不存在嵌套调用,ra 寄存器不会发生改变,所以不需要将 ra 存入栈帧。

    3. 但是factor函数存在嵌套调用,ra 在进入下一个函数时会发生变化,为了保证函数的嵌套调用,应确保ra的动态平衡。通过在函数正式开始前将ra压入这个函数的栈帧内,来保证ra的更改不会对函数调用产生影响。

  2. 请解释在call factor前的mv a0, a5这条汇编指令的目的。

    1. a0,a1实际上是参数寄存器,用于在函数调用过程中保存参数,以及在函数返回时传递返回值。

    2. 通过mv a0,a5使得进入下一个factor的参数是n-1

  3. 请简要描述调用factor(10)时栈的变化情况;并回答栈最大内存占用是多少,发生在什么时候。

            <- 栈底
    ra(10)  <- -8
    s0(10)  <- -16
    n(10)   <- -24
            <- -32
    ra(9)   <- -40
    s0(9)   <- -48
    m(9)    <- -56
            <- -64
        ......
    ra(1)   <- -...
    s0(1)   <- -304
    n(1)    <- -312
            <- -320
    ra(1)   <- -328
    s0(1)   <- -336
    n(1)    <- -344
            <- -352
    
    1. 刚开始构建了一个大小为32的factor(10)的栈帧,用于存放函数返回地址,原栈栈底以及当前函数的n。在进入factor(9)以后,会在factor(10)的后面建立factor(9)的大小为32的栈帧.

    2. 依次类推直到调用函数factor(0),总计大小为32*11=352字节。

    3. 在factor(0)执行结束之后,factor(0)的栈帧会被删去,函数依次回调直到factor(10)执行结束之后,栈的内容被清空。

  4. 假设栈的大小为4KB,请问factor(n)的参数n最大是多少?

    1. 4KB = \(2^{12}\) B

    2. 每个栈帧大小为32B = 2^5B

    3. 可以开\(2^8\)个栈帧,也就是可以嵌套调用\(2^8\)次。即n的最大值为 \(2^8-1\)。 

  5. 请简要描述src/lab4-1/factor_opt.ssrc/lab4-1/factor_plain.s的区别。

    1. Opt采用递推计算factor(n)的值

    2. Plain使用递归计算factor(n)的值

  6. 请从栈内存占用的角度比较src/lab4-1/factor_opt.ssrc/lab4-1/factor_plain.s的优劣。

    1. Opt基本没有用到栈,只用了寄存器进行计算。十分节省储存空间。

    2. Plain使用栈保存每一个过程中的一些数值,虽然用了更大的储存空间,但是可以通过程序设计使得能够单次运算访问多个factor的值。同时更加稳定。

  7. 请查阅尾递归优化的相关资料,解释编译器在生成src/lab4-1/factor_opt.s时做了什么优化,该优化的原理,以及什么时候能进行该优化。

  8. 做了一个被称为“尾调用”的优化。“尾调用”是指某个函数的最后一步是调用另一个函数。即最后一步新调用的返回值直接被当前函数返回,从而避免新的运行栈生成。

  9. 对于单次询问可以进行此优化,对于多次询问有其他更好的优化方法,比如用数组记录每一次计算的值。

理解switch语句产生的跳转表

  1. 请简述在src/lab4-1/switch.s中是如何实现switch语句的。

    1. 通过将一个跳转表标签L4地址写入a4寄存器,再将switch的变量减去最小的case对应到标签L4地址的另一个标签。再按序排列相关标签,实现不用if比较达成数值比较的效果。
  2. 请简述用跳转表实现switch和用if-else实现switch的优劣,在什么时候应该采用跳转表,在什么时候应该采用if-else。

    1. 优势是减少比较次数,通过地址引用实现数据比较。

    2. 当case分支情况较多会采取跳转表,分支较少的情况下采用if-else。

    3. 当case分支较密集,范围跨度比较小时采用跳转表,跨度较大时使用if-else,减少跳转表无用篇幅。

冒泡解析

程序开始先开辟一块栈。依次压入s0,*arr,len,i,j.

在“for1”中将i写入a4寄存器,len-1写入a1寄存器,比较i和len-1,超出则至exit1

否则开始j循环,用寄存器a5存放j,计算出a0+a5*8的值存入a7,也就是arr[j]的地址,然后依次将arr[j]和arr[j+1]赋值给a2,a3。如果a2>=a3就进行交换写回储存空间。

再将j的值从a栈中读出写入a5,计算j+1后写回,计算len-1-i与新的j进行比较,如超出则返回i循环。

最后结束程序时先取出原s0,再清空栈。

斐波那契解析

程序开始先开辟一块栈。依次压入ra,s0,n.

再将n传入a5寄存器,与1进行比较,如果大于1执行后续程序,否则将a0赋值为1后返回上一级函数。

第一次先计算f(n-1),将返回结果压入栈中,再计算f(n-2),将返回结果与栈中的前一个结果相加得到f(n)后,在重新取出ra,s0并且清空栈后返回。