官網(wǎng)介紹:
直觀認(rèn)識
NEON整體描述
Arm NEON technology is an advanced SIMD (single instruction multiple data) architecture extension for the Arm Cortex-A series and Cortex-R52 processors.

NEON technology was introduced to the Armv7-A and Armv7-R profiles. It is also now an extension to the Armv8-A and Armv8-R profiles.
NEON technology is intended to improve the multimedia user experience by accelerating audio and video encoding/decoding, user interface, 2D/3D graphics or gaming. NEON can also accelerate signal processing algorithms and functions to speed up applications such as audio and video processing, voice and facial recognition, computer vision and deep learning.
其他具體的細(xì)節(jié),在官網(wǎng)還有很多描述,自己去看吧!
持續(xù)優(yōu)化
初識
NEON是SIMD架構(gòu)下的一個(gè)加速部分,其實(shí)本來是給多媒體做的,因?yàn)槎嗝襟w計(jì)算量大,而且計(jì)算的方式比較單一,因此那伙設(shè)計(jì)cpu的家伙可能就想,我能不能針對真快做個(gè)并行部分?
于是這玩意就出來了,嗯!就是這么來的(我瞎掰的)。
詳細(xì)的教程如下系列:
安卓開發(fā)看這個(gè)入手:ARM NEON programming quick reference
Coding for NEON - Part 1: Load and Stores
Coding for NEON - Part 2: Dealing With Leftovers
Coding for NEON - Part 3: Matrix Multiplication
Coding for NEON - Part 4: Shifting Left and Right
Coding for NEON - Part 5: Rearranging Vectors
實(shí)例
ARM NEON技術(shù)在車位識別算法中的應(yīng)用
這里面講到了兩個(gè)核心點(diǎn):
- 循環(huán)的時(shí)候,可以把循環(huán)展開,從而稀釋掉循環(huán)判斷在占用時(shí)間中的比例,這個(gè)優(yōu)化的是CPU的計(jì)算時(shí)間;
- 在數(shù)據(jù)運(yùn)算的時(shí)候,比如乘法,我們可以把剛剛第一步展開的4個(gè)32bit一次性丟進(jìn)Q寄存器,然后一次就計(jì)算完了,理論上是四倍計(jì)算加速??!
總結(jié):雖然講到了加速,而且還講了點(diǎn)編譯器的相關(guān)優(yōu)化,但是都還不夠geek,不夠徹底,不夠爽!
你看看下面這一篇,就知道什么叫做嚴(yán)謹(jǐn),什么叫做geek了!
從一個(gè)復(fù)數(shù)點(diǎn)積算法看NEON的匯編優(yōu)化
這里面講到了指令重排、微架構(gòu)、流水線等很深的東西,不懂,先知道設(shè)計(jì)原則就好。
其中尤其關(guān)注的點(diǎn)是:指令流水排布
- 那指令流水排布跟啥有關(guān)?即主要需要注意不要引起CPU的Hazard。
- 介紹Hazard的連接在這:鏈接。
Hazard
簡短說下:在CPU微架構(gòu)里,當(dāng)下一條指令無法在接下來的時(shí)鐘周期內(nèi)被執(zhí)行,由此導(dǎo)致的指令流水線問題就叫做Hazard。
栗子??:你看哈,我現(xiàn)在是往Q0里面寫數(shù)據(jù),然后進(jìn)入了流水線(假設(shè)是分取、算、存等步驟),下一條指令馬上就是從Q0里面取出數(shù)據(jù)進(jìn)行運(yùn)算,那么很有可能我取的數(shù)據(jù)還是Q0原來的數(shù)據(jù)?。∫?yàn)橛捎诹魉€的原因,很有可能上面的Q0還沒寫回去,我就把數(shù)據(jù)給讀出來了!
看下面的圖(可治陳年老頸椎):

