打印
[G32R]

Helium在G32R501上的实战:一步步见证向量并行计算的威力!

[复制链接]
394|11
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主

[i=s] 本帖最后由 kai迪皮 于 2025-7-14 20:58 编辑 [/i]<br /> <br />

1. 背景

在 Arm 处理器的大家族中,Cortex-A 系列因其高性能、高主频常被用于智能手机、平板电脑或高端应用处理器。而在以嵌入式、物联网为主的 Cortex-M 系列中,“低功耗、成本敏感、实时性”是更重要的需求。如果在 Cortex-M 设备上做大量数字信号处理、机器学习,过去往往只能依赖:

  1. 标量浮点单元(FPU):一次只能对一个浮点数做运算;
  2. 一些编译器层面的 loop-unrolling、优化,让标量运算稍微提速;
  3. 若需要真正的 SIMD 并行,则很难在 Cortex-M 里找到现成的硬件方式。

然而,Arm 在 2020 年左右推出了“Helium”——这是一套专门面向 M Profile 的向量扩展(MVE,M-Profile Vector Extension)。它为像 Cortex-M52、M55、M85 这样的内核带来了真正的 SIMD 特性,使得一次加载可处理 4 个 32 位浮点数或更多定点数据,并且提供了丰富的指令(vld/vmul/vadd/vsub/vst等)。这样,一颗Cortex-M MCU就能接近 Cortex-A 上 Neon 指令那种“单指令多数据”的高并行度,极大提升处理效率。


2. 为什么不直接把Neon搬过来?

谈到 SIMD 并行,大家或许首先想到的是 Neon 指令集——Neon 在Cortex-A及部分Cortex-R处理器里十分常见,广泛用于多媒体、图像视频处理和机器学习。那为何 Arm 不把 Neon 直接塞进 Cortex-M 就完事了?

2.1 Neon 是什么?

Neon 是 Arm 针对 Cortex-A(以及部分 Cortex-R)推出的 SIMD 指令集,用于如手机 SoC 的多媒体、图像/视频处理、机器学习推理等繁重运算。Neon 要求较大的硬件和寄存器资源,且通常在运行 Linux/Android 这类操作系统的平台上,能充分发挥高吞吐率、多线程流水线调度的优势。

2.2 Helium(MVE)为何另起炉灶

Cortex-M 作为 MCU,面积更小、功耗更低、且对实时中断响应要求严苛。如果把 Neon 全搬过来,可能会带来:

• 资源占用和成本显著增加; • 功耗不符合 MCU 级需求; • 复杂的流水线与中断机制可能冲突。

因此,Arm 重新定义了更“轻巧”的向量扩展——Helium (MVE),在保留 SIMD 并行优势的同时,兼顾 Cortex-M 的低功耗与实时特质。

2.3 Helium 与 Neon 的相似与差别

  • 相同点:
  • 都是 128-bit SIMD;
  • 都能批量处理多路数据,如同时处理 4 个 float;
  • 指令类型相似,如 vld、vst、vmul、vadd 等。
  • 不同点:
  • Helium 更贴合 M Profile 的堆栈、上下文切换;
  • 使用谓词寄存器(tail predication),方便应对剩余元素不整除的情形;
  • 面向低功耗与小型 MCU 环境进行了特殊优化。

总的来说,Neon 面向“大块头”的 Cortex-A,而 Helium (MVE) 为“小巧”的 Cortex-M52 或 M55 等内核而生。二者血脉相承,却分工不同。


3. 设置应用场景:基于矩阵元素运算

为了让 Helium 的效果更直观,我们选了一个常见场景:对矩阵(或向量)里每个元素做运算,比如:

  • 元素自乘:A[i] = A[i] × A[i];
  • 两个矩阵 A[i] 与 B[i] 对应元素相乘,写入 C[i];
  • A[i] = scaleFactor × A[i] 等缩放操作。

这些“逐元素乘法”在图像滤波、音频处理、神经网络激活函数中常见。若矩阵规模较大,仅靠标量 FPU 循环就会效率偏低;如果能同时处理 4 个浮点数,就能让【总循环数】大幅减少,真正凸显 SIMD 并行优势。

3.1 代码层面:两个版本的对比目标

我们将在下文分别展示:

  • 标量版本(FPU 模式):朴素的 while 循环,每次只乘 1 个数;
  • MVE 向量版本:手写 Helium Intrinsics,让编译器明确生成向量指令。

