manout's blog

Something about me

todo list

  • convolution sgemm + bias 汇编优化
  • 真机测试之前的模型。检查误差的大小
    • 误差没有超出临界值
  • [x]convolution 1x1C 加汇编级 bias 优化
  • [x]convolution 3x3C 加汇编级 bias 优化 导师说不用做了

数据类型

  • 32 bits 单精度浮点数
  • 8, 16, 32, 64 bits 无符号有符号整形
  • 8, 16 bits 多项式

类型说明符

  • 无符号数
    U8, U16, U32, U64
  • 有符号数
    S8, S16, S32, S64
  • 无指定类型整型
    I8,I16, I32, I63
  • 浮点数
    F16, F32
  • 0 - 1 多项式
    P8

指令格式

Neon 指令的一般格式如下

1
V{<mod>}<op>{<shape>}{<cond>}{.<datatype>} {dest}, <src1>, <src2>
  • mod
    • Q
      指令使用饱和算法,所以结果总会在指定的数据类型的表示范围之间
    • H
      指令会使结果减半,将结果右移一位实现
    • D
      指令会使结果乘二
    • R
      指令会将结果取整,等价于加上 0.5 之后截断小数部分
  • op
    操作,如 ADD, SUB
  • shape
    • L
      操作双字 vector, 生成四倍长字 vector
      结果宽度通常是操作数的两倍,类型相同
    • W
      操作双字 + 四倍长字,结果和第一个操作数都是第二个操作数宽度的两倍
    • N
      操作四倍长字,结果生成双字,结果宽度一般是操作数的一半。
  • cond
    与 IT 指令混用
  • datatype
    数据类型,s8, u8, f32
  • dest
    目的寄存器
  • src1
    第一操作数
  • src2
    第二操作数

饱和算法

  • ARM 中的饱和算法
    • 对于有符号饱和运算,若结果小于 , 那么返回结果
    • 对于无符号饱和运算,若结果小于 0, 那么结果返回 0, 如果结果大于 , 那么返回
  • Neon 中的饱和算法
    指令中使用 Q 前缀指定饱和算法,原理与 ARM 相同。

语法格式

1
2
3
4
5
6
7
asm volatile
{
汇编语句模板
: 输出部分
: 输入部分
: 破坏描述部分
}
  • asm volatile
    表示后面的代码为内嵌汇编,asm__asm__ 的别名。volatile 表示编译器不能优化代码。
  • 汇编语句模板
    由汇编语句序列构成,语句之间用;, \n, \n\t 分隔. 指令中的操作数可以用占位符引用 C 语言变量. 指令中使用占位符表示的操作数总被视为 long 型(4 个字节)。使用的操作符可以作用与字或字节,默认作用于低字节。可以用 hb 修饰,如 %h1.
  • 输出部分
    格式为"=?"(var) 的形式,var 可以是任意内存变量(输出结果会存到这个变量中),? 一般是下面这些标识符。
    • a, b, c, d, S, D 分别代表 eax, ebx, ecx, edx, esi, edi,寄存器
    • r 代表上面这些寄存器中任意一个(哪个闲置用哪个)
    • m 内存
    • i 立即数(常量,只用于输入操作数)
    • g, 寄存器,内存,立即数(由编译器决定)
      在汇编中,用 %序号 代表这些输入/输出操作数,序号从 0 开始。为了与操作数分离,寄存器用 标出, 如 %%eax.
  • 输入操作数
    格式为 ?(var), ? 除了可以是上面这些标识符,还可以是输出操作数的序号,表示用 var 初始化该输出操作数。
  • 破坏描述部分
    在汇编代码中修改,又没有在输入/输出列表中列出的寄存器,这样 gcc 就不会擅自使用这些寄存器,用 memory 表示在内联汇编中修改了内存,之前缓存在寄存器中的内存变量需要重新读取。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
int a=1, b=2, c=0;
// add
// c = a + b
asm(
"addl %2, %0" // 1
: "=g"(c) // 2
: "0"(a), "g"(b) // 3
: "memory"); // 4
printf("现在c是:%d\n", c);
return 0;
}

Neon 基础

Neon 是适用于 ARM 系列处理器的一种 SIMD(Single Instruction Multiple Data)扩展结构。Neon 有自己的执行管道和寄存器组。也就是说 Neon 和 ARM 指令是各自独立执行的。 Neon 的寄存器有

  • 32 个 64 位的寄存器(D0-D31)
  • 16 个 128 位的寄存器(Q0-Q15), ARM64 中有 32 个 128 位的寄存器

