http://www.jianshu.com/p/70601b36540f
最近公司在视频直播项目中要使用H.265/HEVC,具体的是使用HW硬件编码H.264/AVC,云端转码成H.265/HEVC并推流的解决方案。方案中使用的解码器是FFMpeg中的H.265解码器,该解码器是从OpenHEVC直接获取的,比起备受好评的H.264/AVC解码器,这个解码器目前优化不足,在手机上占用资源较高。因此一个工作就是优化该解码器在手机上的性能表现,主要使用ARM提供的SIMD指令进行优化。
Single Instruction Multiple Data (SIMD),单指令多数据。从字面理解,就是在CPU执行中,一条操作指令可以同时操作多个寄存器,从而在物理上倍数的加速运行。我理解范畴内的X86平台上最早的SIMD指令应该是奔腾MMX上自带的MMX指令,其寄存器宽度是64位,可以同时操作8个字节。MultiMedia eXtensions (MMX)是多媒体扩展的意思,其最初的设计目的就是为了加速图像/视频等高并行数据的处理速度。
在这里,一条SIMD加法指令可以同时得到8个加法结果。就计算步骤本身而言,比单独使用8条加法指令能够获得8倍的加速比。从该示例也可以看出,随着寄存器长度的变长,单指令能够处理的数据量也越来越大,从而获得更高的加速性能。在Intel最新的AVX2指令集中,寄存器最大长度已经达到512位。
NEON指令是从Armv7架构开始引入的SIMD指令,其共有16个128位寄存器。发展到最新的Arm64架构,其寄存器数量增加到32个,但是其长度仍然为最大128位,因此操作上并没有发生显著的变化。对于这样的寄存器,因为可以同时存储并处理多组数据,称之为向量寄存器。Intrinsics是使用C语言的方式对NEON寄存器进行操作,因为相比于传统的使用纯汇编语言,具有可读性强,开发速度快等优势。如果需要在代码中调用NEON Intrinsics函数,需要加入头文件"arm_neon.h"。
NEON Intrinsics内置的整数数据类型主要包括以下几种:
其中,第一个数字代表的是数据类型宽度为8/16/32/64位,第二个数字代表的是一个寄存器中该类型数据的数量。如int16x8_t代表16位有符号数,寄存器中共有8个数据。
NEON Intrinsics支持的所有指令可参看ARM NEON Intrinsics,其包含了常用的arm汇编指令类型,如数学运算,逻辑运算等。另外,其引入了有针对性的加载/存储/转置/交叉存取等指令。部分常见的指令在会下面的示例环节中予以说明。需要注意的是,指令中的助记符与arm汇编是相同的。
其它可能用到的助记符包括:
原始代码
// uint8_t *_dst, uint8_t *_src, int16_t *src2
// int height, int width
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
dst[x] = av_clip_pixel(((src[x] << 6) + src2[x] + offset) >> shift);
}
src += srcstride;
dst += dststride;
src2 += MAX_PB_SIZE;
}
`
改写代码
int16x8_t result_16x8;
int16x8_t offset_16x8 = vmovq_n_s16(offset);
int16x8_t minusshift_16x8 = vmovq_n_s16(-1 * shift);
int16x8_t min_16x8 = vmovq_n_s16(0);
int16x8_t max_16x8 = vmovq_n_s16(255);
for (y = 0; y < height; y++) {
for (x = 0; x < width; x+=8) {
result_16x8 = vshlq_n_s16(vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x]))), 6);
result_16x8 = vshlq_s16(vqaddq_s16(vqaddq_s16(result_16x8, vld1q_s16(&src2[x])), offset_16x8), minusshift_16x8);
vst1_u8(&dst[x], vqmovn_u16(vreinterpretq_u16_s16(vmaxq_s16(vminq_s16(result_16x8, max_16x8), min_16x8))));
}
src += srcstride;
dst += dststride;
src2 += MAX_PB_SIZE;
}
`
说明:
原始代码
/*
#define QPEL_FILTER(src, stride) \
(filter[0] * src[x - 3 * stride] + \
filter[1] * src[x - 2 * stride] + \
filter[2] * src[x - stride] + \
filter[3] * src[x ] + \
filter[4] * src[x + stride] + \
filter[5] * src[x + 2 * stride] + \
filter[6] * src[x + 3 * stride] + \
filter[7] * src[x + 4 * stride])
DECLARE_ALIGNED(16, const int8_t, ff_hevc_qpel_filters[3][16]) = {
{ -1, 4,-10, 58, 17, -5, 1, 0, -1, 4,-10, 58, 17, -5, 1, 0},
{ -1, 4,-11, 40, 40,-11, 4, -1, -1, 4,-11, 40, 40,-11, 4, -1},
{ 0, 1, -5, 17, 58,-10, 4, -1, 0, 1, -5, 17, 58,-10, 4, -1}
};
*/
filter = ff_hevc_qpel_filters[mx - 1];
for (y = 0; y < height + QPEL_EXTRA; y++) {
for (x = 0; x < width; x++)
tmp[x] = QPEL_FILTER(src, 1);
src += srcstride;
tmp += MAX_PB_SIZE;
}
改写代码
/*
DECLARE_ALIGNED(16, const int8_t, ff_hevc_qpel_filtersT[3][64]) = {
{ -1, -1, -1, -1, -1, -1, -1, -1, 4, 4, 4, 4, 4, 4, 4, 4,//(0)
-10,-10,-10,-10,-10,-10,-10,-10, 58, 58, 58, 58, 58, 58, 58, 58,
17, 17, 17, 17, 17, 17, 17, 17, -5, -5, -5, -5, -5, -5, -5, -5,
1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0},
{ -1, -1, -1, -1, -1, -1, -1, -1, 4, 4, 4, 4, 4, 4, 4, 4,//(1)
-11,-11,-11,-11,-11,-11,-11,-11, 40, 40, 40, 40, 40, 40, 40, 40,
40, 40, 40, 40, 40, 40, 40, 40,-11,-11,-11,-11,-11,-11,-11,-11,
4, 4, 4, 4, 4, 4, 4, 4, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,//(2)
-5, -5, -5, -5, -5, -5, -5, -5, 17, 17, 17, 17, 17, 17, 17, 17,
58, 58, 58, 58, 58, 58, 58, 58,-10,-10,-10,-10,-10,-10,-10,-10,
4, 4, 4, 4, 4, 4, 4, 4, -1, -1, -1, -1, -1, -1, -1, -1}
};
*/
int16x8_t filteT_16x8_0, filteT_16x8_1, filteT_16x8_2, filteT_16x8_3, filteT_16x8_4, filteT_16x8_5, filteT_16x8_6, filteT_16x8_7;
int16x8_t result_16x8;
filter = ff_hevc_qpel_filtersT[mx - 1];
filteT_16x8_0 = vmovl_s8(vld1_s8(&filter[0]));
filteT_16x8_1 = vmovl_s8(vld1_s8(&filter[8]));
filteT_16x8_2 = vmovl_s8(vld1_s8(&filter[16]));
filteT_16x8_3 = vmovl_s8(vld1_s8(&filter[24]));
filteT_16x8_4 = vmovl_s8(vld1_s8(&filter[32]));
filteT_16x8_5 = vmovl_s8(vld1_s8(&filter[40]));
filteT_16x8_6 = vmovl_s8(vld1_s8(&filter[48]));
filteT_16x8_7 = vmovl_s8(vld1_s8(&filter[56]));
for (y = 0; y < height + QPEL_EXTRA; y++) {
for ( x = 0; x < width; x += 8 ) {
// init the output reg
result_16x8 = vmovq_n_s16(0);
// (0)
result_16x8 = vmlaq_s16(result_16x8, vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x-3]))), filteT_16x8_0);
// (1)
result_16x8 = vmlaq_s16(result_16x8, vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x-2]))), filteT_16x8_1);
// (2)
result_16x8 = vmlaq_s16(result_16x8, vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x-1]))), filteT_16x8_2);
// (3)
result_16x8 = vmlaq_s16(result_16x8, vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x]))), filteT_16x8_3);
// (4)
result_16x8 = vmlaq_s16(result_16x8, vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x+1]))), filteT_16x8_4);
// (5)
result_16x8 = vmlaq_s16(result_16x8, vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x+2]))), filteT_16x8_5);
// (6)
result_16x8 = vmlaq_s16(result_16x8, vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x+3]))), filteT_16x8_6);
// (7)
result_16x8 = vmlaq_s16(result_16x8, vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x+4]))), filteT_16x8_7);
// store the output data
vst1q_s16(&tmp[x], result_16x8);
}
src += srcstride;
tmp += MAX_PB_SIZE;
}
说明:
在C实现中,每个结果需要读取包括自身在内的8个输入,乘以相应的系数并累加。最简单直观的实现方法是
output_16x8 = vmulq_s16( vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x-3]))), vmovl_s8(vld1_s8(ff_hevc_qpel_filters[mx - 1])));
这样实现,会使得8个乘积分布在同一个向量寄存器中,需要通过取寄存器的不同元素实现累加,加法部分无法并行。
在C实现中,其数学表示为两个1x8和8x1的矩阵之间的乘法。分析数据间的关系,将矩阵乘法转换为矩阵转置乘法,可以得出前文改写代码的实现。在该实现中,由于滤波器系统固定,因此预先定义了其转置矩阵并扩展。在进行'乘加'操作的过程中,一个循环将8个结果全部计算完毕,使得乘法/加法均实现了并行化。
P.S. 这里,单独设置了8个向量寄存器变量并展开使得代码较长,使用循环+数组的方式也可以得到同样的结果,且代码较短。但是在底层高频函数中,尽量展开循环可以最大化的提升效率。
本文只介绍了使用ARM NEON Intrinsics的原理和基本应用。实际中需要对待优化的函数原理及能使用的资源了解清楚才能使用最有效的方法并行化程序。