通过编译并在 G32R501 上实际运行,我们能清晰看到这两种方法在不同时期、不同编译优化条件下的性能表现。


4. 代码实现

在CMSIS-DSP库里,你可能会看到类似下面的函数原型“arm_mult_f32”。这里我们把arm_mult_f32拆分成两个函数以便可以同时在G32R501平台上运行: arm_mult_f32_fpu 与 arm_mult_f32_mve。

在正式开始我们的测试之前,我们要安装好CMSIS-DSP pack,里面有一些我们所使用到的内容,我们引用该pack可以让我们少做很多“轮子”。

image-20250714201631046.png

4.1 标量循环:arm_mult_f32_fpu

这是一个常见的写法:在 while 循环里,每次取一个元素做乘法,然后放回结果数组。这个函数对编译器而言信号很明确:它是标量运算,没有使用SIMD 指令。

ARM_DSP_ATTRIBUTE void arm_mult_f32_fpu(
 const float32_t * pSrcA,
 const float32_t * pSrcB,
    float32_t * pDst,
    uint32_t blockSize)
{
  uint32_t blkCnt; /* Loop counter */

  blkCnt = blockSize;
  while (blkCnt > 0U)
  {
    *pDst++ = (*pSrcA++) * (*pSrcB++);
    blkCnt--;
  }
}

在日常开发中,这种循环最常见:灵活、好理解。不过一旦数据规模变大,就可能遇到性能瓶颈。

4.2 MVE向量循环:arm_mult_f32_mve

Helium 里一次能处理 128 bits,也就是 4 个 32 位浮点数。这里我们利用了 MVE 提供的 C intrinsics,明确告诉编译器要走向量化指令:

ARM_DSP_ATTRIBUTE void arm_mult_f32_mve(
 const float32_t * pSrcA,
 const float32_t * pSrcB,
    float32_t * pDst,
    uint32_t blockSize)
{
  uint32_t blkCnt; /* Loop counter */

  f32x4_t vec1;
  f32x4_t vec2;
  f32x4_t res;

  /* Compute 4 outputs at a time */
  blkCnt = blockSize >> 2U;
  while (blkCnt > 0U)
  {
    vec1 = vld1q(pSrcA);   // load 4 floats from pSrcA
    vec2 = vld1q(pSrcB);   // load 4 floats from pSrcB
    res = vmulq(vec1, vec2); // vector multiply
    vst1q(pDst, res);     // store 4 results

    pSrcA += 4;
    pSrcB += 4;
    pDst += 4;
    blkCnt--;
  }

  /* Tail: handle leftover if not multiple of 4 */
  blkCnt = blockSize & 0x3;
  if (blkCnt > 0U)
  {
   mve_pred16_t p0 = vctp32q(blkCnt);
   vec1 = vld1q(pSrcA);
   vec2 = vld1q(pSrcB);
   vstrwq_p(pDst, vmulq(vec1, vec2), p0);
  }
}

一次性能处理4个浮点数据,不再是单条标量指令。最大好处:缩减循环迭代次数,编译器也不用猜测你是不是想要 SIMD——代码里就是实打实的向量指令。


5. 测试数据:无优化→-Ofast,一步步见证性能悬殊

接下来我们在 G32R501(支持 Helium)上,用 blockSize=192 做了三组实验。编译器为 armclang,重点考察“标量函数 vs. 显式 MVE 函数”的实时性能(CPU cycles)。

5.1 场景一:无优化 (-O0)

如果我们把编译优化关掉,或者保持低级别,让编译器不做自动向量化,那就能观察到纯“显式向量”VS.“标量循环”的对比。

大致结果如下:

  • MVE 版(arm_mult_f32_mve)约 3209 cycles
  • 标量版(arm_mult_f32_fpu)约 5021 cycles
  • 速度比 = 5021 / 3209 ≈ 1.56

image-20250714202331595.png

可以明显看出,显式向量化要比标量快 50% 以上。这也是 SIMD 并行的直观魅力——一次指令并行处理多个数据,循环次数减少。

5.2 场景二:-Ofast 最高优化

当我们切换到 -Ofast 编译等级时,编译器会尽可能地利用各种高级别优化。

image-20250714203553473.png

结果如下:

  • MVE 版:约 544 cycles
  • 标量版:约 551 cycles
  • 速度比 = 1.01

image-20250714202701471.png

