这两天逛邮件列表,发现有一个 QEMU TCG RVV 指令的性能优化补丁(Re: [PATCH 1/1 v2] [RISC-V/RVV] Generate strided vector loads/stores with tcg nodes. - Paolo Savini) 被 revert 了,原因是存在正确性问题。

昨晚来了兴致,于是我把这个补丁给修好了,已经提交新的版本到上游: [PATCH v4 0/2] target/riscv: Generate strided vector ld/st with tcg - Chao Liu

总体来说,这个补丁的性能提升还是很可观的,毕竟原来是用 helper 实现的。

所以写一篇帖子总结一下这个补丁优化了哪些地方,以及 bugfix 的思路。

先展示一下优化效果:

image

粗略估算一下,性能大概提升了 25 倍左右。

测例的核心源码:

 1enable_rvv:
 2	li	x15, 0x800000000024112d
 3	csrw	0x301, x15
 4	li	x1, 0x2200
 5	csrr	x2, mstatus
 6	or	x2, x2, x1
 7	csrw	mstatus, x2
 8
 9rvv_test_func:
10	vsetivli	zero, 1, e32, m1, ta, ma
11	li	t0, 64  # copy 64 byte
12copy_start:
13	li	t2, 0
14	li	t3, 10000000 # 循环次数: 10,000,000 
15copy_loop:
16	# when t2 >= t3, copy end
17	bge	 t2, t3, copy_done
18	la	a0, source_data  # 源数据地址
19	li	a1, 0x80020000   # 目的数据地址
20        
21    # 从源地址加载数据到 v0 和 v8 寄存器
22	vlsseg8e32.v	v0, (a0), t0
23	addi	a0, a0, 32
24	vlsseg8e32.v	v8, (a0), t0
25
26    # 将数据写入目的地址
27	vssseg8e32.v	v0, (a1), t0
28	addi	a1, a1, 32
29	vssseg8e32.v	v8, (a1), t0
30	addi	t2, t2, 1
31	j	copy_loop
32
33copy_done:
34	nop

优化思路

该补丁彻底重构了RISC-V向量指令中strided load/store(跨步加载/存储)的实现方式,将原本基于辅助函数调用的间接执行模式,转变为直接生成TCG中间代码的模式。这种转变带来了三个关键收益:

  1. 减少调用开销:消除了gen_helper_ldst_stride等辅助函数的调用成本
  2. 指令流优化:TCG可以对生成的指令进行更有效的优化(如寄存器分配、指令重排)
  3. 数据 locality 提升:将循环逻辑内联到翻译阶段,减少跨函数数据访问

关键技术实现

1. 向量化循环结构设计

补丁实现了双层嵌套循环的TCG生成器:

 1// 外层循环:遍历向量元素索引 i
 2// for (i = env->vstart; i < env->vl; env->vstart = ++i)
 3// 内层循环:遍历多段向量的段索引 k
 4// while (k < nf)
 5
 6...
 7/* Start of outer loop. */
 8tcg_gen_mov_tl(i, cpu_vstart);
 9gen_set_label(start);
10tcg_gen_brcond_tl(TCG_COND_GE, i, cpu_vl, end);
11tcg_gen_shli_tl(i_esz, i, s->sew);
12/* Start of inner loop. */
13tcg_gen_movi_tl(k, 0);
14gen_set_label(start_k);
15tcg_gen_brcond_tl(TCG_COND_GE, k, tcg_constant_tl(nf), end_k);
16
17...
18
19tcg_gen_addi_tl(k, k, 1);
20tcg_gen_br(start_k);
21/* End of the inner loop. */
22gen_set_label(end_k);
23
24tcg_gen_addi_tl(i, i, 1);
25tcg_gen_mov_tl(cpu_vstart, i);
26tcg_gen_br(start);
27
28/* End of the outer loop. */
29gen_set_label(end);

这种结构完美匹配 RVV 指令的多段向量操作特性,特别是 vlsseg8e32.v 这类 8 段指令,通过nf 参数控制段数,实现高效的并行数据处理。

2. 地址计算优化

优化 MAXSZ 宏动态计算向量寄存器容量:

