1. 向量加法内核

任务目标

在RISC-V向量扩展(RVV)环境下实现向量加法内核,将两个长度为N的浮点数组逐元素相加,并与等价的标量实现进行性能对比。通过本案例掌握RVV intrinsics的基本使用方法,并建立可复现的性能测量流程。

环境与依赖

  • 支持RVV的编译器:GCC 12+(-march=rv64gcv)或Clang 15+
  • 模拟器/开发板:QEMU 7.0+(qemu-riscv64)或含RVV的实机(如SiFive U74)
  • 头文件:<riscv_vector.h>(编译器自带)

核心实现

// vec_add.c
#include <riscv_vector.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 4096
#define ITER 1000

// 标量版本
void add_scalar(const float *a, const float *b, float *c, int n) {
    for (int i = 0; i < n; i++)
        c[i] = a[i] + b[i];
}

// RVV向量版本(使用intrinsic)
void add_vector(const float *a, const float *b, float *c, int n) {
    size_t vl;
    for (size_t i = 0; i < n; i += vl) {
        vl = vsetvl_e32m8(n - i);  // 每次处理尽可能多的元素
        vfloat32m8_t va = vle32_v_f32m8(a + i, vl);
        vfloat32m8_t vb = vle32_v_f32m8(b + i, vl);
        vfloat32m8_t vc = vfadd_vv_f32m8(va, vb, vl);
        vse32_v_f32m8(c + i, vc, vl);
    }
}

// 性能测试辅助
double time_ms(void (*func)(const float*, const float*, float*, int),
               const float *a, const float *b, float *c) {
    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);
    for (int i = 0; i < ITER; i++)
        func(a, b, c, N);
    clock_gettime(CLOCK_MONOTONIC, &t1);
    return (t1.tv_sec - t0.tv_sec) * 1000.0 +
           (t1.tv_nsec - t0.tv_nsec) / 1e6;
}

int main() {
    float *a = aligned_alloc(64, N * sizeof(float));
    float *b = aligned_alloc(64, N * sizeof(float));
    float *c = aligned_alloc(64, N * sizeof(float));
    float *c_ref = aligned_alloc(64, N * sizeof(float));

    for (int i = 0; i < N; i++) {
        a[i] = i * 0.5f;
        b[i] = i * 0.25f;
    }

    // 正确性验证
    add_vector(a, b, c, N);
    add_scalar(a, b, c_ref, N);
    int ok = 1;
    for (int i = 0; i < N; i++) {
        if (c[i] != c_ref[i]) { ok = 0; break; }
    }
    printf("Correctness: %s\n", ok ? "PASS" : "FAIL");

    // 性能对比
    double t_scalar = time_ms(add_scalar, a, b, c);
    double t_vector = time_ms(add_vector, a, b, c);
    printf("Scalar: %.3f ms\n", t_scalar);
    printf("Vector: %.3f ms\n", t_vector);
    printf("Speedup: %.2fx\n", t_scalar / t_vector);

    free(a); free(b); free(c); free(c_ref);
    return 0;
}

编译与运行

# 使用QEMU模拟(需先安装qemu-riscv64)
riscv64-linux-gnu-gcc -O3 -march=rv64gcv -mabi=lp64d -static \
    vec_add.c -o vec_add -lm
qemu-riscv64 ./vec_add

# 若在RVV实机上运行,直接编译执行:
# gcc -O3 -march=rv64gcv -mabi=lp64d vec_add.c -o vec_add -lm
# ./vec_add

验证方法与判定标准

  • 正确性:程序输出 Correctness: PASS,表示向量与标量结果完全一致(浮点加法满足结合律,此处N较小且数值简单,可精确比对)。
  • 性能对比:观察 Speedup 值。在QEMU中由于模拟开销,加速比可能接近1或小于1,这属于正常现象;在真实RVV硬件上,预期加速比大于1(通常2~8倍,取决于硬件VLEN和编译器优化质量)。
  • 判定通过:正确性PASS且程序能完整跑完即为功能通过;性能数据仅作记录,不设硬性阈值。

关键说明

  • vsetvl_e32m8 请求使用m8(8个向量寄存器组)配置,每次循环处理尽可能多的元素;vl 为实际处理的元素数,自动处理尾部不足一个寄存器的元素。
  • 使用 aligned_alloc(64, ...) 确保数据对齐,避免跨页边界导致的性能损失。
  • 若编译器不支持 -march=rv64gcv,可尝试 -march=rv64gcv_zvl128b 指定最小向量长度。
  • 本案例仅演示向量加法内核本身,不涉及内存带宽优化(如数据预取)或多核并行,这些属于后续案例内容。