不可思议的是,这时标量循环已经能做到几乎和显式MVE一样快。二者仅差 7 个周期,几乎可以当作持平。这跟我们在无优化时看到的 1.56 倍差距完全不一样。难道说 Helium 自己失灵了吗?还是标量浮点爆发了?

我们深究一下,看看汇编代码的情况。我们通过使用 fromelf --text -c -o "$L@L.asm" "#L" 查看反汇编代码(设置完毕后重新编译一次代码):

image-20250714203014784.png

我们发现编译器把 arm_mult_f32_fpu 生成了包含向量操作的函数,出现了vmul.f32这种操作。

  arm_mult_f32_fpu
    0x00002298:  b580    ..   PUSH   {r7,lr}
    0x0000229a:  f24310c0  C...  MOVW   r0,#0x31c0
    0x0000229e:  f2403100  @..1  MOVW   r1,#0x300
    0x000022a2:  f2c00000  ....  MOVT   r0,#0
    0x000022a6:  f2c20100  ....  MOVT   r1,#0x2000
    0x000022aa:  1a08    ..   SUBS   r0,r1,r0
    0x000022ac:  2810    .(   CMP   r0,#0x10
    0x000022ae:  d212    ..   BCS   0x22d6 ; arm_mult_f32_fpu + 62
    0x000022b0:  f04f0ec0  O...  MOV   lr,#0xc0
    0x000022b4:  f2403000  @..0  MOVW   r0,#0x300
    0x000022b8:  f24311c0  C...  MOVW   r1,#0x31c0
    0x000022bc:  f2c20000  ....  MOVT   r0,#0x2000
    0x000022c0:  f2c00100  ....  MOVT   r1,#0
    0x000022c4:  ecb10a01  ....  VLDM   r1!,{s0}
    0x000022c8:  ee200a00   ...  VMUL.F32 s0,s0,s0
    0x000022cc:  eca00a01  ....  VSTM   r0!,{s0}
    0x000022d0:  f00fc009  ....  LE    lr,{pc}-0xc
    0x000022d4:  e00e    ..   B    0x22f4 ; arm_mult_f32_fpu + 92
    0x000022d6:  f24312c0  C...  MOVW   r2,#0x31c0
    0x000022da:  20c0    .    MOVS   r0,#0xc0
    0x000022dc:  f2c00200  ....  MOVT   r2,#0
    0x000022e0:  f020e001   ...  DCI.W  0xf020e001 ; ? Undefined
    0x000022e4:  ecb21f04  ....  LDC   p15,c1,[r2],#0x10
    0x000022e8:  ff000d50  ..P.  VMUL.F32 q0,q0,q0
    0x000022ec:  eca11f04  ....  STC   p15,c1,[r1],#0x10
    0x000022f0:  f01fc009  ....  LETP   lr,{pc}-0xc
    0x000022f4:  bd80    ..   POP   {r7,pc}
    0x000022f6:  0000    ..   MOVS   r0,r0

可见不再是简单“FMUL + store”的标量操作,而是经过编译器一番“整容”式的高级编译,使得和MVE(其实已经是使用MVE指令了)的实现在周期数上拉近差距,也就出现了1.01的差异。

5.3 场景三:禁止自动向量化

了更好地对比,我们可以把 arm_mult_f32_fpu.c 放到单独的编译命令并指定 -mcpu=cortex-m52+cdecp0+nomve选项,从而强行禁止编译器对那段标量循环进行自动向量化。也就是让它只能走单浮点 FMUL + store 的老路。

image-20250714202456567.png

再测试,得到:

  • MVE 版: 544 cycles(和上一场景相同)
  • 标量版(禁止自动向量化): 817 cycles
  • 速度比 = 817 / 544 ≈ 1.50

和前面无优化时的1.56差不多,再度证明如果不自动向量化,标量写法就没那么“神速”。

image-20250714202813154.png

总结起来,我们通过这几步观察,就得到了非常有意思的结论:在 -Ofast 等较高优化等级下,编译器会自动分析循环并进行向量化(auto-vectorization),使得“标量函数”也变得和手动MVE版本几乎同等效率


6. 总结

G32R501 作为支持 Helium 的代表之一,不仅仅是“功能上多了几条指令”,而是提供了充分的浮点(或定点)向量化能力、并且在相同主频下大幅提升数据吞吐。同时 Arm 在 CMSIS-DSP 库做了大量支持,可以直接调用现成的 API 做滤波、变换、统计等。这些 API 底层就已经根据宏定义自动用上 MVE 指令,可以让开发者几乎“零门槛”地享受向量化红利。

另外我们不需要过分担忧编译器不会向量化,只要数据结构和循环逻辑比较“干净”,-Ofast 就能把它变成并行处理。

所以,可以说在 G32R501 这类具备 Helium 指令集的 MCU 工程里,你不用担心“写不写 Intrinsics”的问题。只要你选对了编译选项( -Ofast ),就能享受到自动向量化的便利。如果你想要在某些关键环节进一步控制指令、管理缓存或内存对齐,可以再去写手工 intrinsics。可总结为:“能手写就手写,更有逼格;懒得管就让编译器替你搞定,也完全可行!”

这里是代码:upload 附件:g32r501_driverlib_mve.zip(解压至 G32R501_SDK_V1.1.0\driverlib\g32r501\examples\eval\即可使用)

image-20250714204727199.png

以上便是本次分享的全部内容,感谢您的阅读,在评论区发表你的看法吧~。

使用特权

评论回复
沙发
kai迪皮|  楼主 | 2025-7-14 20:58 | 只看该作者

使用特权

评论回复
板凳
zjsx8192| | 2025-7-15 09:04 | 只看该作者
dsp指令嘛?

使用特权

评论回复
地板
kai迪皮|  楼主 | 2025-7-15 09:59 | 只看该作者

是和M85内核同款的MVE指令,加速向量类运算的

使用特权

评论回复
5
xch| | 2025-7-15 10:30 | 只看该作者
看你折腾很久就没看见标题挂出的羊头: 尽是狗肉。

使用特权

评论回复
6
kai迪皮|  楼主 | 2025-7-17 10:36 | 只看该作者
xch 发表于 2025-7-15 10:30
看你折腾很久就没看见标题挂出的羊头: 尽是狗肉。

您没看到在相同平台下向量化计算后效率比单用FPU提升了1.5倍的效率么?

使用特权

评论回复
7
xch| | 2025-7-17 10:59 | 只看该作者
kai迪皮 发表于 2025-7-17 10:36
您没看到在相同平台下向量化计算后效率比单用FPU提升了1.5倍的效率么? ...

不知道啥叫“并行计算”吗?

使用特权

评论回复
8
kai迪皮|  楼主 | 2025-7-17 11:21 | 只看该作者
本帖最后由 kai迪皮 于 2025-7-17 11:31 编辑
xch 发表于 2025-7-17 10:59
不知道啥叫“并行计算”吗?

https://read.qq.com/read/1049070899/14

“SIMD操作并不意味着采用的是超标量处理器。超标量意味着CPU通过同时将指令分派给不同
处理器内的执行单元,在一个时钟周期内可以执行有多条指令。SIMD也不应与多线程操作
(Multiple Threaded Operation,SMT)混淆,后者可能具有并行运行的并发线程。”



另外你在底层看寄存器的运算,相较于fpu的32bit 4次计算,Helium可以一次性计算4个32bit。

使用特权

评论回复
9
xch| | 2025-7-17 11:55 | 只看该作者
Helium可以,不等于你可以。原生向量数据类型都没引用,并行个屁。

使用特权

评论回复
10
xch| | 2025-7-17 11:56 | 只看该作者
至少得演示个4倍,8倍速的羊肉吧? 1.5倍速的是狗肉。

使用特权

评论回复
11
kai迪皮|  楼主 | 2025-7-17 13:54 | 只看该作者
xch 发表于 2025-7-17 11:56
至少得演示个4倍,8倍速的羊肉吧? 1.5倍速的是狗肉。

目前的测试数据是同平台的一个矩阵相乘的差距。多矩阵相乘,多矩阵操作这种算法复杂度上升时提升效果会愈加明显。这里纯属抛砖引玉,如果你需要更加权威的数据,完全可以看看ARM的官方参考资料。

另每个人对性能提升的要求不一,也会有人觉得不提升个100倍也不算什么巨大提升。

使用特权

评论回复
12
kai迪皮|  楼主 | 2025-7-17 13:57 | 只看该作者
xch 发表于 2025-7-17 11:55
Helium可以,不等于你可以。原生向量数据类型都没引用,并行个屁。

我不理解你的表达,我现在测试的不就是 cortex-M52 的Helium特性吗?
另外我也使用到了原生的向量数据类型:“f32x4_t”,这些分享中都有说明的呀。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

42

主题

277

帖子

11

粉丝