Part A(第一部分)

实验3是项目2A的先决条件,强烈建议学习第6-9讲、讨论3和作业2。

仅对于A部分,请尽量不要使用s类型寄存器(s0s1s2...),因为调用约定将在后面的实验4和B部分中教授。

在这一部分中,您将实现一些数学运算,这些运算将在后面的分类中使用。

在开始之前,请从起始仓库拉取代码并更新Venus。

git pull starter main
bash test.sh download_tools

任务1:绝对值(引导)

为了让您熟悉此项目的工作流程,我们将引导您完成此任务。

运行测试

在此项目中,测试使用Python编写,并编译为RISC-V汇编代码。

提供的测试的Python源代码位于unittests.py中。查看unittests.py的内容。虽然在任务1-4中测试已经为您编写好了,但熟悉单元测试框架有助于理解测试的功能。

要运行测试,在您的本地机器上,首先在61c-proj2目录中运行bash test.sh。这将为您提供可以运行的测试命令的概览。特别是,bash test.sh part_a会编译并运行任务1-5的所有测试。您还可以提供特定函数的名称来编译并运行该函数的所有测试。

对于此任务,由于我们正在实现abs函数,在您的本地机器上,运行bash test.sh test_abs。这将创建一个test-src文件夹,其中包含编译为RISC-V的Python测试。

由于我们还没有实现abs函数,一些测试会失败。让我们尝试实现abs

您可以在文本编辑器中编辑文件,也可以直接在Venus中编辑。要在Venus中编辑文件,请切换到Files标签页。在这里您可以打开和编辑汇编文件。请记住经常保存文件,使用Control+S(Windows)或Command+S(Mac)。Venus不会自动保存您的工作。

打开src/abs.s(在文本编辑器或Venus中),然后复制粘贴下面的实现。

abs:
  # 从内存加载数字
  lw t0 0(a0)
  blt t0, zero, done

  # 取反a0
  sub t0, x0, t0

  # 将数字存回内存
  sw t0 4(a0)

done:
  jr ra

再次在您的本地机器上,运行bash test.sh test_abs。测试没有通过,所以我们的实现可能有问题。

使用VDB通过Venus调试测试

首先,在您的网络浏览器中打开Venus并挂载您的文件。(如果遇到问题,请参阅规格的设置部分。)

让我们先设置一个断点。在abs函数的开头输入ebreak。这会在blt t0, zero, done指令之前放置一个断点。

要启动调试器,在Venus终端中,运行cd /vmfs/test-src,然后运行ls。这应该列出您可以运行的所有测试文件。运行vdb test_abs_one.s来启动绝对值测试的调试器。

Venus模拟器标签页中,点击Run开始运行程序。调试器将在我们设置的断点处暂停。暂停时,您可以检查寄存器和内存。特别注意寄存器a0在这里包含一个大数字,因为此测试将参数a0作为内存地址调用您的函数。您可以在Venus的"Memory"标签页中查找此数字,以查看存储在此内存地址的数据。我们建议将显示设置更改为"Hex"。

您还可以在调试器中逐行执行代码。点击Step执行下一条指令blt t0, zero, done。逐步执行函数并检查寄存器。看看您能否发现我们实现中的错误。

点击揭示答案

在第一行中,如果数字小于0,我们跳过取反并跳转到done。然而,我们实际上想要在数字大于等于0时跳过取反,在数字小于0时执行取反。

一种修复方法是当参数大于等于0时跳转到done。其他修复方法也是可行的!

修改src/abs.s中的实现来修复错误。再次运行bash test.sh test_abs,您应该看到3个测试中有2个现在通过了。

使用memcheck通过test.sh调试测试

在此项目中,您经常会遇到代码逻辑看起来正确但测试仍然失败的情况。这些错误通常是由无效的内存访问引起的。本学期我们引入了一个新功能memcheck,可以让您轻松调试这些错误,但如果您在使用此功能时遇到任何问题,请在Ed上告知课程工作人员!

Memcheck引入了两个不同的标志,您可以添加到test.sh命令中:-mc用于普通memcheck,-mcv用于更详细的版本。让我们尝试使用memcheck来查看代码有什么问题。

运行bash test.sh test_abs -mc,除了上一节末尾看到的错误消息外,您应该看到类似以下的错误:

