ARM NEON指令集優化理論與實踐

ARM
NEON指令集優化理論與實踐

一.簡介

NEON就是一種基於SIMD思想的ARM技術,相比於ARMv6或之前的架構,NEON結合了64-bit和128-bit的SIMD指令集,提供128-bit寬的向量運算(vector operations)。NEON技術從ARMv7開始被採用,目前可以在ARM Cortex-A和Cortex-R系列處理器中採用。NEON在Cortex-A7、Cortex-A12、Cortex-A15處理器中被設置爲默認選項,但是在其餘的ARMv7 Cortex-A系列中是可選項。NEON與VFP共享了同樣的寄存器,但它具有自己獨立的執行流水線。

二. NEON寄存器

在這裏插入圖片描述

三. NEON指令集

所有的支持NEON指令都有一個助記符V,下面以32位指令爲例,說明指令的一般格式:

V{}{}{}{.

}{},
src1, src2

Q: The instruction uses
saturating arithmetic, so that the result is saturated within the range
of the specified data type, such as VQABS, VQSHL etc.
H: The instruction will
halve the result. It does this by shifting right by one place
(effectively a divide by two with truncation), such as VHADD, VHSUB.
D: The instruction doubles
the result, such as VQDMULL, VQDMLAL, VQDMLSL and VQ{R}DMULH.
R: The instruction will
perform rounding on the result, equivalent to adding 0.5 to the result
before truncating, such as VRHADD, VRSHR.

- the operation (for example, ADD, SUB, MUL).
- Shape,即前文中的Long
(L), Wide (W), Narrow (N).
- Condition, used with IT instruction.
<.dt> - Data type, such as s8, u8, f32 etc.
- Destination.
- Source operand 1.
- Source operand 2.

注: {} 表示可選的參數。

比如:

VADD.I16
D0, D1, D2 @ 16位加法

VMLAL.S16
Q2, D8, D9 @ 有符號16位乘加

四.NEON支持的指令總結

運算:和、差、積、商
共享的 NEON 和 VFP 指令:涉及加載、多寄存器間的傳送、存儲

五. NEON 優化技術

在利用NEON優化程序時,有下述幾項比較通用的優化技巧。

  1. 降低數據依賴性

在ARM v7-A NEON指令通常需要3~9個指令週期,NEON指令比ARM指令需要更多週期數。因此,爲了減少指令延時,最好避免將當前指令的目的寄存器當作下條指令的源寄存器。如下例所示:

/***************************************************************/

// C代碼

float 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;

}

// NEON實現一

float SumSquareError_NEON1(const float* src_a, const
float* src_b, int count)

{

float sse;

asm volatile (

"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},
[%0]! \n”

“vld1.32 {q2, q3},
[%0]! \n”

“vld1.32 {q12, q13},
[%1]! \n”

“vld1.32 {q14, q15},
[%1]! \n”

"subs       %2, %2, #16                    \n"

// q0, q1, q2,

q3 是vsub的目的地寄存器.

// 也是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;

}

// NEON實現二

float SumSquareError_NEON2(const float* src_a, const
float* src_b, int count)

{

float sse;

asm volatile (

"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},
[%0]! \n”

“vld1.32 {q2, q3},
[%0]! \n”

“vld1.32 {q12, q13},
[%1]! \n”

“vld1.32 {q14, q15},
[%1]! \n”

"subs       %2, %2, #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;

}

/***************************************************************/

在NEON實現一中,我們把目的寄存器立刻當作源寄存器;在NEON實現二中,我們重新排布了指令,並給予目的寄存器儘量多的延時。經過測試實現二比實現一快30%。由此可見,降低數據依賴性對於提高程序性能有重要意義。一個好消息是編譯器能自動調整NEON
intrinsics以降低數據依賴性。這個利用NEON intrinsics的一個很大優勢。

  1. 減少跳轉

NEON指令集沒有跳轉指令,當需要跳轉時,我們需要藉助ARM指令。在ARM處理器中,分支預測技術被廣泛使用。但是一旦分支預測失敗,懲罰還是比較高的。因此我們最好儘量減少跳轉指令的使用。其實,在有些情況下,我們可以用邏輯運算來代替跳轉,如下例所示:

ARM NEON指令集提供了下列指令來幫助用戶實現上述邏輯實現:

/***************************************************************/

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

/***************************************************************/

• VCEQ, VCGE, VCGT, VCLE, VCLT……

• VBIT, VBIF, VBSL……

減少跳轉,不僅僅是在NEON中使用的技巧,是一個比較通用的問題。即使在C程序中,這個問題也是值得注意的。

  1. 其它技巧

在ARM NEON編程時,一種功能有時有多種實現方式,但是更少的指令不總是意味着更好的性能,要依據測試結果和profiling數據,具體問題具體分析。下面列出來我遇到的一些特殊情況。

  1. 浮點累加指令

通常情況下,我們會用VMLA/VMLS來代替VMUL + VADD/
VMUL + VSUB,這樣使用較少的指令,完成更多的功能。但是與浮點VMUL相比,浮點VMLA/VMLS具有更長的指令延時,如果在指令延時中間不能插入其它計算的情況下,使用浮點VMUL + VADD/ VMUL + VSUB反而具有更好的性能。

一個真實例子就是Ne10庫函數的浮點FIR函數。代碼片段如下所示:

實現1:在兩條VMLA指令之間,僅有VEXT指令。而根據指令延時表,VMLA需要9個週期。

實現2:對於qAcc0,依然存在指令延時。但是VADD/VMUL只需要5個週期。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章