4. Realm管理扩展基础

好,咱们进入SVE2。说实话,ARMv9最让我兴奋的就是这个扩展。NEON用了这么多年,突然来了个SVE2,很多人第一反应是“又要学新东西了”。但我想说,这玩意儿值得学。

4.1 SVE2与NEON的对比

先说说NEON。NEON是固定宽度的SIMD——128位,你写代码的时候就得知道这个宽度。SVE2呢?它是向量长度无关的。什么意思?你的代码写一次,可以在128位、256位、甚至2048位的硬件上跑,不用改。

特性 NEON SVE2
向量宽度 固定128位 128~2048位,实现定义
寄存器数量 32个(128位) 32个Z寄存器 + 16个P寄存器
Predicate支持 无 有,逐元素控制
Gather/Scatter 有限 原生支持
代码可移植性 需针对宽度调整 向量长度无关

我在一个项目里遇到过这种情况:用NEON写的图像处理库,在Cortex-A72上跑得好好的,换到A76上想用更宽的SIMD,结果得重写。SVE2就解决了这个痛点。

核心差异:NEON是“我知道宽度,我来优化”;SVE2是“我不知道宽度,但代码能自适应”。

4.2 向量长度无关编程

这是SVE2的精髓。你的代码不依赖具体的向量宽度,硬件跑多宽就多宽。怎么做到的?靠的是whilelo指令和Predicate。

举个例子,数组加法:

// NEON版本:固定处理4个float
void add_neon(float *a, float *b, float *c, int n) {
    for (int i = 0; i < n; i += 4) {
        float32x4_t va = vld1q_f32(a + i);
        float32x4_t vb = vld1q_f32(b + i);
        float32x4_t vc = vaddq_f32(va, vb);
        vst1q_f32(c + i, vc);
    }
}

// SVE2版本:向量长度无关
void add_sve2(float *a, float *b, float *c, int n) {
    while (n > 0) {
        svbool_t pg = svwhilelt_b32(0, n);
        svfloat32_t va = svld1_f32(pg, a);
        svfloat32_t vb = svld1_f32(pg, b);
        svfloat32_t vc = svadd_f32_m(pg, va, vb);
        svst1_f32(pg, c, vc);
        a += svcntw();
        b += svcntw();
        c += svcntw();
        n -= svcntw();
    }
}

看到没?svcntw()返回当前硬件能处理多少个float。在128位上是4个,256位上是8个。代码完全不用改。

我的习惯:写SVE2代码时,先假设向量宽度是未知的。用svcnt系列函数获取实际宽度,用whilelo处理边界。这样代码天然可移植。

4.3 Predicate寄存器与Loop Vectorization

Predicate是SVE2的“开关”。每个Predicate位控制对应数据通道是否执行。这玩意儿在循环向量化里太有用了。

想想看,循环的剩余元素怎么处理?NEON里你得写尾循环处理代码。SVE2呢?Predicate直接帮你搞定。

// 处理任意长度的数组,不需要尾循环
void process_any_length(int32_t *in, int32_t *out, int n) {
    svbool_t pg;
    int32_t i = 0;
    
    while (i < n) {
        // 生成有效的Predicate
        pg = svwhilelt_b32(i, n);
        
        // 加载、处理、存储
        svint32_t vec = svld1_s32(pg, in + i);
        svint32_t result = svmul_n_s32_m(pg, vec, 2);
        svst1_s32(pg, out + i, result);
        
        i += svcntw();
    }
}

我曾经在一个音频处理项目里用这个特性。采样率不同,帧长也不同。用SVE2的Predicate,一个函数处理所有情况,省了多少维护工作。

注意:Predicate不是免费的。生成Predicate的指令(如whilelo)有开销。如果循环体很小,这个开销可能抵消向量化的收益。我建议循环体至少4条以上向量指令时再用。

4.4 SVE2的Intrinsic编程

写汇编太痛苦,用Intrinsic吧。ARM提供了完整的SVE2 Intrinsic接口,风格和NEON类似,但更灵活。

命名规则:sv<操作>_<类型>_<变体>

Intrinsic模式 含义 示例
_m 合并模式,受Predicate控制 svadd_f32_m(pg, a, b)
_z 归零模式,Predicate为0的通道置0 svadd_f32_z(pg, a, b)
_x 不关心模式,Predicate为0的通道值未定义 svadd_f32_x(pg, a, b)

我个人习惯用_m模式,因为它最直观——Predicate控制哪些通道写结果。但如果你需要未激活的通道清零,用_z。

// 实际项目中的例子:带条件的向量乘法
svfloat32_t conditional_mul(svbool_t pg, svfloat32_t a, svfloat32_t b, float threshold) {
    // 先比较
    svbool_t cmp = svacgt_f32(pg, a, threshold);
    // 满足条件的做乘法,不满足的保留原值
    return svmul_f32_m(cmp, a, b);
}

避坑指南:我曾经在_x模式上栽过跟头。未定义的值在某些微架构上会导致性能异常。除非你确定不在乎那些通道的值,否则用_m或_z更安全。

知识结构图

SVE2编程知识体系 SVE2编程 与NEON对比 向量长度无关 Predicate寄存器 Intrinsic编程 固定128位 无Predicate whilelo指令 svcnt()函数 逐元素控制 循环向量化 _m/_z/_x模式 命名规则 核心思想:一次编写,任意宽度运行 代码不依赖具体向量宽度,硬件决定实际并行度 图像处理 音频编解码 机器学习推理

实战建议

学SVE2,我建议你从迁移NEON代码开始。找个你熟悉的NEON函数,用SVE2重写。你会发现,大部分情况下代码更简洁,而且不用操心边界处理。

小技巧:GCC和LLVM都支持SVE2 Intrinsic。编译时加-march=armv9-a+sve2。如果你在QEMU上测试,用-cpu max,sve128=on模拟128位SVE2。

嗯,SVE2的内容远不止这些。后面我们会深入具体的指令和优化技巧。先把基础打牢,后面的路就好走了。