【genius_platform软件平台开发】第四点:ARM NEON Intrinsics 使用详解
目錄
前言
SIMD簡介
ARM NEON Intrinsics簡介
函數改寫示例
結語
前言
最近公司在視頻直播項目中要使用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)
其它可能用到的助記符包括:
- l 長指令,數據擴展
- w 寬指令,數據對齊
- n 窄指令, 數據壓縮
示例2
- uint8x8_t vld1_u8 (const uint8_t *)
函數改寫示例
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 regresult_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 datavst1q_s16(&tmp[x], result_16x8);}src += srcstride;tmp += MAX_PB_SIZE; }說明:
在C實現中,每個結果需要讀取包括自身在內的8個輸入,乘以相應的系數并累加。最簡單直觀的實現方法是
這樣實現,會使得8個乘積分布在同一個向量寄存器中,需要通過取寄存器的不同元素實現累加,加法部分無法并行。
在C實現中,其數學表示為兩個1x8和8x1的矩陣之間的乘法。分析數據間的關系,將矩陣乘法轉換為矩陣轉置乘法,可以得出前文改寫代碼的實現。在該實現中,由于濾波器系統固定,因此預先定義了其轉置矩陣并擴展。在進行'乘加'操作的過程中,一個循環將8個結果全部計算完畢,使得乘法/加法均實現了并行化。
P.S. 這里,單獨設置了8個向量寄存器變量并展開使得代碼較長,使用循環+數組的方式也可以得到同樣的結果,且代碼較短。但是在底層高頻函數中,盡量展開循環可以最大化的提升效率。
結語
本文只介紹了使用ARM NEON Intrinsics的原理和基本應用。實際中需要對待優化的函數原理及能使用的資源了解清楚才能使用最有效的方法并行化程序。
總結
以上是生活随笔為你收集整理的【genius_platform软件平台开发】第四点:ARM NEON Intrinsics 使用详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重生,放手去爱!
- 下一篇: 以连边为中心的功能连接用于个体识别