1.neon的使用方法
- NEON優化庫(Optimized libraries)
- 向量化編譯器(Vectorizing compilers)
- NEON intrinsics
- NEON assembly
根據優化程度需求不同,第4種最爲底層,若熟練掌握效果最佳,一般也會配合第3種一起使用。本文將會重點介紹第3、4種方法。先簡要介紹前兩種:
(1)Libraries:直接在程序中調用優化
- Ne10:一個ARM的開源項目,提供數學運算、圖像處理、FFT函數等。
- Libyuv :一個包含YUV數據的轉換和擴展功能的開源庫。
- Skia :一個開源的2D圖形庫,用作谷歌Chrome和Chrome OS、Android、Mozilla Firefox和Firefox OS以及其他許多產品的圖形引擎。
- OpenMax DL:支持加速視頻編解碼、信號處理、色彩空間轉換等。
(2)Vectorizing compilers:GCC編譯器的向量優化選項
- 在GCC選項中加入向量化表示能有助於C代碼生成NEON代碼,如
-ftree-vectorize。
2.NEON intrinsics
提供了一個連接NEON操作的C函數接口,編譯器會自動生成相關的NEON指令,支持ARMv7-A或ARMv8-A平臺。所有的intrinsics函數都在:GNU官方說明文檔.
一個簡單的例子:
//add for int array. assumed that count is multiple of 4
#include<arm_neon.h>
// C version
void add_int_c(int* dst, int* src1, int* src2, int count)
{
int i;
for (i = 0; i < count; i++)
dst[i] = src1[i] + src2[i];
}
}
// NEON version
void add_float_neon1(int* dst, int* src1, int* src2, int count)
{
int i;
for (i = 0; i < count; i += 4)
{
int32x4_t in1, in2, out;
in1 = vld1q_s32(src1);
src1 += 4;
in2 = vld1q_s32(src2);
src2 += 4;
out = vaddq_s32(in1, in2);
vst1q_s32(dst, out);
dst += 4;
}
}
代碼中的vld1q_s32
會被編譯器轉換成vld1.32 {d0, d1}, [r0]
指令,同理vaddq_s32
和vst1q_s32
被轉換成vadd.i32 q0, q0, q0
,vst1.32 {d0, d1}, [r0]
。若不清楚指令意義,請參見ARM® Compiler armasm User Guide - Chapter 12 NEON and VFP Instructions。
3.NEON asssembly
NEON可以有兩種寫法:
(1)Assembly文件
- 純彙編文件,後綴爲”.S”或”.s”。注意對寄存器數據的保存。具體對通用寄存器的詳解不是本文的重點,有興趣的讀者請自行補充該部分知識。
(2)inline assembly內聯彙編
- 優點:在C代碼中嵌入彙編,調用簡單,無需手動存儲寄存器
- 缺點:有較爲複雜的格式需要事先學習,不好移植到其他語言環境。
比如上述intrinsics代碼產生的彙編代碼爲:
// ARMv7-A/AArch32
void add_float_neon2(int* dst, int* src1, int* src2, int count)
{
asm volatile (
"1: \n"
"vld1.32 {q0}, [%[src1]]! \n"
"vld1.32 {q1}, [%[src2]]! \n"
"vadd.f32 q0, q0, q1 \n"
"subs %[count], %[count], #4 \n"
"vst1.32 {q0}, [%[dst]]! \n"
"bgt 1b \n"
: [dst] "+r" (dst)
: [src1] "r" (src1), [src2] "r" (src2), [count] "r" (count)
: "memory", "q0", "q1"
);
}
4.NEON優化心得
建議的NEON調優步驟:
-
理清所需的寄存器、指令。 建議根據要實現的任務,畫出數據變換流程,和每步所需的具體指令,儘可能找到最優的實現流程。這一步非常關鍵,如果思路出錯或是不夠優化,則會影響使用NEON的效果,並且對程序修改帶來麻煩,一定要找到最優的實現算法哦~
-
先實現intrinsics(可選)。 初學者先實現intrinsics是有好處的,字面理解性更強,且有助於理解NEON指令。建議隨時打印關鍵步驟的數據,以檢查程序的正誤。
-
寫成彙編進一步優化。 將intrinsics生成的彙編代碼進行優化調整。一般來說,有以下幾點值得注意【乾貨】:
- 只要intrinsics運算指令足夠精簡,運算類的彙編指令就不用大修;
- 大部分的問題會出在存取、移動指令的濫用、混亂使用上;
- 優化時要儘量減少指令間的相關性,包括結構相關、數據相關控制相關,保證流水線執行效率更高;
- 大概估算所有程序指令取指、執行、寫回的總理論時間,以此估算本程序可以優化的空間;
- 熟練對每條指令準備發射、寫回時間有一定的認識,有助於對指令的優化排序;
- 一定要多測試不同指令的處理時間!!原因是你所想跟實際有出入,且不同的編譯器優化的效果可能也有些不同;
- 一定要有一定的計算機體系結構基礎,對存儲結構、流水線有一定的體會!!
【注意】在此筆者溫馨提示各位看官(⊙o⊙)不僅是NEON,所有的性能優化是個經驗活兒,需要自己動手才能領悟更多的訣竅,總結一下NEON優化就是:
- 第一優化算法實現流程;
- 第二優化程序存取;
- 第三優化程序執行;
- 第四哪兒能優化,就優化哪兒~~
對NEON優化使用的好壞直接導致優化效果,優化效果好的會節省70%以上的時間。
5.內聯彙編使用心得
當讀者熟練後就可以直接上手內聯彙編了。時間有限,本文中不具體介紹inline assembly的使用方法,我後續可能會將這部分單獨寫成一篇博客。感興趣者請參見ARM GCC Inline Assembler Cookbook
一些使用心得:
- inline assembly下面的三個冒號一定要注意
- output/input registers的寫法一定要寫對,clobber list也一定要寫完全,否則會造成令你頭疼的問題 (T-T) …
- 這個問題在給出的cookbook中也有介紹,但是並不全面,有些問題只有自己碰到了再去解決。 筆者就曾經被虐了很久,從生成的彙編發現編譯器將寄存器亂用,導致指針操作完全混亂,毫無頭緒…
- 一般情況下建議的寫法舉例:
asm volatile (
... /* assembly code */
: "+r"(arg0) // %0
"+r"(arg1) // %1 // Output Registers
: "r"(arg2) // %2 // Input Registers
: "cc", "memory", r0, r1
- 傳入內聯彙編程序段的C參數是有限的
- 筆者親測對於Cortex-A7平臺output/input registers基本在9以內纔可保證,否則會報出
can't find a register in class 'GENERAL_REGS' while reloading 'asm'
錯誤。