实际上 D 寄存器和 Q 寄存器是重叠的。如下图所示。两个 D 寄存器对应一个 Q 寄存器。

neon_register

在 ARM64 中,虽然 Q 寄存器有 32 个,但是 D 寄存器也只有 32 个,也就是说 Q0 只和 D0 重叠,Q1 只和 D1 重叠。

Neon 的作用在于使用向量化技术加速计算。

关键概念

  • 数据类型(data type)
    Neon 寄存器能存放大部分的基本数据类型,如 int8, int16。一个寄存器能存储的数据个数与数据大小和寄存器大小有关。
  • 向量(vector)
    在 Neon 中一个寄存器可以看做一个向量。如 64bits 的寄存器 D0,存储 4 个 int16 元素,就可以看做 length 为 4 的向量。
  • 管道(lang)
    在上面 D0 存储 4 个 int16, 那么 D0 就有四个通道, pipe0 到 pipe3, pipe0 在 D0 的低 bit 位.

使用方法

intrinsics(内部函数)

使用 intrinsics 不如使用内联汇编效率高。但是使用 intrinsics 较为简单且易于维护。这些函数在编译时会直接转化为 Neon 的汇编指令

1
2
3
4
5
#include <arm_neon.h>
uint32x4_t double_elements(uint32x4_t input)
{
return (vaddq_u32(input, input));
}

使用开源库

基于 Neon 的开源库如 Project Ne10, OpenMAX DL.

内联汇编

直接在 C/C++ 代码中内联汇编,但是可移植性差,且难度较高

编译器产生向量化指令

可以通过添加一些编译选项的方法使能向量化编译,但是对于复杂算法编译器的效果较差

NEON 内部函数

数据类型

基本数据类型

  • 64 位
    int8x8_t, int16x4_t, int32x2_t, int64x1_t,
    uint8x8_t, uint16x4_t, uint32x2_t, uint64x1_t,
    float32x2_t, float64x1_t(少见)
  • 128 位
    int8x16_t, int16x8_t, int32x4_t, int64_2_t,
    uint8x16_t, uint16_8_t, uint32x4_t, uint64x4_t,
    float32x4_t

结构化数据

差不多每种基本数据类型都有其对应的结构化数据类型

int8x8x2_tint16x4x2_tuint8x16x2_tuint16x8x2_t

int8x8x3_tint16x4x3_tuint8x16x3_tuint16x8x3_t

int8x8x4_tint16x4x4_tuint8x16x4_tuint16x8x4_t

函数分类

Neon 内部函数分为以下几类,使用时要包含 arm_neon.h 头文件

  • 初始化寄存器
  • 从内存加载数据到 neon 寄存器
  • 将 Neon 寄存器数据存储到内存
  • 直接从 Neon 寄存器获取某个通道的值
  • 直接设置 Neon 寄存器某个通道的值
  • 寄存器数据重排
  • 加减法
  • 乘法
  • 乘加组合运算
  • 乘减组合运算
  • 取整
  • 比较运算
  • 绝对值
  • 取最大最小值
  • 取倒数
  • 平方根倒数
  • 移位运算
  • 取负数
  • 按位运算
  • 统计
  • 数据类型转换

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void first_example_c(unsigned char  *src, unsigned char  *dst, int size, unsigned char  factor)
{
for (int i = 0; i < size; i++)
{
dst[i] = src[i] * factor;
}
}

void first_example_neon(unsigned char *src, unsigned char *dst, int size, unsigned char factor)
{
int main_loop = size / 8; //一个循环处理8个数据,则需要 size / 8个循环
uint8x8_t factor_vct = vdup_n_u8(factor); //将系数factor装载入neon寄存器
for (int i = 0; i < main_loop; i++)
{
uint8x8_t src_vct = vld1_u8(src);//将源数据装载入neon寄存器
uint8x8_t dst_vct = vmul_u8(src_vct, factor_vct); //执行乘法操作,且将结果放入dst_vct寄存器中
vst1_u8(dst, dst_vct); //将dst_vct寄存器中的结果放回内存中
src += 8, dst += 8; //改变地址,指向下个循环要处理的数据。
}
}
0%