前篇回顾
Helium指令集
这里介绍的是Helium的汇编语言指令集,虽然大部分程序员不会直接使用这些指令,而是通过C语言或者高级语言编程实现,但是了解汇编语言指令集,可以有如下收获:
- 在优化C代码时,为了确定其是否被充分地矢量化,能够审视编译器的输出以及熟悉指令集是非常有帮助的。
- 当调试不能正常工作的代码时,通过阅读反汇编代码去理解每一行发生了什么,对于寻找代码的问题是非常有用的。
- 了解指令集可能有助于编写高效的代码,甚至节省功耗,尤其是在使用原语函数的情况下。
Helium指令结构和其他Cortex-M处理器中的VFP(浮点)指令结构是相似的。
Helium指令格式如下:
Helium指令都是以字母V开始的,然后跟着如下符号,符合中的{}是可选的,<>是必须出现的:
- mod:指令修饰符,可能没有,也可能是Q(saturating)饱和,H(halving)减半,D(doubling)加倍,R(rounding)四舍五入中的一个。
- op:具体操作,例如ADD(相加),SUB(相减),CMP(比较)等。
- shape:有些指令中,可以选择性的指定L(long)或N(narrow),这是 “形态” 相关的修饰符。
- L:Long表示输入元素在操作前会被扩宽。1个8位的元素可能会被扩宽为16位或32位,或者1个16位元素被扩宽为32位。
- N:Narrow表示输入元素在操作前会被压缩。
- extra:有些指令中的特定修饰符,可能是T(top),B(bottom),A(accumulate),X(exchange)或者V(across)中的一个。
- cond:此处指定的条件仅适用于VPT(Predication)模块。可能是T(Then)或者E(Else)。
- .dt:数据类型,可能是F(float)浮点,I(integer)整数,S(signed)有符号,U(unsigned)无符号。
- dst:目标寄存器,可以是通用寄存器(R)或者矢量寄存器(Q)。
- src:源寄存器,可以是通用寄存器(R)或者矢量寄存器(Q)。
- rot:旋转,用于一些操作复数的指令。
下面给出一条指令示例展示:
VLDRW.U32 Q0, [R0]
该指令中的首字母是V,表示这是一条Helium(或是Neon,或者浮点)指令,LDR表示寄存器从内存加载内容,W表示按字大小操作,<mod>,<shape>,<extra>都为空,数据类型是U32,无符号32位整数。加载的目标是128位寄存器Q0(矢量寄存器),源是标量寄存器R0指向的内存地址。该指令表示将从R0存储的地址中加载4个32位宽的数据到Q0寄存器中。
Helium指令分类如下:
Helium编程方式
Helium编程方式目前来说,一共4种。
- 矢量库
- 自动矢量化
- 原语函数(intrinsics)编程
- 汇编指令编程
矢量库
目前,ARM CMSIS DSP和NN是已经对Helium优化好的Helium矢量库。使用矢量库来进行Helium编程,是最简单的方法。
- CMSIS DSP是数字信号处理函数库,具有针对8位整数,16位整数,32位整数和32位浮点数的不同函数,提供了丰富的函数,包括基本数学函数,复数数学函数,滤波器函数,变换函数,矩阵操作函数,电机控制函数,插值函数,统计函数等。该库包含了这些函数的Helium优化版本,并不断更新迭代中。
- CMSIS NN是神经网络函数库,以最小的内存开销针对Cortex-M处理器优化的软件内核,同样地,这些函数也可以利用Helium得到最优性能。
CMSIS矢量库中的函数代码有3个C预处理器定义来选择Helium版本。
#define ARM_MATH_HELIUM
#define ARM_MATH_MVEI //支持整型Helium
#define ARM_MATH_MVEF //支持浮点型Helium
比如CMSIS DSP中的 arm_clip_f32函数,可以看到该函数已经使用了Helium原语函数。
比如CMSIS NN中的arm_nn_lstm_update_cell_state_s16函数,可以看到该函数使用了Helium原语函数。
当使用矢量库的时候,不同编译器中的MVE设置
Keil MDK 5(5.38以上版本)
在图标“Options for target”中选择“Target”页面中的“Vector Extensions”,通过下拉列表选择
- “Not Used”(不使用helium,即宏ARM_MATH_HELIUM没有被定义,使用标量相关函数)。
- “Integer”(宏ARM_MATH_HELIUM和ARM_MATH_MVEI被定义,使用整型Helium)。
- “Integer + Floating Point” (宏ARM_MATH_HELIUM,ARM_MATH_MVEI和ARM_MATH_MVEF被定义,使用整型和浮点型Helium)。
IAR EWARM(v9.40.1以上版本)
自动矢量化
自动矢量化就是编译器在C/C++代码中自动检测到可以使用Helium指令并执行优化的过程。优化后的代码在速度和尺寸方面可能与手工优化的汇编代码或包含原语函数的C代码一样高效,这只需要很少的时间去编写和调试代码,而且无须对目标微架构有详细了解。C代码也更有可移植性。
如下面的代码,这是一种很常见的普通写法,一个for循环里面做一些逻辑判断处理。
通过使用自动矢量化后的反汇编代码如下,红色框部分的代码里面已经出现了Helium的汇编指令。
自动矢量化和编译器的优化等级设置有关,当Arm Complier 6和LLVM编译器的优化等级为-O2或者更高时,自动矢量化默认使能,在MDK Arm Complier 6中可以使用“-fno-vectorize”选项可以禁止自动矢量化。当优化等级为-O1时,自动矢量化默认禁止,使用“-fvectorize”选项可以使能自动矢量化,当优化等级为-O0时,自动矢量化总是被禁止。其他编译器的行为可能不同,具体可以查阅对应的文档。
原语函数(intrinsics)编程
原语函数是允许利用Helium而不必直接编写汇编代码的一组C/C++函数。ACLE文档中包括Helium原语规范。目前最新的文档为mve-2021Q4。原语函数的实现包含在arm_mve.h文件中。函数包含简短的汇编语言部分,它们被内联到调用的代码中。
ACLE文档
https://github.com/ARM-software/acle/releases/tag/r2023Q2
使用原语函数有如下优点:
- 程序员能够直接访问Helium指令集,这允许编写充分优化的代码,利用所有Helium特性。
- C/C++可用于大多数代码,只有当需要优化而矢量化C编译器无法执行优化时,才会使用Helium原语。这就意味着只有在必要时才使用底层代码。
- 相比于采用汇编语言编写的代码,含有Helium原语的C和C++代码可以移植到一个新的平台,仅需要少量修改,甚至无须修改。
- 使用原语避免了很多与直接使用汇编语言编码相关的难点。
完整的指令列表详见:
https://developer.arm.com/architectures/instruction-sets/intrinsics/
原语函数中,Helium矢量数据类型名字模式如下所示,这在“arm_mve.h”中有详细定义和描述。
<type><size>x<number_of_lanes>_t
- type:元素类型,可能是int整形,uint无符号整形,float浮点。
- size:元素大小,可能是8位,16位,32位。
- number_of_lanes:通道总数。可以是16通道,8通道,或者4通道。
如:
uint8x16_t是一个描述16个无符号8位的矢量。
int16x8_t是一个描述8个16位的矢量。
float16x8_t是一个描述4个16位浮点数(半精度)的矢量。
float32x4_t是一个描述4个32位浮点数(单精度)的矢量。
注:Helium是128位寄存器,它的元素大小和通道总数相乘的结果只能是128,不能是64,也就是说,不支持int8x8_t/uint8x8_t/int16x4_t/uint16x4_t/float16x4_t/float32x2_t数据类型。这点和Neon是不同的。Neno可以支持64和128。
此结构类型仅由加载、存储、转置、交织和去交织指令使用;要对实际数据执行操作,请从各个寄存器中选择元素。如:<var_name>.val[0] 和<var_name>.val[1]。
下图代码片段是使用原语函数进行矢量相乘的例子。
原语编程里面还涉及原语预测,原语尾部处理等知识,本处不在展开说明,详细信息可以访问arm官网查阅相关文档了解和学习。
汇编语言编程
在汇编代码中直接编写Helium指令是很没有必要的,通常只会在特殊的场景下才会这样做。即当编程人员可以比编译器更好地分配寄存器时,比如有太多重写变量和输入输出变量。
下图所示为复数矢量点积的汇编语言代码。
作者:Ant Ling
来源:瑞萨嵌入式小百科
推荐阅读
欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。