你看上面一條指令假設(shè)需要花很多個(gè)指令周期(NEON指令確實(shí)如此),那么就被推入了流水線,并分割為好多的小步驟,其中最后一個(gè)步驟就是把數(shù)據(jù)寫回Q0,讓當(dāng)前這條指令的下一句就是先取Q0的值,然后做運(yùn)算啥的,你從圖上看,是不是很有可能上一條指令的Q0還沒寫進(jìn)去,我下面的Q0就讀出來啦!因此在微架構(gòu)里面叫這玩意做:Hazard。
當(dāng)然咯!這種情況肯定是不允許出現(xiàn)的,具體怎么解我不知道,我知道的是,肯定得等Q0寫完才能讀,因此勢必流水線就會被破壞,勢必就有資源會被作為犧牲品,因此這就是為什么不提倡上述這種操作方式,再下面的NEON優(yōu)化部分,我會有更專業(yè)的操作來驗(yàn)證。
講了Ne10這個(gè)庫的結(jié)構(gòu),并教你怎么編譯啊、使用啊啥的,如文章名所示,就是一個(gè)入門文章。
這個(gè)就是實(shí)例了,講具體在安卓啊,蘋果啊環(huán)境下是如何使用的。
什么?。磕阋€要優(yōu)化NEON?
Introduction
如其名,就是Neon優(yōu)化的,講了一些實(shí)踐技巧。hin重要?。?/p>
Skill1:Remove data dependencies
在ARMv7-A平臺(==注意,其他平臺不一定是這個(gè)效果了,需要去精調(diào)的哈==)下,NEON指令通常比ARM標(biāo)準(zhǔn)指令集需要更多的指令周期。
(原文:On ARMv7-A platform, NEON instructions usually take more cycles than ARM instructions. To reduce instruction latency, it’s better to avoid using the destination register of current instruction as the source register of next instruction.)
因此,為了減少指令延時(shí)時(shí)間,避免使用當(dāng)前指令的目地寄存器作為下一條指令的源寄存器。
這里我大膽猜測下:為什么當(dāng)前指令的目的寄存器作為下一條指令的源寄存器會增加延時(shí)?我覺得是由于流水線的問題,你想啊,NEON指令要的周期數(shù)比ARM指令要多,也就是是說在指令流水線里面呆的時(shí)間越長,因此為了保證數(shù)據(jù)不會出現(xiàn)Hazard(見上面的wiki解釋),于是我剛進(jìn)流水線我就得把流水線給清了,這無疑是極大浪費(fèi)資源跟效率的。本人拙見,請點(diǎn)評。
C語言實(shí)現(xiàn)版本:
就是兩塊內(nèi)存中的數(shù)據(jù)求差分值,然后平方和,就是求均方差的函數(shù),在求出差分(寫入作為目的寄存器)后馬上又要使用差分值平方(平方作為源寄存器)。
loat SumSquareError_C(const float* src_a, const float* src_b, int count)
{
float sse = 0u;
int i;
for (i = 0; i < count; ++i) {
float diff = src_a[i] - src_b[i];
sse += (float)(diff * diff);
}
return sse;
}
匯編實(shí)現(xiàn)版本1:
這里幾乎是常規(guī)的寫法,你看注釋那里,可以看到q0是vsub的目的寄存器,馬上又是下面vmla的源寄存器了,所以這里是會打斷流水線的(粗略解釋見上面Hazard部分)。
float SumSquareError_NEON1(const float* src_a, const float* src_b, int count)
{
float sse;
asm volatile (
// Clear q8, q9, q10, q11
"veor q8, q8, q8 \n"
"veor q9, q9, q9 \n"
"veor q10, q10, q10 \n"
"veor q11, q11, q11 \n"
"1: \n"
"vld1.32 {q0, q1}, [%[src_a]]! \n"
"vld1.32 {q2, q3}, [%[src_a]]! \n"
"vld1.32 {q12, q13}, [%[src_b]]! \n"
"vld1.32 {q14, q15}, [%[src_b]]! \n"
"subs %[count], %[count], #16 \n"
// q0, q1, q2, q3 are the destination of vsub.
// they are also the source of vmla.
"vsub.f32 q0, q0, q12 \n"
"vmla.f32 q8, q0, q0 \n"
"vsub.f32 q1, q1, q13 \n"
"vmla.f32 q9, q1, q1 \n"
"vsub.f32 q2, q2, q14 \n"
"vmla.f32 q10, q2, q2 \n"
"vsub.f32 q3, q3, q15 \n"
"vmla.f32 q11, q3, q3 \n"
"bgt 1b \n"
"vadd.f32 q8, q8, q9 \n"
"vadd.f32 q10, q10, q11 \n"
"vadd.f32 q11, q8, q10 \n"
"vpadd.f32 d2, d22, d23 \n"
"vpadd.f32 d0, d2, d2 \n"
"vmov.32 %3, d0[0] \n"
: "+r"(src_a),
"+r"(src_b),
"+r"(count),
"=r"(sse)
:
: "memory", "cc", "q0", "q1", "q2", "q3", "q8", "q9", "q10", "q11",
"q12", "q13","q14", "q15");
return sse;
}
匯編實(shí)現(xiàn)版本2:
你看這個(gè)操作就合理多了,把減法操作跟平方操作不連起來,在中間穿插其他的操作,從而保證Q0在被取的時(shí)候,流水線肯定把Q0的值寫進(jìn)去了,也就說留出足夠的時(shí)間出來了。
float SumSquareError_NEON2(const float* src_a, const float* src_b, int count)
{
float sse;
asm volatile (
// Clear q8, q9, q10, q11
"veor q8, q8, q8 \n"
"veor q9, q9, q9 \n"
"veor q10, q10, q10 \n"
"veor q11, q11, q11 \n"
"1: \n"
"vld1.32 {q0, q1}, [%[src_a]]! \n"
"vld1.32 {q2, q3}, [%[src_a]]! \n"
"vld1.32 {q12, q13}, [%[src_b]]! \n"
"vld1.32 {q14, q15}, [%[src_b]]! \n"
"subs %[count], %[count], #16 \n"
"vsub.f32 q0, q0, q12 \n"
"vsub.f32 q1, q1, q13 \n"
"vsub.f32 q2, q2, q14 \n"
"vsub.f32 q3, q3, q15
\n"
"vmla.f32 q8, q0, q0 \n"
"vmla.f32 q9, q1, q1 \n"
"vmla.f32 q10, q2, q2 \n"
"vmla.f32 q11, q3, q3 \n"
"bgt 1b \n"
"vadd.f32 q8, q8, q9 \n"
"vadd.f32 q10, q10, q11 \n"
"vadd.f32 q11, q8, q10 \n"
"vpadd.f32 d2, d22, d23 \n"
"vpadd.f32 d0, d2, d2 \n"
"vmov.32 %3, d0[0] \n"
: "+r"(src_a),
"+r"(src_b),
"+r"(count),
"=r"(sse)
:
: "memory", "cc", "q0", "q1", "q2", "q3", "q8", "q9", "q10", "q11",
"q12", "q13","q14", "q15");
return sse;
}
結(jié)論: 在當(dāng)前的ARMv7平臺下測試效果是匯編版本2比版本1快了約30%,此外,intrinsics方式寫的NEON指令可以通過編譯器來進(jìn)行精調(diào)。
Note: this test runs on Cortex-A9. The result may be different on other platforms.
Skill2:Reduce branches
在NEON的指令集里面是沒有分支跳轉(zhuǎn)指令的,如果你硬是要跳轉(zhuǎn)的話那就得用ARM指令集的了;
在ARM處理器中分支預(yù)測技術(shù)被廣泛應(yīng)用,但是一旦分支預(yù)測失敗了,那花費(fèi)的代價(jià)尤其高??!(為啥呢?我認(rèn)為是....我怎么知道?。∥矣植皇窃O(shè)計(jì)ARM內(nèi)核的,但是我們可以知道的是:你買股票預(yù)測錯(cuò)了,你肯定是會有所損失的,搞不好還會上天臺的...)
因此避免使用跳轉(zhuǎn)指令是個(gè)明智的選擇,但是實(shí)際過程中總的要分支的吧,怎么整?唯有等效替換,比如用邏輯操作代替分支選擇。
talk less, show me the code.(亮代碼吧!兄嘚~)
C語言版本:
if( flag )
{
dst[x * 4] = a;
dst[x * 4 + 1] = a;
dst[x * 4 + 2] = a;
dst[x * 4 + 3] = a;
}
else
{
dst[x * 4] = b;
dst[x * 4 + 1] = b;
dst[x * 4 + 2] = b;
dst[x * 4 + 3] = b;
}
NEON版本:
//dst[x * 4] = (a&Eflag) | (b&~Eflag);
//dst[x * 4 + 1] = (a&Eflag) | (b&~Eflag);
//dst[x * 4 + 2] = (a&Eflag) | (b&~Eflag);
//dst[x * 4 + 3] = (a&Eflag) | (b&~Eflag);
VBSL qFlag, qA, qB
VBSL(按位選擇):
- 如果目標(biāo)的對應(yīng)位為1,則該指令從第一個(gè)操作數(shù)中選擇目標(biāo)的每一位;
- 如果目標(biāo)的對應(yīng)位為 0,則從第二個(gè)操作數(shù)中選擇目標(biāo)的每一位。
ARM NEON 指令集提供以下指令來幫助用戶實(shí)現(xiàn)上述的邏輯操作:
VCEQ, VCGE, VCGT, VCLE, VCLT……
VBIT, VBIF, VBSL……
小結(jié):減少分支并不是僅僅對NEON有用,這是一個(gè)對所有代碼(指令集)都有用的小trick啦~ 甚至連在c語言里面這都是一條很有效的準(zhǔn)則:少用分支。
Skill3:Preload data-PLD
ARM處理器是加載/存儲的系統(tǒng)(load/store system)。
除了加載/存儲指令集,其他的操作都是對寄存器的,因此提高加載/存儲的效率對優(yōu)化應(yīng)用具有很高的實(shí)際意義。
預(yù)加載指令允許處理器去通知內(nèi)存系統(tǒng),告訴他:hey!兄弟,我很有可能待會得來這個(gè)地址取個(gè)東西,先幫我準(zhǔn)備好,ok?
假如數(shù)據(jù)被正確預(yù)加載至cache了,于是cache 的hit rate將會極大提高,因此性能就極大提高了!
但是,預(yù)取也不是百試百靈的,在新的處理器里面預(yù)取非常難用,而且一個(gè)預(yù)取的不好就會降低性能的。
PLD syntax:
PLD{cond} [Rn {, #offset}]
PLD{cond} [Rn, +/-Rm {, shift}]
PLD{cond} label
Where:
Cond - is an optional condition code.
Rn - is the register on which the memory address is based.
Offset - is an immediate offset. If offset is omitted, the address is the value in Rn.
Rm - contains an offset value and must not be PC (or SP, in Thumb state).
Shift - is an optional shift.
Label - is a PC-relative expression.
PLD操作的一些特征:
- 獨(dú)立于加載/存儲的的指令運(yùn)行(Independent of load and store instruction execution);
- 當(dāng)處理器在持續(xù)執(zhí)行其他指令的時(shí)候,預(yù)加載是在后臺運(yùn)行的;
- 偏移量指定為實(shí)際情況(The offset is specified to real cases)。
Skill4:Misc
在ARM NEON編程里面,不同的指令序列能實(shí)現(xiàn)同樣的操作;但是更少的指令并不總是意味著更好的性能(也就是說指令數(shù)量跟性能不是絕對的線性對應(yīng)關(guān)系)。
那指令數(shù)量跟性能的關(guān)系是基于什么呢?
基于特定情況下的benchmark and profiling result(基準(zhǔn)和分析結(jié)果),如下就是一些特定情況下的實(shí)踐分析。
Floating-point VMLA/VMLS instruction
注意咯:這里的數(shù)據(jù)僅對Cortex-A9平臺有效,對于其他的平臺結(jié)果就需要重新評估啦!
通常,VMUL+VADD/VMUL+VSUB指令能夠被VMLA/VMLS指令替換,因?yàn)橹噶顢?shù)量更少了,更精簡了。
但是,對比于浮點(diǎn)VMUL操作,浮點(diǎn)VMLA/VMLS操作有更長的指令delay,假如在這段delay空隙中沒有其他的指令能夠插入的話,使用浮點(diǎn)VMUL+VADD/VMUL+VSUB操作將會表現(xiàn)出更好的性能。
問題:如何會更好?我認(rèn)為還是一個(gè)流水線的問題罷!
舉個(gè)實(shí)例:Ne10庫中的浮點(diǎn)FIR函數(shù),代碼片段如下所述:
Implementation 1: VMLA
這里在VMLA之間只有一個(gè)VEXT指令,而VMLA則需要9個(gè)指令周期的延時(shí)(according to the table of NEON floating-point instructions timing)
Implementation 1: VMLA
VEXT qTemp1,qInp,qTemp,#1
VMLA qAcc0,qInp,dCoeff_0[0]
VEXT qTemp2,qInp,qTemp,#2
VMLA qAcc0,qTemp1,dCoeff_0[1]
VEXT qTemp3,qInp,qTemp,#3
VMLA qAcc0,qTemp2,dCoeff_1[0]
VMLA qAcc0,qTemp3,dCoeff_1[1]
Implementation 2: VMUL+VADD
這里仍然還有qAcc0的數(shù)據(jù)依賴缺(就是說流水線里面的Hazard,見前文的分析),但是 VADD/VMUL只需要5個(gè)指令周期哈!
Implementation 2: VMUL+VADD
VEXT qTemp1,qInp,qTemp,#1
VMLA qAcc0,qInp,dCoeff_0[0] ]
VMUL qAcc1,qTemp1,dCoeff_0[1]
VEXT qTemp2,qInp,qTemp,#2
VMUL qAcc2,qTemp2,dCoeff_1[0]
VADD qAcc0, qAcc0, qAcc1
VEXT qTemp3,qInp,qTemp,#3
VMUL qAcc3,qTemp3,dCoeff_1[1]
VADD qAcc0, qAcc0, qAcc2
VADD qAcc0, qAcc0, qAcc3
小結(jié):實(shí)測第二個(gè)版本有更好的性能。
上述僅是代碼的一部分,想要詳細(xì)的代碼請看這:Github上Ne10庫的源碼
具體實(shí)現(xiàn)代碼位置在:modules/dsp/NE10_fir.neon.s:line 195
下面是上面提到的,指令周期對照表:

