test1.S
文件通俗解释
总体功能:这个文件主要是为了展示一些基本的向量加载、存储和运算操作,演示了如何在向量寄存器中进行简单的向量乘法和加法操作。
代码分析
-
设置向量长度和步长:
li x29, 32
和li x4, 4
设定了一个向量长度(32)和步长(4 字节)。vsetvl x5, x29, x4
设置向量寄存器的长度,以便后续指令知道一次操作多少数据。
-
填充内存:
- 代码接下来有两个循环
loop
和loop2
,分别将一些数据写入到不同的内存地址。 loop
循环从 1 开始计数,把值存储在0x123
偏移的地址位置,递增存储 32 次。loop2
循环从 3 开始计数,把值存储在0x523
偏移的地址位置,递增存储 32 次。
- 代码接下来有两个循环
-
向量加载和运算:
vlw.v
将之前存储在0x123
和0x523
地址的 32 个元素加载到向量寄存器v1
和v2
。vmul.vv v4, v3, v2
执行v3 * v2
向量乘法,结果存储到v4
中。vadd.vv v3, v1, v2
执行v1 + v2
向量加法,结果存储到v3
中。
-
存储结果:
vsw.v
指令将v3
和v4
中的结果分别存储回0x123
和0x523
。
-
终止程序:
- 使用
nop
(空指令)和ebreak
(停止程序)。
- 使用
test2.S
文件通俗解释
总体功能:这个文件是一个更加复杂的向量运算程序,它先初始化一些数据到内存中,然后执行多个向量加载、乘法和加法操作,模拟矩阵或大数组操作。
代码分析
-
设置向量长度和初始化:
li x11, 32
和li x4, 4
设置向量长度为 32 和步长为 4 字节。vsetvl x5, x11, x4
设置向量寄存器的长度。- 初始化循环
init
计算步长并进行计数。
-
填充内存:
loop
循环从 1 开始计数,把数据写入0x623
偏移的内存地址,循环 42 次。loop2
从 3 开始,把数据写入0x423
偏移的地址,循环 42 次。
-
向量运算:
loop3
是主运算循环,包含向量的乘法和加法操作。vlw.v
将数据从0x623
和0x423
加载到向量寄存器v1
和v2
。vmul.vv v3, v1, v2
和vadd.vv v4, v3, v1
分别进行乘法和加法,结果存储在v3
和v4
中。vsw.v
将结果存储回内存。sub x7, x7, x5
更新循环条件,使loop3
继续运行,直到所有元素都处理完。
-
终止程序:
- 使用
nop
和ebreak
。
- 使用
高级“反编译”解释
如果将这两个 .S
文件转换为更高级的伪代码,它们可以类似于如下的 C 语言代码:
// 设置向量长度为 32,步长为 4
const int VECTOR_LENGTH = 32;
const int STEP = 4;
int memory1[32], memory2[32];
// 初始化 memory1 和 memory2
for (int i = 0; i < VECTOR_LENGTH; i++) {
memory1[i] = i + 1; // 1, 2, 3, ..., 32
}
for (int i = 0; i < VECTOR_LENGTH; i++) {
memory2[i] = 3 * (i + 1); // 3, 6, 9, ..., 96
}
// 向量加载并进行向量运算
int v1[VECTOR_LENGTH], v2[VECTOR_LENGTH], v3[VECTOR_LENGTH], v4[VECTOR_LENGTH];
for (int i = 0; i < VECTOR_LENGTH; i++) {
v1[i] = memory1[i];
v2[i] = memory2[i];
}
for (int i = 0; i < VECTOR_LENGTH; i++) {
v4[i] = v1[i] * v2[i]; // 向量乘法
v3[i] = v1[i] + v2[i]; // 向量加法
}
// 存储结果回内存
for (int i = 0; i < VECTOR_LENGTH; i++) {
memory1[i] = v3[i];
memory2[i] = v4[i];
}
// 设置向量长度为 32,步长为 4
const int VECTOR_LENGTH = 32;
const int STEP = 4;
int memory1[42], memory2[42];
// 初始化 memory1 和 memory2
for (int i = 0; i < 42; i++) {
memory1[i] = i + 1; // 1, 2, 3, ..., 42
}
for (int i = 0; i < 42; i++) {
memory2[i] = 3 * (i + 1); // 3, 6, 9, ..., 126
}
// 主运算循环
while (remaining_elements > 0) {
// 向量加载
int v1[VECTOR_LENGTH], v2[VECTOR_LENGTH], v3[VECTOR_LENGTH], v4[VECTOR_LENGTH];
for (int i = 0; i < VECTOR_LENGTH; i++) {
v1[i] = memory1[i];
v2[i] = memory2[i];
}
// 向量运算
for (int i = 0; i < VECTOR_LENGTH; i++) {
v3[i] = v1[i] * v2[i]; // 向量乘法
v4[i] = v3[i] + v1[i]; // 向量加法
}
// 存储结果回内存
for (int i = 0; i < VECTOR_LENGTH; i++) {
memory1[i] = v3[i];
memory2[i] = v4[i];
}
remaining_elements -= VECTOR_LENGTH; // 更新剩余元素数
}
test1.S
通俗说明
这个代码文件展示了如何使用向量指令将两个数列加载到向量寄存器里,进行向量加法和乘法,然后把结果写回内存。可以把它看成一个向量的基本操作练习。
-
设置向量的长度和类型:
- 程序一开始会设置“向量的长度”为 32 个元素(每个元素 4 字节,即 32 位整数)。
- 这种设置是为了告诉后面的向量指令,处理的数据是一个长度为 32 的向量,而不是逐个元素处理。
-
初始化两个数列并存入内存:
- 代码定义了两个循环
loop
和loop2
,分别把数据写入到内存的不同位置:- 第一个循环 (
loop
):从 1 开始逐渐加 1,依次存储 32 个数。 - 第二个循环 (
loop2
):从 3 开始逐渐加 3,依次存储 32 个数。
- 第一个循环 (
- 这两个数列被存储在内存中不同的偏移位置(
0x123
和0x523
),这是为了之后加载到向量寄存器时,可以区分它们。
- 代码定义了两个循环
-
将数列加载到向量寄存器:
vlw.v
指令从内存中读取数据,将第一个数列加载到v1
,第二个数列加载到v2
。- 这时候,
v1
和v2
中各自存储了 32 个数,代表两个不同的数据序列。
-
向量加法和乘法:
- 向量乘法:
vmul.vv
指令将v3
和v2
中的每个元素一一相乘,结果存入v4
。 - 向量加法:
vadd.vv
指令将v1
和v2
中的每个元素一一相加,结果存入v3
。 - 这些运算展示了向量指令的威力,它可以在一次指令中处理 32 个数据,而不是单独处理每一个元素。
- 向量乘法:
-
存储计算结果:
- 计算完的结果被存储回内存:
v3
的结果存入0x123
,v4
的结果存入0x523
。 - 这样一来,内存中原来存储的数列就被替换成了计算后的加法和乘法结果。
- 计算完的结果被存储回内存:
总结:这个程序通过一个简单的例子,展示了如何用向量指令高效地将两个数列相加和相乘,同时用较少的指令完成多个数据的操作。
test2.S
通俗说明
这个代码文件比 test1.S
复杂得多,主要是实现了一组向量的初始化、加载、加法和乘法操作。可以把它看成一个复杂的向量运算练习,涉及了多层循环和更复杂的数据操作。
-
设置向量的长度和初始化循环:
- 程序一开始还是设置向量长度为 32,并且有一个小循环
init
,设置初始值(通过步长计算)。 - 这里的初始化操作并不复杂,目的是为后面更复杂的循环做准备。
- 程序一开始还是设置向量长度为 32,并且有一个小循环
-
在内存中存储两个数据序列:
- 第一个循环 (
loop
):从 1 开始,每次加 1,存储 42 个数据到内存位置0x623
。 - 第二个循环 (
loop2
):从 3 开始,每次加 3,存储 42 个数据到内存位置0x423
。 - 这些数据的存储位置和数量与
test1.S
不同,这次它们存储的是长度为 42 的数据序列。
- 第一个循环 (
-
加载并计算向量数据:
loop3
是主要的运算循环,它包含了加载、乘法和加法操作。- 在
loop3
中:- 使用
vlw.v
加载两个数据序列,分别存入向量寄存器v1
和v2
。 - 然后执行向量乘法和加法操作,将结果分别存入
v3
和v4
。
- 使用
-
将结果存储回内存:
- 计算得到的结果被存储回内存,
v3
存入0x623
,v4
存入0x423
。 sub x7, x7, x5
会减少剩余的元素数量,以便loop3
可以继续处理下一个 32 长度的向量,直到所有 42 个元素处理完。
- 计算得到的结果被存储回内存,
-
控制循环:
loop3
的循环条件通过x7
和x5
控制,确保所有数据都被处理。- 每次执行完循环后,剩余元素数减少,当剩余元素数小于等于 0 时,循环结束。
总结:这个程序通过复杂的多层循环和条件控制,展示了如何使用向量指令处理更大的数据集合。相比 test1.S
,这个文件的逻辑更复杂,显示了如何在内存中初始化、加载和计算较大规模的数据。
任务分析
-
核心目标:
- 将 RVV(RISC-V Vector Extension) 指令集集成到 QtRVSim 模拟器中,以支持向量操作。
- 确保 RVV 指令在命令行模式下能够正确运行。
- 通过优化指令和减少指令周期数来提高分数。
-
不需要修改 GUI:
- 这个项目不要求你修改 GUI(图形用户界面)部分的代码。
- 你的重点应放在模拟器核心功能的实现和命令行测试上。
-
减少指令周期数:
- 尽可能减少 RVV 指令的执行周期,以提高项目分数。
- 通过优化指令执行流程,比如“链式操作”或“向量归约”,可以减少执行时间。
实现步骤
1. 理解 RVV 指令集
- RVV 指令主要用于处理向量数据,能够一次性操作多个数据项,这在矩阵运算等并行任务中非常高效。
- RVV 指令的基本操作包括:
- 向量加载/存储:如
vlw.v
(向量加载)和vsw.v
(向量存储)。 - 向量算术运算:如
vadd.vv
(向量加法)和vmul.vv
(向量乘法)。
- 向量加载/存储:如
- 项目要求我们实现并测试这些指令,确保它们能够在命令行模式下正常工作。
2. 选择需要修改的文件
根据项目提示,以下文件可能需要修改:
-
src/machine/instruction.cpp
:- 在这里添加新的 RVV 指令的定义,包括指令名称、类型、机器码和掩码。
- 可以通过查看其他已定义的指令作为参考。
-
src/machine/core.cpp
:- 该文件包含模拟器的主流水线(取指、译码、执行、写回、内存访问)。
- 在适当的位置调用
state.cycle_count++
来增加指令的周期数。 - 你可能需要在“执行”阶段添加对 RVV 指令的处理逻辑。
-
src/machine/execute/alu.cpp
:- 实现每条 RVV 指令的具体操作,例如向量加法、向量乘法等。
- 每个指令对应一个函数或一段代码,用来执行指令所需的操作。
3. 编写 RVV 指令的执行逻辑
在 core.cpp
和 alu.cpp
中实现每条 RVV 指令的执行逻辑。例如:
- 加载/存储指令:定义如何从内存加载数据到向量寄存器,或将向量寄存器的数据存储到内存中。
- 加法/乘法指令:定义如何对两个向量寄存器进行加法或乘法运算,并将结果存储到目标向量寄存器。
4. 优化指令执行
- 通过引入“链式操作”和“向量归约”来减少周期数:
- 链式操作:让下一条指令能够在当前指令部分完成时立即开始,而无需等待当前指令完全结束。
- 向量归约:优化向量数据求和的操作,减少不必要的操作周期。
5. 测试你的实现
-
在命令行模式下测试 RVV 指令,使用现有的
.S
测试文件(如test1.S
和test2.S
)。 -
运行命令行进行测试:
./target/qtrvsim cli --asm tests/test1.S ./target/qtrvsim cli --asm tests/test2.S
-
通过命令行运行这些测试文件,检查是否输出正确的结果,同时记录周期数。
6. 分析和改进
- 如果周期数较高,检查指令执行流程,是否有可以优化的部分。
- 通过减少不必要的指令或周期数,可以提高效率,获得更高分数。
项目完成后的检查
- 确保 RVV 指令正确实现:测试向量加载、存储、加法和乘法指令是否正常工作。
- 优化周期数:确保指令执行的周期数尽可能少。
- 编写报告:在报告中展示测试结果,并解释周期数的来源以及优化的地方。