RVV Benchmark实战:从环境到选型的关键解读 📅 发布时间:2026/8/28 11:19:56 👁 浏览次数: RVV benchmark 最近讨论度明显上来了尤其是 SiFive P870、Lanxin LX500、Epic Semi Contrail AIx 这三类 RISC-V 处理器被放在一起对比的时候。它们都支持 RISC-V 向量扩展 RVV但定位差得很远有的是通用高性能 IP有的是面向 AI 的 SoC有的还停留在评估板阶段。很多人看到一份 benchmark 跑分就想直接得出“谁强谁弱”的结论实际上这条路极其容易踩坑。这篇内容主要拆我实际跑 RVV 基准测试时的完整思路环境准备、最小样例、参数解释、结果解读再到选型落地。适合两类读者——刚拿到支持 RVV 的 RISC-V 设备、想验证向量性能的人以及想通过 benchmark 做处理器选型的人。先给结论跑 RVV 测试真正难的不是把代码跑起来而是保证所有对比条件都对齐并且能解释清楚分数背后的瓶颈。1. 先搞清楚三款芯片在 benchmark 里到底比什么1.1 RVV 基准测试不是简单跑个分RVV 是 RISC-V 的向量扩展。和 x86 AVX、ARM NEON 最大的区别是RVV 的向量长度 VLEN 不是编译器固定的而是由硬件实现的。同一个二进制在不同 VLEN 的芯片上执行的元素数可能完全不同。这带来一个核心变化代码必须写成“每次循环都不知道向量长度、但都能正确运行”的形式。一般通过 vsetvli 指令在运行时把当前可用的向量长度读出来然后按这个长度切片处理。所以一份 RVV benchmark 跑出来的分数至少包含三层信息指令集实现完整度是否完整实现了 RVV 1.0还是只实现了部分指令。微架构执行效率向量流水线多宽、能不能乱序执行、寄存器堆冲突大不大、访存带宽够不够。编译器生成质量同一段 C 代码GCC、Clang、版本不同生成汇编差异很大。只看总分这三层完全混合在一起很容易得出错误结论。比如一个芯片向量单元很强但编译器生成代码很差分数会低得离谱另一个芯片编译器优化很激进但微架构一般分数反而可能很高。所以真正有效的 benchmark不是报一个总分而是把这几层拆开来看。1.2 这三家芯片各自的定位SiFive P870RISC-V 高性能应用处理器 IP面向数据中心、边缘计算、网络设备这类场景支持 RVV 1.0通常被授权给下游厂商做 SoC。Lanxin LX500国产 RISC-V 应用处理器产品线覆盖工控、嵌入式、服务器方向的设备。具体型号、核数、频率差异要看实际开发板。Epic Semi Contrail AIx面向 AI 推理计算的 RISC-V 处理器除了 RVV 之外还有可能包含自定义扩展和专用加速单元。关键提醒这三家厂商提供的开发板、样片、频率、缓存、内存配置差别很大。同样是基于 P870 的 SoC可能是不同的核数、不同频率、不同内存控制器。跑 benchmark 之前先把设备信息确认清楚。一般可以这样做cat /proc/cpuinfo | grep -E processor|isa|mhz|model重点看 isa 一栏里有没有字母v。如果有说明当前 Linux 识别到 RISC-V 向量扩展如果内核比较老或者没配置可能识别不到。跑分之前不确认这一点后面所有的“Illegal instruction”报错都会让你摸不着头脑。注意如果/proc/cpuinfo里看不到v先别急着怪代码很可能是内核没开启 CONFIG_RISCV_ISA_V或者 bootloader 没有配置相关 CSR。主线内核到 6.5 之后才把用户态向量上下文切换补得比较完整老内核跑 RVV 程序可能不报错但在任务切换或信号处理时容易出问题。2. 跑 RVV Benchmark 之前先把环境对齐2.1 工具链版本决定了一半结果RVV 基准测试最容易忽略的是工具链。RVV 1.0 在 2021 年定稿之后GCC 和 LLVM 的适配花了很长时间。GCC 到 13、LLVM 到 16 之后RVV 1.0 的 intrinsics 才算是比较可用的状态。如果你的工具链还是 GCC 11 或 Clang 13很可能用的还是 0.7.1 草稿版本的 intrinsics编译代码会有大量名称差异。RVV 0.7.1 和 RVV 1.0 的 intrinsics 不兼容。比如早期常见的vle32_v_f32m4这类写法在 1.0 里有但很多函数名、mask 参数、返回类型都改过。所以拿到 benchmark 代码后第一步不是改性能而是确认工具链riscv64-unknown-linux-gnu-gcc -v echo #include riscv_vector.h int main(){return 0;} | riscv64-unknown-linux-gnu-gcc -marchrv64gcv -mabilp64d -x c - -o /tmp/vec_test 21如果能编译通过并生成可执行文件基本说明头文件和编译选项匹配。如果报错找不到riscv_vector.h说明工具链太老或者没有把向量 intrinsics 头文件装全。交叉编译器命名可能不同常见的有riscv64-unknown-linux-gnu-gccriscv64-linux-gnu-gcc厂商 SDK 自带的改版 GCC建议统一记录 gcc 版本、-march、-mabi三个信息。对比不同芯片时最好用同一套工具链和同一组参数否则跑分差异说不清楚是芯片的差异还是编译器的差异。2.2 优先在真实设备上跑QEMU 只用来调试如果还没有真实硬件可以用 QEMU 先做正确性验证qemu-riscv64 -cpu rv64,vtrue ./saxpy_rvv但 QEMU 是解释执行时间性能没有参考意义只能验证“代码逻辑是否正确”。等到真机到手再用同样的二进制或源码重跑。如果你有真实开发板先检查用户态向量支持。RISC-V 的向量上下文切换在 Linux 内核里经历了较长演化早期内核只在内核态用过向量用户态使用会有风险。现在主线内核一般没问题但发行版内核不一定同样配置。简单判断方式就是/proc/cpuinfo的 isa 里有没有v以及跑一个 10 万次的向量循环加多线程加信号处理观察是否出现异常。2.3 先把数据规模、迭代次数、随机种子定死跑分对比最怕“各自跑各自方便的数据大小”。比如一个芯片 L2 cache 很大刚好把测试数据全部装进 cache分数自然好看另一个芯片 L2 较小被迫频繁访问内存分数就难看。这不代表芯片向量单元差。我一般会固定三组数据规模小于 L1 cache测纯向量计算能力。略大于 L2 cache测缓存和访存配合。远大于 L2 cache测内存带宽和真实工作负载。每一组数据规模跑 10 次以上取中位数记录最大最小值。直接取第一次时间经常不稳定因为会有冷缓存、动态频率、后台任务等影响。如果一份 benchmark 报告只写“跑了几秒”却不写数据规模、L1/L2 大小关系、频率和内核版本那这份报告基本没法复现。3. 从最小样例开始手写一个 RVV 微基准3.1 一个能跑通的最小 SAXPYSAXPY 是几乎所有向量 benchmark 的起手式y a * x y。代码量少方便观察汇编也方便在不同编译器之间对比。用 RVV intrinsics 写一个最小版本#include riscv_vector.h #include stddef.h void saxpy_rvv(float *y, const float *x, float a, size_t n) { size_t i 0; while (i n) { size_t vl vsetvlmax_f32m4(); vfloat32m4_t vx vle32_v_f32m4(x i, vl); vfloat32m4_t vy vle32_v_f32m4(y i, vl); vfloat32m4_t vr vfma_vf_f32m4(vy, a, vx, vl); vse32_v_f32m4(y i, vr, vl); i vl; } }这里有几个点要解释vsetvlmax_f32m4()的作用是拿到当前硬件支持的最大向量元素数量SEW 是 32 位 floatLMUL 是 4。vle32_v_f32m4从内存加载向量vse32_v_f32m4存回内存。vfma_vf_f32m4(vy, a, vx, vl)计算 vy vy a * vx这是 fused multiply-add一次指令完成乘加也是向量峰值 FLOPS 的计算基础。循环里必须用返回值vl来切片处理不能假设每次一定是固定长度。为什么用 LMUL4 而不是 1LMUL 表示把几个向量寄存器组合成一个更大的向量组可以一次处理更多元素减少指令数。但如果寄存器压力过大编译器可能把 LMUL8 的代码展开成多次 spill性能反而不如 LMUL2。所以不同芯片上最优 LMUL 可能不一样这也是 benchmark 里要单独测的一项。3.2 编译、运行、确认真的生成了向量指令交叉编译命令riscv64-unknown-linux-gnu-gcc -O3 -marchrv64gcv -mabilp64d -static -o saxpy_rvv saxpy_rvv.c如果是在 RISC-V 真机上本地编译命令类似gcc -O3 -marchnative -o saxpy_rvv saxpy_rvv.c-marchnative在 RISC-V 工具链上不一定保证正确识别比较稳妥的还是手动写成-marchrv64gcv。注意如果芯片还支持额外扩展比如压缩指令c、位操作zba/zbb/zbs可以进一步调整但基础就先用gcv。编译完之后用 objdump 看一下汇编确认循环体内真的出现了向量指令riscv64-unknown-linux-gnu-objdump -d saxpy_rvv | grep -E vsetvli|vle32|vse32|vfmacc如果发现循环里全部是标量 load/store 和fadd、fmul说明编译器没有向量化或者-march没带上v或者被别的选项干扰了。这一步非常关键不要只看运行时间就开始调优要先确认在测的确实是向量代码。3.3 正确性验证跑得快但结果错更可怕手写向量代码最容易出问题是边界处理。上面这个简化版代码在 n 不是 vl 整数倍时会越界读写所以实际测试代码要加边界处理或者把 n 定成 vl 的整数倍。更稳妥的写法是在循环后面补一个标量尾巴for (; i n; i) { y[i] a * x[i]; }验证正确性时建议用随机数初始化把向量版本的结果和标量版本的结果对比允许一定浮点误差diff (./saxpy_scalar) (./saxpy_rvv)或者直接在程序里算 checksum 打印出来两个版本对得上才算通过。这里有两点要注意浮点加法顺序变化会导致结果不完全一致所以比较时用相对误差或绝对误差阈值不要直接判等另外尽量用多个随机种子测多轮而不是只测一组固定输入。4. 影响 RVV 跑分的关键参数4.1 VLEN、SEW、LMUL 的关系VLEN 是硬件定死的比如 128、256、512 位都有可能。SEW 是单个元素的位宽float 是 32double 是 64。一次向量指令处理的元素个数由 SEW、LMUL、VLEN 共同决定。可以理解成每个向量寄存器组能装的元素数等于 VLEN 除以 SEW而 LMUL 通过组合多个寄存器来扩大这个数量。LMUL 越大单条指令处理元素越多代码中指令数越少但不是越大越好。LMUL 增大后可用的向量寄存器组数量变少编译器在循环展开、软件流水时更容易受限。而且某些微架构对高位 LMUL 的寄存器读写存在额外开销。跑分时要做的不是“固定一个 LMUL”而是针对同一段内核分别测 LMUL1、2、4、8找到当前芯片上最优值。这个最优值会随数据规模和访存特性变化所以报告里要把 LMUL 和 VLEN 一起写出来。只写“GFLOPS”不写参数组合的结果基本没有参考价值。4.2 自动向量化 vs 手写 intrinsics 的两条路线大部分内核代码不需要手写 intrinsic。新的 GCC 和 Clang 都能做 RISC-V 自动向量化用法跟 x86/ARM 类似gcc -O3 -marchrv64gcv -ftree-vectorize -fopt-info-vec saxpy.c -o saxpy_auto-fopt-info-vec会输出哪些循环被向量化。如果某个循环没有被向量化再看原因常见的有循环携带依赖比如a[i] a[i-1] * 2。指针可能重叠编译器保守处理。可以用restrict声明告诉编译器。循环次数在编译期未知但运行期很小编译器认为不值得向量化。涉及复杂控制流、函数调用、间接寻址。手写 intrinsics 的优势是确定性强知道每条指令在做什么。缺点是代码冗长还要注意不同 RVV 版本的兼容性。我的建议是先用自动向量化拿到基线再用 intrinsic 优化内层循环最后用 objdump 确认差异不要一开始就全盘手写。尤其在做三款芯片横向对比时先让同一份源文件都有自动向量化版本这样对比起来更容易解释。4.3 访存密集任务先算清楚是计算密集还是带宽密集很多 RVV benchmark 表面上在测向量计算实际瓶颈是内存带宽。比如 SAXPY 每个元素只需要一次乘加却要做两次 load 和一次 store访存压力远大于计算压力。这种情况下不管芯片把向量单元做得再强跑分都会受限于 DRAM 带宽。可以先做个粗算。假设单核峰值算力是 16 GFLOPS但测试时发现实际只能跑到 4 GFLOPS大概率是访存瓶颈。判断方法是把数据规模缩小到 L1 cache 能容纳的范围内重测如果性能大幅提升说明内存带宽是瓶颈如果没有明显变化说明向量单元和指令流水线才是瓶颈。可以顺便看一下 roofline 模型峰值算力除以数据访问量得到理论最优算力。如果实际跑分远低于这个理论值再逐步排查访存、循环展开、缓存复用和调度问题。这个思路在 AI 推理场景里特别重要像 GEMM、卷积这类算子能否把数据留在寄存器里反复使用直接决定性能。注意先判断瓶颈类型再决定优化手段。计算密集的任务优化 LMUL 和展开访存密集的任务优化数据布局、分块和预取。顺序反了调半天参数分数也不会动。5. 结果解读和常见误区5.1 单核分数和多核分数不能混在一起谈厂商宣传里经常出现“XX TOPS”或“XX GFLOPS”这通常是所有核、所有向量单元同时跑满、最高频率、理想指令组合下的峰值。实际单核跑分和多核扩展完全不同。看 benchmark 报告时先看三点单核向量吞吐代表单核执行向量指令的能力。多核扩展比4 核跑到 3.6 倍、8 核跑到 6 倍这个扩展性很重要。多核加访存压力多核同时跑向量任务时共享内存带宽会被瓜分可能出现跑分下降。RISC-V 芯片在核间互联、缓存一致性、内存控制器上的差异非常大。所以建议把单核测试和多核测试分开记录不要只给一个“全核总分”。5.2 峰值算力不等于实际吞吐峰值算力的计算公式大致是单核峰值 FLOPS 频率 × (VLEN / 32) × 2 × 每周期 FMA 数量比如 VLEN128、频率 2GHz、每周期执行一组 FMA峰值就是 2GHz × 4 × 2 16 GFLOPS。如果芯片每周期能执行更多 FMA 单元峰值会更高。但实际跑不出峰值是常态。常见损耗因素包括循环尾巴和边界处理。数据加载和存储不能完全隐藏。缓存未命中和 TLB miss。频率动态降频。其他核竞争内存带宽。编译器没有生成最优的指令调度。我一般会算一次“实测峰值”也就是用最佳小程序、最佳 LMUL、数据规模压在 L1/L2 里跑出的最大 GFLOPS。这个值比理论峰值更有参考意义。芯片选型时真正要关注的是实际 workload 里的吞吐而不是理论峰值。5.3 常见问题排查顺序现象优先排查常见原因运行报 Illegal instruction/proc/cpuinfo、内核版本CPU 或内核没开启 V 扩展编译失败找不到riscv_vector.h工具链版本GCC 版本太老或缺少 intrinsics 头文件编译通过但汇编没有向量指令objdump 检查-march缺少v或优化级别过低结果和标量版本不一致对比误差范围浮点加法顺序变化或边界越界性能波动大频率、缓存、后台负载DVFS、热降频、任务调度不同设备分数差异巨大确认频率和数据规模频率不同、cache 大小不同、内存配置不同排查顺序有一个基本原则先看现象再看输入数据再看环境最后才怀疑工具本身。很多人跑出异常分数第一反应是“编译器不行”或“芯片不行”实际上大部分情况是参数没对齐。6. 从跑分到选型落地6.1 横向对比前必须固定这些条件如果你想拿同一套 benchmark 对比 SiFive P870、Lanxin LX500、Epic Semi Contrail AIx建议在文档里固定以下条件编译器同一版本同一-march、-mabi、优化选项。数据规模同一大小并分别覆盖 L1、L2、主存三档。迭代次数相等或足够大到消除启动开销。