3. 向量加载与存储指令
任务目标
使用RISC-V向量扩展指令集中的向量乘加指令(vfmacc.vv)实现单精度浮点向量的点积运算,并与标量实现进行性能对比。通过本案例,掌握向量乘加指令的编码格式、寄存器组使用方式,以及如何通过循环展开减少向量指令开销。
环境依赖
本案例基于 RISC-V Vector Extension v1.0,需要支持V扩展的编译器与模拟器。推荐使用:
- 编译器:GCC 12+(
-march=rv64gcv)或Clang 15+ - 模拟器:QEMU 7.0+(
-cpu rv64,v=true)或Spike(--isa=rv64gcv) - 验证平台:Linux环境,支持
time命令计时
核心实现
以下代码实现长度为N的单精度浮点数组的点积运算。向量版本使用vfmacc.vv指令在单个向量寄存器中完成乘累加,每次处理VLEN/32个元素;标量版本作为性能对比基线。两个函数均通过rdtime指令(映射为cycle())统计时钟周期数。
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <riscv_vector.h>
#define N 1024 // 向量长度,可调整
// 读取时钟周期数(RISC-V rdtime指令)
static inline uint64_t cycle(void) {
uint64_t c;
asm volatile("rdtime %0" : "=r"(c));
return c;
}
// 标量点积实现
float dot_scalar(const float *a, const float *b, int n) {
float sum = 0.0f;
for (int i = 0; i < n; i++) {
sum += a[i] * b[i];
}
return sum;
}
// 向量点积实现(使用vfmacc.vv)
float dot_vector(const float *a, const float *b, int n) {
float sum = 0.0f;
int i = 0;
size_t vl;
// 每次处理VLEN/32个元素(VLEN=128时vl=4)
while (i < n) {
vl = vsetvl_e32m1(n - i); // 设置向量长度
vfloat32m1_t va = vle32_v_f32m1(&a[i], vl);
vfloat32m1_t vb = vle32_v_f32m1(&b[i], vl);
vfloat32m1_t vacc = vfmv_v_f_f32m1(0.0f, vl); // 清零累加器
vacc = vfmacc_vv_f32m1(vacc, va, vb, vl); // 乘加:vacc += va*vb
sum += vfmv_f_s_f32m1_f32(vredsum_vs_f32m1_f32m1(vacc, vfmv_v_f_f32m1(0.0f, vl), vl), vl);
i += vl;
}
return sum;
}
int main() {
// 初始化测试数据
float *a = (float*)malloc(N * sizeof(float));
float *b = (float*)malloc(N * sizeof(float));
if (!a || !b) return 1;
for (int i = 0; i < N; i++) {
a[i] = (float)(i % 7) * 0.5f;
b[i] = (float)(i % 5) * 0.25f;
}
// 预热(确保缓存命中)
volatile float warm = dot_scalar(a, b, N);
(void)warm;
// 标量计时
uint64_t t0 = cycle();
float r_scalar = dot_scalar(a, b, N);
uint64_t t1 = cycle();
// 向量计时
uint64_t t2 = cycle();
float r_vector = dot_vector(a, b, N);
uint64_t t3 = cycle();
// 验证结果一致性
float diff = r_scalar - r_vector;
if (diff < 0) diff = -diff;
int pass = (diff < 1e-3) ? 1 : 0;
printf("Scalar cycles: %lu\n", (unsigned long)(t1 - t0));
printf("Vector cycles: %lu\n", (unsigned long)(t3 - t2));
printf("Speedup: %.2fx\n", (double)(t1 - t0) / (double)(t3 - t2));
printf("Scalar result: %f\n", r_scalar);
printf("Vector result: %f\n", r_vector);
printf("Test %s\n", pass ? "PASSED" : "FAILED");
free(a);
free(b);
return pass ? 0 : 1;
}
编译与运行
# 编译(需支持V扩展的GCC)
gcc -O2 -march=rv64gcv -mabi=lp64d -o dot_product dot_product.c
# 在QEMU中运行
qemu-riscv64 -cpu rv64,v=true ./dot_product
# 或在Spike中运行
spike --isa=rv64gcv pk ./dot_product
验证方法
程序输出包含以下关键信息:
- 正确性验证:比较标量与向量计算结果,两者误差小于
1e-3时输出PASSED。由于浮点运算顺序不同,微小舍入差异属正常现象。 - 性能对比:观察
Speedup值。在VLEN=128位配置下,向量版本理论上可获得约2~4倍加速(受限于内存带宽和循环开销)。若加速比接近1或小于1,请检查:
- 编译器是否启用自动向量化(
-O2已包含); - 数组是否对齐(建议使用
posix_memalign分配16字节对齐内存); - N是否足够大以掩盖向量设置开销(建议N≥256)。
关键说明
本案例中vsetvl_e32m1将向量长度设为可用元素数(不超过硬件VLEN/32),vfmacc.vv执行vacc = vacc + va * vb的融合乘加操作。每次循环处理一组元素后,通过vredsum归约指令将向量寄存器内部分量累加为标量。实际工程中可改用vfmacc.vv配合vadd.vv跨迭代累加,避免每轮归约开销,但本案例为清晰展示指令用法,采用每轮独立归约的简化实现。