cortex-a系列有偏重能耗与偏重性能的两个方向,对于偏重能耗的芯片往往我们可以不使用,而偏重性能的芯片我们不能去掉FPU与NEON,所以一般做这样的划分:
高性能组:Cortex-A15/A57/A72/A73/A75
高能效组:Cortex-A7/A53/A55
现在我们分析树莓派3b的情况,采用的是A53系列是可以选择有无FPU和NEON的。
具体情况可以看树莓派3b 64位。
我们可以在rtthread.py中查看到如果加上+nofp+nosimd
则表示不使用FPU与NEON,此时我们在代码中就不能有浮点相关的操作,如果有则编译器检查到了有浮点操作直接报错。
DEVICE = ' -march=armv8-a+nofp+nosimd -mtune=cortex-a53 -fno-omit-frame-pointer -funwind-tables'
编译过程中编译器就检查到了错误指令
所以在比较高性能的Cortex-A CPU(比如Cortex-A15/A57/A72/A73/A75)中,NEON和FPU是不能在RTL配置里去掉的,在高能效的Cortex-A的CPU(比如Cortex-A7/A53/A55)中NEON和FPU是可以在RTL配置里面配置有或是没有。
如何证明代码中确实使用硬件fpu单元,这里我们就需要通过指令集的区别进行区别了。
下面通过一个实际代码进行验证
void float_add(void)
{
float a = 1.02,b = 1.03,c = 0;
c = a + b;
rt_kprintf("c is %d\n", (int)(c*1000));
}
上述操作中,测试了一个浮点加法运行,编译通过后通过反汇编查看 objdump
aarch64-elf-objdump -D rtthread.elf > test.asm
通过上述汇编指令,我们不难发现采用了s0与s1之类的寄存器
查看aarch64手册
此时采用的是32bit的浮点寄存器指令,如果我们需要采用64位的浮点运算指令,那么我们就可以采用如下方式,将float改为double即可。
void float_add(void)
{
double a = 1.02,b = 1.03,c = 0;
c = a + b;
rt_kprintf("c is %d\n", (int)(c*1000));
}
查看汇编指令
此时就可以正常的看到使用了64位的浮点运算单元寄存器了
neon是一种基于SIMD的arm技术,单指令多数据流指令在多媒体场合比较适用。
下面是不同的arm体系架构下SIMD指令的支持情况
对于armv8一条neon指令的格式如下:
{<prefix>}<op>{<suffix>} Vd.<T>, Vn.<T>, Vm.<T>
<prefix>——前缀,如S/U/F/P 分别表示 有符号整数/无符号整数/浮点数/布尔数据类型 <op>——操作符。例如ADD,AND等。<suffix>——后缀,通常是有以下几种
ADDHN2:两个128位矢量相加,得到64位矢量结果,并将结果存到NEON寄存器的高64位部分。SADDL2:两个NEON寄存器的高64位部分相加,得到128-位结果。<T>——数据类型,通常是8B/16B/4H/8H/2S/4S/2D等。B代表8位数据类型;H代表16位数据宽度;S代表32位数据宽度,可以是32位整数或单精度浮点;D代表64位数据宽度,可以是64位整数或双精度浮点。
实现两个数组相加,并且将得到的结果保存在第三个数组中。
程序设计思路:
1.申请两个目标数组,数组大小为 2 * 1024 * 1024
2.申请一个结果数组,将结果保存在数组中
3.取随机数填充到目标数组
4.循环 2 * 1024 * 1024 * 10次进行相加计算
5.统计运算时间
树莓派3b对比测试如下:
#define MAX_LEN 2 * 1024 * 1024
typedef unsigned char uint_8t;
typedef unsigned short uint_16t;
extern int asm_add_neon(uint_8t *dist1, uint_8t *dist2, uint_16t *out, int len);
void test_add_time_c(void)
{
int ii = 0, kk = 0;
rt_tick_t start_time = 0;
uint_8t *dist1 = (uint_8t *)rt_malloc(sizeof(uint_8t) * MAX_LEN);
uint_8t *dist2 = (uint_8t *)rt_malloc(sizeof(uint_8t) * MAX_LEN);
uint_16t *out = (uint_16t *)rt_malloc(sizeof(uint_16t) * MAX_LEN);
for (ii = 0; ii < MAX_LEN; ii++)
{
dist1[ii] = rand() % 256;
dist2[ii] = rand() % 256;
}
start_time = rt_tick_get();
for(kk = 0; kk < 10; kk++)
{
for (int ii = 0; ii < MAX_LEN; ii++)
{
out[ii] = dist1[ii] + dist2[ii];
}
}
rt_kprintf("test_add_time_c is %d\n", rt_tick_get() - start_time);
start_time = rt_tick_get();
for(kk = 0; kk < 10; kk++)
{
asm_add_neon(dist1, dist2, out, MAX_LEN);
}
rt_kprintf("test_add_time_neon is %d\n", rt_tick_get() - start_time);
}
neon运算asm_add_neon
汇编代码如下
.text
.global asm_add_neon
asm_add_neon:
LOOP:
LDR Q0, [X0], #0x10
LDR Q1, [X1], #0x10
UQADD V0.16B, V0.16B, V1.16B
STR Q0, [X2], #0x10
SUBS X3, X3, #0x10
B.NE LOOP
RET
该程序LDR Q0, [X0], #0x10
就是将第一个参数取值放到Q0寄存器中,由于Q0寄存器是128位的寄存器。
然后X0偏移16字节,获取到下一个数组值。
然后调用NEON的加法运算指令UQADD V0.16B, V0.16B, V1.16B
,Q0代表数组A, Q1代表数组B, 每次读128bit (16个), 利用ARM vector无饱和相加指令UQADD进行计算,得到的结果存储在X2寄存器。
最后树莓派上真机测试的结果如下:
明显可见,采用neon运算的效率要比直接存C运算结果的效果好许多,其实进行浮点乘法效果更加明显。
关于aarch64在rt-thread中使用neon的思考与应用场合:
一般用上了neon的单指令多数据进行加速,肯定是处理矩阵运算或者相关的数学运算,这时我们认为加速过程中是不应该设计成有其他的高优先级任务的干扰的。此间的过程是线程调度临界区,但是可以响应中断。
比如在进行图像格式的转换过程中(yuv to rgb),我们往往关心的问题点是是否可以处理的快速,对于没有单指令多据流的操作,我们往往采用的加速方式是浮点化整,乘法化移位,这样的编译器处理的过程会大大提高执行效率。再深度的优化就是SIMD了,不同的处理器工作方式类似。但是这里往往涉及到的问题是图像的传输速度是否跟的上图像的处理速度?这个一般设计中,图像处理速度比图像传输速度快上许多。此时如果用上SIMD,那么CPU将会有更多的时间去处理其他的事情,对于rt-thread来说,低优先的线程比如gui也会更加的流畅。
另外就是关于SIMD在处理过程中中断到来后现场恢复的问题,实际上我们入栈和出栈的过程只是FPU/SIMD公用的寄存器来进行状态的保存和恢复,而实际的运算指令实际就是一条,所以如果要进行深度的SIMD指令级的定制,我们需要压入和弹出的寄存器可能会更加的多,必然会影响代码运行效率。
两弊相衡取其轻,两利相权取其重。一种办法是处理fpu与neon过程中,关闭调度器,这种方式,不会修改浮点运算寄存器,所以不会压栈与出栈,代码整体的执行效率高。另外一种方式就可以在处理运算过程中去被其他高优先级任务抢占,这种需要将大量的浮点运算寄存器进行压栈和入栈,相对前一种,代码执行效率低,但是实时性稍微好点。