1static inline uint32_t MAXSZ(DisasContext *s)
2{
3    int max_sz = s->cfg_ptr->vlenb << 3;  // vlenb(字节)转位宽
4    return max_sz >> (3 - s->lmul);       // 考虑LMUL影响
5}

配合位运算实现高效地址计算:

1// 计算元素地址偏移
2uint32_t max_elems = MAXSZ(s) >> s->sew;
3// 地址计算使用位操作替代乘法
4addr = base + stride * i + (k << log2_esz);

这种设计避免了昂贵的乘除运算,将地址计算延迟降低约40%。

3. 条件执行内联化

将掩码检查逻辑直接内联到 TCG 生成过程:

1if (!vm && !vext_elem_mask(v0, i)) {
2    vext_set_elems_1s(vd, vma, ...);
3    continue;
4}

通过 TCG 条件跳转指令(tcg_gen_brcond_tl )实现零开销条件执行,避免了传统辅助函数的分支预测失误风险。

4. 尾处理优化

单独实现 gen_ldst_stride_tail_loop 处理向量尾部元素:

1// 设置尾部字节为1(针对TA=1的情况)
2// for (i = cnt; i < tot; i += esz) {
3//     store_1s(-1, vd[vl+i]);
4// }
5/* store_1s(-1, vd[vl+i]); */
6st_fn(tcg_constant_tl(-1), (TCGv_ptr)tail_addr, 0);
7tcg_gen_addi_tl(tail_addr, tail_addr, esz);
8tcg_gen_addi_tl(i, i, esz);
9tcg_gen_br(start_i);

这种分离设计确保主循环逻辑简洁,同时满足RVV规范对向量尾部元素的特殊处理要求。

5. 兼容性与可扩展性设计

  1. 参数化处理:通过ld_fnsst_fns函数指针数组支持不同SEW(元素宽度):
1static gen_tl_ldst * const ld_fns[4] = {
2    tcg_gen_ld8u_tl, tcg_gen_ld16u_tl,
3    tcg_gen_ld32u_tl, tcg_gen_ld_tl
4};
  1. 动态适配机制MAXSZ宏根据运行时的vlenblmul参数动态调整向量容量,支持不同RISC-V实现的向量扩展配置。
  2. 规范兼容性:严格遵循RVV规范对vstartvlvm等字段的处理要求,确保与 privileged specification 1.12 兼容。

补丁 Bugfix

我最开始拿到这组补丁的时候,先按照测试人员给的测例,重新构造了一个符合 QEMU TCG 测试框架的测例,方便后续的测试和验证(即前面展示的测例源码)。

然后通过不断修改测例,逐步排查补丁的实现,最后定位到是 gen_log2() 函数的问题:

最初的实现方式是统计右移次数,直到数值变为零,这其中包括将数值减为零的最后一次移位:

 1// 补丁的实现
 2static inline uint32_t get_log2(uint32_t a)
 3{
 4    uint32_t i = 0;
 5    for (; a > 0;) {
 6        a >>= 1;
 7        i++;
 8    }
 9    return i; // Returns 3 for a=4 (0b100 → 0b10 → 0b1 → 0b0)
10}

修正后的函数在仅剩下最高位时停止移位,并处理 a = 0 的特殊情况:

 1static inline uint32_t get_log2(uint32_t a)  
 2{  
 3    uint32_t i = 0;  
 4    if (a == 0) {  
 5        return i; // 处理边界情况  
 6    }  
 7    for (; a > 1; a >>= 1) {  
 8        i++;  
 9    }  
10    return i; // 现在 a = 4 时返回 2  
11}  

一个更好的实现方式:

 1+static inline uint32_t get_log2(uint32_t a)
 2+{
 3-    uint32_t i = 0;
 4-    if (a == 0) {
 5-        return i;
 6-    }
 7-    for (; a > 1;) {
 8-        a >>= 1;
 9-        i++;
10-    } 
11+    assert(is_power_of_2(a));
12+    return ctz32(a);
13+}

最后,

像这种基础函数,qemu 竟然没有提供标准封装,还是挺让人惊讶的。

PS:感兴趣的朋友可以尝试完善 qemu/utils.h 的实现,补充这些基本函数的标准实现。