[memcheck] Invalid memory access of size 4. Address 0x10000004 is 0 bytes after a block of size 4 in static.
        Program Counter: 0x00000044
        File: ../src/abs.s:23
        Instruction: sw t0 4(a0)
        Registers:
                              x1(ra)=0x0000000C   x2(sp)=0x7FFFFFF0   x3(gp)=0x10000000
          x4(tp)=0x00000000   x5(t0)=0x00000001   x6(t1)=0x00000000   x7(t2)=0x00000000
          x8(s0)=0x00000000   x9(s1)=0x00000000  x10(a0)=0x10000000  x11(a1)=0x00000000
         x12(a2)=0x00000000  x13(a3)=0x00000000  x14(a4)=0x00000000  x15(a5)=0x00000000
         x16(a6)=0x00000000  x17(a7)=0x00000000  x18(s2)=0x00000000  x19(s3)=0x00000000
         x20(s4)=0x00000000  x21(s5)=0x00000000  x22(s6)=0x00000000  x23(s7)=0x00000000
         x24(s8)=0x00000000  x25(s9)=0x00000000 x26(s10)=0x00000000 x27(s11)=0x00000000
         x28(t3)=0x00000000  x29(t4)=0x00000000  x30(t5)=0x00000000  x31(t6)=0x00000000

这些错误设计得尽可能与valgrind相似。尝试使用此错误消息来精确定位非法内存访问发生的位置。

点击揭示答案

在输出的第四行中,显示这是由sw t0 4(a0)指令引起的。我们应该在这一行中写入偏移量0而不是4。

修改src/abs.s中的实现来修复错误。再次运行bash test.sh test_abs,您应该通过所有测试用例。

任务2:ReLU

在此项目中,我们将使用整数数组。请记住,整数数组中的整数存储在连续的内存块中。

要将整数数组作为参数传递,我们将传递指向整数数组开头的指针和数组中的元素数量。

在此图中,寄存器a0存储第一个参数(数组起始地址)。寄存器a1存储第二个参数(数组中整数的数量)。

概念概述:ReLU

ReLU函数接收一个整数数组,并将数组中的每个负值设为0。数组中的正值保持不变。换句话说,对于数组中的每个元素x,ReLU计算max(x, 0)

ReLU应该原地修改数组。例如,如果上面的整数数组被传递给ReLU,结果将存储在内存中的相同位置:

请注意,数组中的负值在内存中被设为0。

你的任务

src/relu.s中填写relu函数。

relu:任务2。
参数 a0 int * 指向整数数组开头的指针。
a1 int 数组中整数的数量。您可以假设此参数与整数数组的实际长度匹配。
返回值

如果输入在以下方面格式不正确,请将适当的返回代码放入a0并运行j exit退出程序。(例如,如果数组长度小于1,运行li a0 36j exit。)

返回代码 异常
36 数组长度小于1。

测试与调试

要测试您的函数,在本地终端中运行bash test.sh test_relu

要调试您的函数,在Venus终端中运行cd /vmfs/test-src,然后运行VDB命令启动调试器:

vdb test_relu_standard.s
vdb test_relu_length_1.s
vdb test_relu_invalid_n.s

以下是一些适用于整个项目的调试技巧:

  • 如果您看到错误"You are attempting to edit the text of the program though the program is set to immutable at address 0x00000000!",这意味着您正尝试写入内存地址0x00000000(或错误中显示的任何内存地址)。这可能是因为您将此地址提供给了存储指令,然后该指令尝试写入此地址。
  • 如果您在启动调试器时看到错误"label exit used but not defined",请确保您使用上面的vdb命令启动调试器。点击"assemble and simulate from editor"不会工作。
  • 遗憾的是,本地测试不检查越界内存访问。如果您在自动评分器上遇到失败的测试,请尝试确保您的代码永远不会写入数组之外的内存。

如果您的ReLU在本地能工作但在Gradescope上不行,以下是一些本地测试未检查的边界情况。您可以修改unittests.py来为这些情况编写自己的测试!

  • 本地测试不会对较大的数字执行ReLU。
  • 观看调试视频

任务3:ArgMax

概念概述:ArgMax

argmax函数接收一个整数数组,并返回数组中最大元素的索引。如果多个元素并列为最大元素,返回最小的索引。

例如,如果整数数组[-6, -1, 6, 1]被传递给argmax函数,输出应为2,因为最大整数(6)位于数组的索引2处。如果整数数组改为[6, 1, 6, 1],则输出应为0,因为最大整数(6)首先出现在索引0处。

你的任务

src/argmax.s中填写argmax函数。

argmax:任务3。
参数 a0 int * 指向整数数组开头的指针。
a1 int 数组中整数的数量。您可以假设此参数与整数数组的实际长度匹配。
返回值 a0 int 最大元素的索引。如果最大元素出现多次,返回最小的索引。

如果输入在以下方面格式不正确,请将适当的返回代码放入a0并运行j exit退出程序。

返回代码 异常
36 数组长度小于1。

测试与调试

要测试您的函数,在本地终端中运行bash test.sh test_argmax

要调试您的函数,在Venus终端中运行cd /vmfs/test-src,然后运行VDB命令启动调试器:

vdb test_argmax_invalid_n.s
vdb test_argmax_length_1.s
vdb test_argmax_standard.s