總結(jié):優(yōu)化手段總結(jié)如下
- 盡可能地利用指令之間的延時(shí)空隙;
- 避免使用分支;
- 關(guān)注cache hit;
NEON assembly and intrinsics
NEON匯編方式跟intrinsics方式對比如下:

我們可以看到是分三個(gè)方面進(jìn)行對比的:
性能:
- assembly: 對有經(jīng)驗(yàn)的開發(fā)者來說,針對特定平臺的匯編代碼總是有最佳的性能表現(xiàn),
- intrinsics: 然而intrinsics方式則嚴(yán)重依賴于使用的工具鏈;
可移植性:
- assembly: 不同的指令集架構(gòu)(ISA: Instruction Set Architecture)有不同的匯編實(shí)現(xiàn);甚至是在同樣的指令集架構(gòu)下,不同微架構(gòu)的匯編代碼都可能需要精調(diào)來實(shí)現(xiàn)理想的性能;
- intrinsics: 編程一次,即可適用于所有的指令集架構(gòu),編譯器甚至?xí)紤]到不同的微架構(gòu)來進(jìn)行性能精調(diào);
操作性:
- assembly: 對比于C來說是很難讀寫的咯!
- intrinsics: 跟C語言類似,讀寫容易;
小結(jié):但是現(xiàn)實(shí)情況是遠(yuǎn)比這復(fù)雜的,尤其當(dāng)碰到ARMv7-A/v8-A 跨平臺問題時(shí),因此接下來我們針對這給出些栗子來進(jìn)行分析。
編寫代碼
對于NEON的初學(xué)者,內(nèi)聯(lián)函數(shù)的方式是比匯編更容易的,但是有經(jīng)驗(yàn)的開發(fā)者(比如我....霧)可能對NEON匯編編程更為熟悉,畢竟我們需要時(shí)間去適應(yīng)內(nèi)聯(lián)函數(shù)的編碼方式?。。。?!
一些在真實(shí)開發(fā)過程中可能會出現(xiàn)的問,現(xiàn)描述如下:

