前言
最近公司在視頻直播項(xiàng)目中要使用H.265/HEVC蝴光,具體的是使用HW硬件編碼H.264/AVC禁筏,云端轉(zhuǎn)碼成H.265/HEVC并推流的解決方案激况。方案中使用的解碼器是FFMpeg中的H.265解碼器,該解碼器是從OpenHEVC直接獲取的玛歌,比起備受好評(píng)的H.264/AVC解碼器,這個(gè)解碼器目前優(yōu)化不足擎椰,在手機(jī)上占用資源較高支子。因此一個(gè)工作就是優(yōu)化該解碼器在手機(jī)上的性能表現(xiàn),主要使用ARM提供的SIMD指令進(jìn)行優(yōu)化达舒。
SIMD簡(jiǎn)介
Single Instruction Multiple Data (SIMD)值朋,單指令多數(shù)據(jù)。從字面理解巩搏,就是在CPU執(zhí)行中昨登,一條操作指令可以同時(shí)操作多個(gè)寄存器,從而在物理上倍數(shù)的加速運(yùn)行贯底。我理解范疇內(nèi)的X86平臺(tái)上最早的SIMD指令應(yīng)該是奔騰MMX上自帶的MMX指令丰辣,其寄存器寬度是64位,可以同時(shí)操作8個(gè)字節(jié)禽捆。MultiMedia eXtensions (MMX)是多媒體擴(kuò)展的意思笙什,其最初的設(shè)計(jì)目的就是為了加速圖像/視頻等高并行數(shù)據(jù)的處理速度。
-
一個(gè)簡(jiǎn)單的SIMD示意圖如下所示:
在這里胚想,一條SIMD加法指令可以同時(shí)得到8個(gè)加法結(jié)果琐凭。就計(jì)算步驟本身而言,比單獨(dú)使用8條加法指令能夠獲得8倍的加速比浊服。從該示例也可以看出统屈,隨著寄存器長(zhǎng)度的變長(zhǎng),單指令能夠處理的數(shù)據(jù)量也越來(lái)越大牙躺,從而獲得更高的加速性能鸿吆。在Intel最新的AVX2指令集中,寄存器最大長(zhǎng)度已經(jīng)達(dá)到512位述呐。
ARM NEON Intrinsics簡(jiǎn)介
NEON指令是從Armv7架構(gòu)開(kāi)始引入的SIMD指令,其共有16個(gè)128位寄存器蕉毯。發(fā)展到最新的Arm64架構(gòu)乓搬,其寄存器數(shù)量增加到32個(gè)思犁,但是其長(zhǎng)度仍然為最大128位,因此操作上并沒(méi)有發(fā)生顯著的變化进肯。對(duì)于這樣的寄存器激蹲,因?yàn)榭梢酝瑫r(shí)存儲(chǔ)并處理多組數(shù)據(jù),稱之為向量寄存器江掩。Intrinsics是使用C語(yǔ)言的方式對(duì)NEON寄存器進(jìn)行操作学辱,因?yàn)橄啾扔趥鹘y(tǒng)的使用純匯編語(yǔ)言,具有可讀性強(qiáng)环形,開(kāi)發(fā)速度快等優(yōu)勢(shì)策泣。如果需要在代碼中調(diào)用NEON Intrinsics函數(shù),需要加入頭文件"arm_neon.h"抬吟。
數(shù)據(jù)類型
NEON Intrinsics內(nèi)置的整數(shù)數(shù)據(jù)類型主要包括以下幾種:
- (u)int8x8_t;
- (u)int8x16_t;
- (u)int16x4_t;
- (u)int16x8_t;
- (u)int32x2_t;
- (u)int32x4_t;
- (u)int64x1_t;
其中萨咕,第一個(gè)數(shù)字代表的是數(shù)據(jù)類型寬度為8/16/32/64位,第二個(gè)數(shù)字代表的是一個(gè)寄存器中該類型數(shù)據(jù)的數(shù)量火本。如int16x8_t代表16位有符號(hào)數(shù)危队,寄存器中共有8個(gè)數(shù)據(jù)。
常用指令
NEON Intrinsics支持的所有指令可參看ARM NEON Intrinsics钙畔,其包含了常用的arm匯編指令類型茫陆,如數(shù)學(xué)運(yùn)算,邏輯運(yùn)算等擎析。另外簿盅,其引入了有針對(duì)性的加載/存儲(chǔ)/轉(zhuǎn)置/交叉存取等指令。部分常見(jiàn)的指令在會(huì)下面的示例環(huán)節(jié)中予以說(shuō)明叔锐。需要注意的是挪鹏,指令中的助記符與arm匯編是相同的。
示例1:
- int16x8_t vqaddq_s16 (int16x8_t, int16x8_t)
- int16x4_t vqadd_s16 (int16x4_t, int16x4_t)
- 第一個(gè)字母'v'指明是vector向量指令愉烙,也就是NEON指令讨盒;
- 第二個(gè)字母'q'指明是飽和指令,即后續(xù)的加法結(jié)果會(huì)自動(dòng)飽和步责;
- 第三個(gè)字段'add'指明是加法指令返顺;
- 第四個(gè)字段'q'指明操作寄存器寬度,為'q'時(shí)操作QWORD, 為128位蔓肯;未指明時(shí)操作寄存器為DWORD遂鹊,為64位;
- 第五個(gè)字段's16'指明操作的基本單元為有符號(hào)16位整數(shù)蔗包,其最大表示范圍為-32768 ~ 32767秉扑;
- 形參和返回值類型約定與C語(yǔ)言一致。
其它可能用到的助記符包括:
- l 長(zhǎng)指令,數(shù)據(jù)擴(kuò)展
- w 寬指令舟陆,數(shù)據(jù)對(duì)齊
- n 窄指令, 數(shù)據(jù)壓縮
示例2
- uint8x8_t vld1_u8 (const uint8_t *)
- 第二個(gè)字段'ld'表示加載指令
- 第三個(gè)字段'1'(注意是1误澳,不是l)表示順次加載。如果需要處理圖像的RGB分量秦躯,可能會(huì)用到vld3忆谓。關(guān)于vld/vst指令更詳細(xì)的說(shuō)明,請(qǐng)自己參閱arm官方文檔踱承。
函數(shù)改寫示例
1. 簡(jiǎn)單示例
原始代碼
// 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;
}
說(shuō)明:
- 這里只針對(duì)寬度為8的倍數(shù)進(jìn)行了改寫倡缠,實(shí)際代碼中需要對(duì)傳入?yún)?shù)進(jìn)行判斷
- vld1_u8讀取8字節(jié)數(shù)據(jù),vmovl_u8對(duì)讀取的uint8x8進(jìn)行寬度擴(kuò)展
- vreinterpretq_s16_u16對(duì)數(shù)據(jù)類型進(jìn)行強(qiáng)制轉(zhuǎn)換
- vshlq_n_s16對(duì)數(shù)據(jù)進(jìn)行左移處理(P.S. NEON提供了右移指令茎活,但是只能使用整數(shù)常量昙沦。需要根據(jù)變量進(jìn)行右移時(shí),只能使用左移負(fù)數(shù)位的方法妙色。)
- vqmovn_u16對(duì)處理結(jié)果進(jìn)行寬度壓縮
- vst1_u8將處理后的int16x8_t數(shù)據(jù)寫回內(nèi)存
2.進(jìn)階示例
原始代碼
/*
#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;
}
說(shuō)明:
在C實(shí)現(xiàn)中桅滋,每個(gè)結(jié)果需要讀取包括自身在內(nèi)的8個(gè)輸入,乘以相應(yīng)的系數(shù)并累加身辨。最簡(jiǎn)單直觀的實(shí)現(xiàn)方法是
output_16x8 = vmulq_s16( vreinterpretq_s16_u16(vmovl_u8(vld1_u8(&src[x-3]))), vmovl_s8(vld1_s8(ff_hevc_qpel_filters[mx - 1])));
這樣實(shí)現(xiàn)丐谋,會(huì)使得8個(gè)乘積分布在同一個(gè)向量寄存器中,需要通過(guò)取寄存器的不同元素實(shí)現(xiàn)累加煌珊,加法部分無(wú)法并行号俐。
在C實(shí)現(xiàn)中,其數(shù)學(xué)表示為兩個(gè)1x8和8x1的矩陣之間的乘法定庵。分析數(shù)據(jù)間的關(guān)系吏饿,將矩陣乘法轉(zhuǎn)換為矩陣轉(zhuǎn)置乘法,可以得出前文改寫代碼的實(shí)現(xiàn)蔬浙。在該實(shí)現(xiàn)中猪落,由于濾波器系統(tǒng)固定,因此預(yù)先定義了其轉(zhuǎn)置矩陣并擴(kuò)展畴博。在進(jìn)行'乘加'操作的過(guò)程中笨忌,一個(gè)循環(huán)將8個(gè)結(jié)果全部計(jì)算完畢,使得乘法/加法均實(shí)現(xiàn)了并行化俱病。
P.S. 這里官疲,單獨(dú)設(shè)置了8個(gè)向量寄存器變量并展開(kāi)使得代碼較長(zhǎng),使用循環(huán)+數(shù)組的方式也可以得到同樣的結(jié)果亮隙,且代碼較短途凫。但是在底層高頻函數(shù)中,盡量展開(kāi)循環(huán)可以最大化的提升效率溢吻。
結(jié)語(yǔ)
本文只介紹了使用ARM NEON Intrinsics的原理和基本應(yīng)用维费。實(shí)際中需要對(duì)待優(yōu)化的函數(shù)原理及能使用的資源了解清楚才能使用最有效的方法并行化程序。