如果您的argmax在本地能工作但在Gradescope上不行,以下是一些本地测试未检查的边界情况。您可以修改unittests.py来为这些情况编写自己的测试!

  • 本地测试不检查当数组中最大元素是数组最后一个元素时代码是否正常工作。
  • 本地测试不检查当最大元素出现多次时代码是否正常工作。
  • 观看调试视频

任务4:点积

概念概述:点积

点积函数接收两个整数数组,将数组的对应项相乘,并返回所有乘积的总和。

例如,如果这两个整数数组被传递给点积函数,函数将返回(1*6) + (2*1) + (3*6) + (4*1) + (5*6) + (6*1) + (7*6) + (8*1) + (9*6) = 170

概念概述:数组步长

如果我们不想遍历数组的每个元素,而是想遍历每隔一个或每隔两个元素呢?为此,我们将定义数组的步长。

要以步长n遍历数组,从数组的开头开始,只考虑每n个元素,跳过中间的元素。

请注意,步长以元素数量给出,而不是字节数。这意味着以步长1遍历等同于遍历数组的每个元素。

例如,在上图中,两个数组都使用步长2,因此我们跳过数组中的每隔一个元素。应考虑5个元素,因此我们在将5对元素相乘后停止。函数将返回(1*6) + (3*6) + (5*6) + (7*6) + (9*6) = 150

在上图中,第一个数组使用步长2,因此我们跳过该数组中的每隔一个元素。第二个数组使用步长3,因此我们使用该数组中的每第三个元素。应考虑3个元素,因此我们在将3对元素相乘后停止。函数将返回(1*6) + (3*1) + (5*6) = 39

你的任务

src/dot.s中填写dot函数。

dot函数可以假设a2参数(计算中要使用的元素数量)不会导致数组越界访问。但是,在本项目后面从其他函数调用dot时,您需要确保这一点。

dot:任务4。
参数 a0 int * 指向第一个数组开头的指针。
a1 int * 指向第二个数组开头的指针。
a2 int 计算中要使用的元素数量。
a3 int 第一个数组的步长。
a4 int 第二个数组的步长。
返回值 a0 int 两个数组的点积,使用给定的元素数量和步长。

如果输入在以下方面格式不正确,请将适当的返回代码放入a0并运行j exit退出程序。

返回代码 异常
36 要使用的元素数量小于1。
37 任一数组的步长小于1。

测试与调试

要测试您的函数,在本地终端中运行bash test.sh test_dot

要调试您的函数,在Venus终端中运行cd /vmfs/test-src,然后运行VDB命令启动调试器:

vdb test_dot_length_1.s
vdb test_dot_length_error.s
vdb test_dot_length_error2.s
vdb test_dot_standard.s
vdb test_dot_stride.s
vdb test_dot_stride_error1.s
vdb test_dot_stride_error2.s

另外,请查看调试视频

任务5:测试

在此任务中,您将为一些已经实现好的数学函数编写测试。

概念概述:损失函数

损失函数接收两个整数数组,输出一个整数数组,包含每对对应项之间差异的某种度量。一些损失函数还输出所有差异度量的总和。本项目使用三种不同的损失函数。

绝对损失函数计算并输出每对对应项之间的绝对差,然后输出所有绝对差的总和。

平方损失函数计算并输出每对对应项之间差的平方,然后输出所有平方差的总和。

零一损失函数计算每对对应项是否相等,不输出任何总和。

这些损失函数使用辅助函数initialize-zero。它接收数组的长度作为输入,输出一个新分配的给定长度的数组,填充为零。

你的任务

studenttests.py中填写三个损失函数和initialize-zero辅助函数的测试。请注意,这些函数已经为您实现,您可以在coverage-src中查看。请不要重新实现这些函数,您的工作是编写覆盖测试来测试它们的功能。

我们建议您查看unittests.py以了解编写测试的Python框架的工作方式。

要确保您的测试被单元测试框架运行,请确保您的函数名以test_开头!例如,函数def test_length_0(self)会运行,但函数def length_0(self)不会运行。

损失函数:任务5。
参数 a0 int * 指向第一个输入数组开头的指针。
a1 int * 指向第二个输入数组开头的指针。
a2 int 数组中整数的数量。
a3 int * 指向输出数组开头的指针,结果将存储在此处。
返回值 a0 int 输出数组中元素的总和。(零一损失没有返回值。)
initialize_zero:任务5。
参数 a0 int 要创建的数组大小。
返回值 a0 int * 指向新分配的零数组的指针。

如果输入格式不正确,函数将返回以下错误代码:

返回代码 异常
36 数组长度小于1。
26 malloc返回错误。

要测试代码覆盖率,运行bash test.sh coverage。要在此部分获得满分,请确保您的测试达到100%覆盖率。(换句话说,您的测试必须使实现的每一行都被执行。)

提交与评分

将您的代码提交到Gradescope上的Project 2A作业。

为确保自动评分器正确运行,请不要在起始代码中添加任何.import语句。同时,请确保您的代码中没有ecall指令。