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,请检查:
  1. 编译器是否启用自动向量化(-O2已包含);
  2. 数组是否对齐(建议使用posix_memalign分配16字节对齐内存);
  3. N是否足够大以掩盖向量设置开销(建议N≥256)。

关键说明

本案例中vsetvl_e32m1将向量长度设为可用元素数(不超过硬件VLEN/32),vfmacc.vv执行vacc = vacc + va * vb的融合乘加操作。每次循环处理一组元素后,通过vredsum归约指令将向量寄存器内部分量累加为标量。实际工程中可改用vfmacc.vv配合vadd.vv跨迭代累加,避免每轮归约开销,但本案例为清晰展示指令用法,采用每轮独立归约的简化实现。