使用ARM NEON Intrinsics加速Video Codec

前言

最近公司在视频直播项目中要使用H.265/HEVC,具体的是使用HW硬件编码H.264/AVC,云端转码成H.265/HEVC并推流的解决方案。方案中使用的解码器是FFMpeg中的H.265解码器,该解码器是从OpenHEVC直接获取的,比起备受好评的H.264/AVC解码器,这个解码器目前优化不足,在手机上占用资源较高。因此一个工作就是优化该解码器在手机上的性能表现,主要使用ARM提供的SIMD指令进行优化。

SIMD简介

Single Instruction Multiple Data (SIMD),单指令多数据。从字面理解,就是在CPU执行中,一条操作指令可以同时操作多个寄存器,从而在物理上倍数的加速运行。我理解范畴内的X86平台上最早的SIMD指令应该是奔腾MMX上自带的MMX指令,其寄存器宽度是64位,可以同时操作8个字节。MultiMedia eXtensions (MMX)是多媒体扩展的意思,其最初的设计目的就是为了加速图像/视频等高并行数据的处理速度。

  • 一个简单的SIMD示意图如下所示:


    SIMD 8x8加法示意图

在这里,一条SIMD加法指令可以同时得到8个加法结果。就计算步骤本身而言,比单独使用8条加法指令能够获得8倍的加速比。从该示例也可以看出,随着寄存器长度的变长,单指令能够处理的数据量也越来越大,从而获得更高的加速性能。在Intel最新的AVX2指令集中,寄存器最大长度已经达到512位。

ARM NEON Intrinsics简介

NEON指令是从Armv7架构开始引入的SIMD指令,其共有16个128位寄存器。发展到最新的Arm64架构,其寄存器数量增加到32个,但是其长度仍然为最大128位,因此操作上并没有发生显著的变化。对于这样的寄存器,因为可以同时存储并处理多组数据,称之为向量寄存器。Intrinsics是使用C语言的方式对NEON寄存器进行操作,因为相比于传统的使用纯汇编语言,具有可读性强,开发速度快等优势。如果需要在代码中调用NEON Intrinsics函数,需要加入头文件"arm_neon.h"。

数据类型

NEON Intrinsics内置的整数数据类型主要包括以下几种:

  • (u)int8x8_t;
  • (u)int8x16_t;
  • (u)int16x4_t;
  • (u)int16x8_t;
  • (u)int32x2_t;
  • (u)int32x4_t;
  • (u)int64x1_t;

其中,第一个数字代表的是数据类型宽度为8/16/32/64位,第二个数字代表的是一个寄存器中该类型数据的数量。如int16x8_t代表16位有符号数,寄存器中共有8个数据。

常用指令

NEON Intrinsics支持的所有指令可参看ARM NEON Intrinsics,其包含了常用的arm汇编指令类型,如数学运算,逻辑运算等。另外,其引入了有针对性的加载/存储/转置/交叉存取等指令。部分常见的指令在会下面的示例环节中予以说明。需要注意的是,指令中的助记符与arm汇编是相同的。

示例1:
  • int16x8_t vqaddq_s16 (int16x8_t, int16x8_t)
  • int16x4_t vqadd_s16 (int16x4_t, int16x4_t)
  1. 第一个字母'v'指明是vector向量指令,也就是NEON指令;
  2. 第二个字母'q'指明是饱和指令,即后续的加法结果会自动饱和;
  3. 第三个字段'add'指明是加法指令;
  4. 第四个字段'q'指明操作寄存器宽度,为'q'时操作QWORD, 为128位;未指明时操作寄存器为DWORD,为64位;
  5. 第五个字段's16'指明操作的基本单元为有符号16位整数,其最大表示范围为-32768 ~ 32767;
  6. 形参和返回值类型约定与C语言一致。

其它可能用到的助记符包括:

  • l 长指令,数据扩展
  • w 宽指令,数据对齐
  • n 窄指令, 数据压缩
示例2
  • uint8x8_t vld1_u8 (const uint8_t *)
  1. 第二个字段'ld'表示加载指令
  2. 第三个字段'1'(注意是1,不是l)表示顺次加载。如果需要处理图像的RGB分量,可能会用到vld3。关于vld/vst指令更详细的说明,请自己参阅arm官方文档。

函数改写示例

1. 简单示例

原始代码

// 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;
}

说明:

  • 这里只针对宽度为8的倍数进行了改写,实际代码中需要对传入参数进行判断
  • vld1_u8读取8字节数据,vmovl_u8对读取的uint8x8进行宽度扩展
  • vreinterpretq_s16_u16对数据类型进行强制转换
  • vshlq_n_s16对数据进行左移处理(P.S. NEON提供了右移指令,但是只能使用整数常量。需要根据变量进行右移时,只能使用左移负数位的方法。)
  • vqmovn_u16对处理结果进行宽度压缩
  • vst1_u8将处理后的int16x8_t数据写回内存
2.进阶示例

原始代码

/*
#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的原理和基本应用。实际中需要对待优化的函数原理及能使用的资源了解清楚才能使用最有效的方法并行化程序。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容