Flexibility of instruction
使用匯編的方式可能會更靈活,主要體現(xiàn)在數(shù)據(jù)的load/store。
當(dāng)然這個(gè)不足,可以在將來的編譯器升級過程中進(jìn)行解決。
有時(shí)候,編譯器是有能力將兩條內(nèi)聯(lián)函數(shù)指令優(yōu)化為一條匯編指令的,比如:

因此,伴隨著ARMv8工具連的升級,有望使得內(nèi)聯(lián)函數(shù)方式有跟匯編一樣的靈活度;
Register allocation:
當(dāng)使用NEON匯編編程時(shí),寄存器必須由用戶分配,因此你必須清楚地知道哪個(gè)寄存器被占用了;
使用內(nèi)聯(lián)函數(shù)方式的好處之一是,你只管定義變量,不用管它的安全性,因?yàn)榫幾g器會自動分配寄存器的,這是一個(gè)優(yōu)點(diǎn),但是有時(shí)候也是缺點(diǎn);
實(shí)踐證明:在內(nèi)聯(lián)函數(shù)編程模式下同時(shí)使用太多的NEON寄存器會使得gcc編譯器產(chǎn)生寄存器分配異常。當(dāng)這種情況發(fā)生時(shí),許多的數(shù)據(jù)都被推進(jìn)棧區(qū)(為啥?你心里沒點(diǎn)B數(shù)么?NEON寄存器總共就這么多,提這么多的要求,消化不下啦,只能放到郊區(qū)的內(nèi)存里面去啦,郊區(qū)那么遠(yuǎn),你說浪費(fèi)時(shí)間不?。。。?,這將會極大地降低程序的性能。
因此當(dāng)使用內(nèi)聯(lián)函數(shù)編程時(shí),得好好考慮這個(gè)問題,當(dāng)出現(xiàn)性能異常的時(shí)候,比如C的性能居然比NEON的還要好,這個(gè)時(shí)候你首先要做的就是反匯編,來確認(rèn)是不是出現(xiàn)寄存器分配問題了!
對于ARMv8-A AArch64,有更多的NEON寄存器(32個(gè) 128bit NEON寄存器),因此對于寄存器分配問題的影響就較低了!
Performance and compiler
在一個(gè)特定的平臺下,NEON匯編的的性能表現(xiàn)僅僅取決于其實(shí)現(xiàn)代碼,與編譯器鳥關(guān)系都沒有的?。『锰幘褪悄隳茴A(yù)測并估計(jì)你手調(diào)代碼的性能表現(xiàn),這很正常嘛!
相反的,內(nèi)聯(lián)函數(shù)的性能嚴(yán)重依賴于使用的編譯器。不同的編譯器會帶來不同的性能,通常是老編譯器性能會有最差的性能,同時(shí)使用老編譯器時(shí)你要注意你內(nèi)聯(lián)函數(shù)的兼容性??!
精調(diào)代碼的時(shí)候,你不能預(yù)測和控制性能,但是偶爾也會有驚喜喲!,有時(shí)候內(nèi)聯(lián)函數(shù)的性能反而超過匯編方式,這不是不會出現(xiàn),但是可以說是“罕見”。
編譯器將會在NEON優(yōu)化的過程中產(chǎn)生影響,下面這張圖描述了NEON實(shí)現(xiàn)和優(yōu)化的通常過程:

NEON匯編和內(nèi)聯(lián)方式有同樣的實(shí)現(xiàn)過程,編碼-調(diào)試-性能測試,但是他們卻有不同的優(yōu)化步驟:
匯編精調(diào)的方法如下:
- 改變實(shí)現(xiàn)方式,比如改變指令、調(diào)節(jié)并行度;
- 調(diào)整指令序列來減少數(shù)據(jù)依賴性(上面已經(jīng)分析過了,就是怕流水線斷掉);
- 試試我前面提到的那些skills
當(dāng)精調(diào)匯編代碼的時(shí)候,一個(gè)經(jīng)驗(yàn)之談(復(fù)雜的、富有經(jīng)驗(yàn)的)是:
- 明確知道使用指令的數(shù)量;
- 使用PMU(
Performance Monitoring Unit)來獲取執(zhí)行周期; - 基于已使用指令的時(shí)間消耗來調(diào)整指令的序列,并盡你所能地最小化指令延時(shí)(延時(shí)間隙插入指令);
這種方式的的缺點(diǎn)是優(yōu)化僅僅是針對于某個(gè)micro-architecture的,移植性不好啊!同時(shí)對于相對較小的收益來說,這也是非常耗時(shí)的,也就是性價(jià)比不是很高啊!
NEON intrinsics 精調(diào)的方法更難喲!:
- 使用匯編優(yōu)化里面的那一套方法,整一遍試一下!
- 反匯編看數(shù)據(jù)依賴跟寄存器使用情況;
- 檢查性能是否滿足期望,如果沒有,那就換個(gè)編譯器再來一遍,知道性能滿足期望了!
當(dāng)移植ARMv7-A的匯編代碼到ARMv7-A/v8-A進(jìn)行兼容時(shí),匯編代碼的性能可以作為一個(gè)參考,因此我們很容易就知道何時(shí)算優(yōu)化結(jié)束。
然而,當(dāng)內(nèi)聯(lián)函數(shù)方式來優(yōu)化ARMv8-A的代碼時(shí),是沒有性能參考點(diǎn)的,因此很難確定當(dāng)前的性能是否是最優(yōu)值;
基于ARMv7-A的經(jīng)驗(yàn),可能有這樣的疑問:是不是匯編就一定有更好的性能呢?我認(rèn)為隨著 ARMv8-A 環(huán)境的成熟化,內(nèi)聯(lián)函數(shù)將會有更好的性能。
Cross-platform and portability
許多現(xiàn)已存的NEON匯編代碼,僅能在ARMv7-A/ARMv8-A平臺下的AArch32模式下運(yùn)行,假如你想讓這些代碼在ARMv8-A AArch64模式下運(yùn)行,你必須重寫這些代碼,這需要花費(fèi)很多的功夫啊!

在這樣的情形下,假如代碼是用內(nèi)聯(lián)函數(shù)方式實(shí)現(xiàn)的,它們能夠在ARMv8-A AArch64 模式下直接運(yùn)行。
跨平臺是一個(gè)明顯的優(yōu)勢,同時(shí),不同平臺你僅僅需要保持一份代碼,這大大減少了維護(hù)工作。
然而,由于ARMv7-A/ARMv8-A平臺不同的硬件資源(Q寄存器數(shù)量的差異),有時(shí)候即使是使用內(nèi)聯(lián)函數(shù),但還是需要兩套代碼的。
下面以Ne10工程下的FFT實(shí)現(xiàn)為例子:
// radix 4 butterfly with twiddles
scratch[0].r = scratch_in[0].r;
scratch[0].i = scratch_in[0].i;
scratch[1].r = scratch_in[1].r * scratch_tw[0].r - scratch_in[1].i * scratch_tw[0].i;
scratch[1].i = scratch_in[1].i * scratch_tw[0].r + scratch_in[1].r * scratch_tw[0].i;
scratch[2].r = scratch_in[2].r * scratch_tw[1].r - scratch_in[2].i * scratch_tw[1].i;
scratch[2].i = scratch_in[2].i * scratch_tw[1].r + scratch_in[2].r * scratch_tw[1].i;
scratch[3].r = scratch_in[3].r * scratch_tw[2].r - scratch_in[3].i * scratch_tw[2].i;
scratch[3].i = scratch_in[3].i * scratch_tw[2].r + scratch_in[3].r * scratch_tw[2].i;
以上的代碼片段列出了FFT的基本操作單元-radix4 butterfly,從代碼里面我們能推出如下信息:
- 假如2個(gè)
radix4 butterflies在1個(gè)循環(huán)里面執(zhí)行,則需要20個(gè)64-bit NEON寄存器; - 假如4個(gè)
radix4 butterflies在1個(gè)循環(huán)里面執(zhí)行,則需要20個(gè)128-bit NEON寄存器
而且, ARMv7-A/v8-A AArch32 and v8-A AArch64資源如下:
ARMv7-A/v8-A AArch32有:32 64-bit or 16 128-bit NEON registers.ARMv8-A AArch64 有:32 128-bit NEON registers.
考慮到上述因素,Ne10庫的FFT實(shí)現(xiàn)代碼里面,最終是有添加個(gè)匯編版本的:
- 匯編版本是用于
ARMv7-A/v8-A AAch32,里面是一個(gè)循環(huán)內(nèi)執(zhí)行2 radix4 butterflies; - 內(nèi)聯(lián)函數(shù)版本用于
ARMv8-A AArch64,里面是一個(gè)循環(huán)內(nèi)執(zhí)行4 radix4 butterflies;
上述實(shí)例表明:當(dāng)維護(hù)一份跨平臺(ARMv7-A/v8-A)的代碼時(shí),你需要關(guān)注一些例外情況。
總結(jié)
內(nèi)聯(lián)函數(shù)優(yōu)化的越來越好了,甚至在ARMv8 平臺下有優(yōu)于匯編的性能,同時(shí)兼容性方面又比匯編好,因此使用內(nèi)聯(lián)函數(shù)是上上之選。
畢竟,NEON肯定會更新的,到時(shí)一更新你的底層匯編得全部跟著更新,但是使用內(nèi)聯(lián)函數(shù)的話就不要考慮這些了,反正編譯器都幫我們做了嘛!
最后關(guān)于內(nèi)聯(lián)函數(shù)告訴后輩們幾點(diǎn)人生經(jīng)驗(yàn):
- 使用的寄存器數(shù)量要考慮周全;
- 編譯器注意好啊!
- 一定要看看產(chǎn)生的匯編代碼啊!