姜堰 萬(wàn)邦建設(shè)集團(tuán)網(wǎng)站/seosem是什么職位
NEON優(yōu)化:性能優(yōu)化經(jīng)驗(yàn)總結(jié)
- 1. 什么是 NEON
- Arm Adv SIMD 歷史
- 2. 寄存器
- 3. NEON 命名方式
- 4. 優(yōu)化技巧
- 5. 優(yōu)化 NEON 代碼(Armv7-A內(nèi)容,但區(qū)別不大)
- 5.1 優(yōu)化 NEON 匯編代碼
- 5.1.1 Cortex-A 處理器之間的 NEON 管道差異
- 5.1.2 內(nèi)存訪問(wèn)優(yōu)化
Reference:
- NEON優(yōu)化:性能優(yōu)化經(jīng)驗(yàn)總結(jié)
- NEON官方內(nèi)聯(lián)函數(shù)
- Arm NEON programming quick reference
- Learn the architecture - Neon programmers’ guide
1. 什么是 NEON
NEON 技術(shù)是用于 Arm Cortex-A
系列處理器的先進(jìn) SIMD(單指令多數(shù)據(jù))架構(gòu)。它可以加速多媒體和信號(hào)處理算法,如視頻編碼器/解碼器、2D/3D圖形、游戲、音頻和語(yǔ)音處理、圖像處理、電話和聲音。
NEON 指令執(zhí)行“打包 SIMD”處理:
- 寄存器被認(rèn)為是相同數(shù)據(jù)類型元素的向量
- 數(shù)據(jù)類型支持:帶符號(hào)/無(wú)符號(hào) 8 8 8 位, 16 16 16 位, 32 32 32 位, 64 64 64 位,ARM 32 32 32位平臺(tái)上的單精度浮點(diǎn)數(shù),ARM 64 64 64位平臺(tái)上的單精度浮點(diǎn)數(shù)和雙精度浮點(diǎn)數(shù)。
- 指令在所有通道中執(zhí)行相同的操作
Arm Adv SIMD 歷史
Armv6 | Armv7-A | Armv8-A AArch64 |
---|---|---|
SIMD extension | NEON | NEON |
在32位通用ARM寄存器上操作 | 獨(dú)立的寄存器庫(kù),32x64位NEON寄存器 | 獨(dú)立的寄存器庫(kù),32x128位NEON寄存器 |
8位或16位整數(shù) | 8/16/32/64位整數(shù) | 8/16/32/64位整數(shù) |
每條指令進(jìn)行2x16位/4x8位操作 | 單精度浮點(diǎn)數(shù) | 單精度浮點(diǎn)數(shù)、雙精度浮點(diǎn)數(shù) |
– | 每條指令最多16x8位操作(16x4吧,待確認(rèn)) | 每條指令最多16x8位操作 |
2. 寄存器
Armv7-A 和 AArch32 具有相同的通用 Arm 寄存器 - 16 16 16 x 32 32 32 位通用 Arm 寄存器(R0-R15)。
Armv7-A 和 AArch32 具有 32 x 64 位 NEON 寄存器(D0-D31)。這些寄存器也可以看作是 16 × 128 位寄存器(Quad-word
, Q0-Q15)。每個(gè) Q0-Q15 寄存器映射到一對(duì)D寄存器,如下圖所示。
相比之下,AArch64 具有 31 31 31 個(gè) 64 64 64 位通用 Arm 寄存器和 1 1 1 個(gè)具有不同名稱的特殊寄存器,這取決于使用它的上下文。這些寄存器可以被看作是 31 31 31 個(gè) 64 64 64 位寄存器(X0-X30)或 31 31 31 個(gè) 32 32 32 位寄存器(W0-W30)。
AArch64 具有 32 32 32 x 128 128 128 位 NEON 寄存器(V0-V31,Vector Registers
)。這些寄存器也可以看作是 32 32 32 位 Sn寄存器(Single-word Registers)
或 64 64 64 位 Dn寄存器(Double-word Registers)
。
(也就是說(shuō),V 寄存器有 128 位,D 寄存器 64 位,S寄存器 32 位,可以將 V 寄存器拆開(kāi)使用)
3. NEON 命名方式
-
變量命名方式:
baseWxLxN_t
- base:是基礎(chǔ)數(shù)據(jù)類型
- W:是基礎(chǔ)類型的寬度
- L:是向量的長(zhǎng)度
- N:是向量數(shù)組的個(gè)數(shù)
- 如
uint8x16_t
、uint8x16x3_t
-
函數(shù)命名方式:
ret v[p][q][r]name[u][n][q](args)
- ret:返回值類型
- v:表示vector
- q:飽和運(yùn)算,溢位后,為自動(dòng)限制在數(shù)據(jù)類型的最大范圍內(nèi)
- r:圓整操作
- name:SIMD指令名稱
- u:unsigned
- n:narrow
- q:做后綴表示128位滿位寬寄存器運(yùn)算 quarter*32
4. 優(yōu)化技巧
-
熱點(diǎn)函數(shù)涉及到大量 IO 讀寫操作時(shí),數(shù)據(jù)的內(nèi)存地址盡量與 NEON 數(shù)組或系統(tǒng)位數(shù)對(duì)齊,如32位對(duì)齊,可降低訪問(wèn)開(kāi)銷;
-
重點(diǎn)優(yōu)先搞 NEON 指令并行計(jì)算,能大幅降低開(kāi)銷;
-
大部分的 NEON 問(wèn)題會(huì)出在存取、移動(dòng)指令的濫用、混亂使用上(neon的寄存器和普通arm的寄存器是分開(kāi),也就是說(shuō)arm的普通指令和neon指令之間不可以有過(guò)多的數(shù)據(jù)交換,但是sse沒(méi)有這個(gè)限制?待驗(yàn)證);
-
for
循環(huán):- 如
for (b = 0; b < num; b++)
: - 可改成
for (b = 0; b < num - 3; b += 4)
; - 或者
for (b = num - 1; b >= 3; b -= 4)
; - 需注意結(jié)尾不能整除的幾個(gè)還是用非SIMD方式計(jì)算:
- 原始:for (i = 0; i < size; i++);
- 并行:for (i = 0; i < size - 3; i += 4);
- 掃尾:for (; i < size; i ++)。
- 如
-
數(shù)組索引取值:
- 數(shù)組索引以及索引內(nèi)部涉及運(yùn)算的,盡量換成指針偏移加減來(lái)做;
- 避免大范圍索引跳躍,減少 cache miss。
-
內(nèi)存使用:
- 優(yōu)先用局部變量,而非 malloc 堆內(nèi)存,減少 cache miss;
- 針對(duì)具體變量類型,手動(dòng) for 循環(huán)并行拷貝值,可能比 memcpy() 函數(shù)更高效,因?yàn)?memcpy 內(nèi)部還涉及大量判斷,以保證平臺(tái)兼容性;
- 用NEON指令時(shí),4 路運(yùn)算的數(shù)組(128位=16字節(jié)),內(nèi)存地址最好要 16 字節(jié)對(duì)齊。
-
指令運(yùn)算
- 矩陣乘場(chǎng)景,在不大幅增加寄存器變量的前提下,外部的A也最好并行多讀幾路數(shù)據(jù)進(jìn)來(lái),跟B的各列運(yùn)算,減少B各列的讀取次數(shù);
- 乘加指令,add 和 mul 可以合并為 mla,一條指令完成乘加操作。
-
C 語(yǔ)言編碼級(jí)考慮
- C 語(yǔ)言中一條事件的處理函數(shù)盡可能在一個(gè)源文件中(便于編譯器自動(dòng)向量化);
switch
比if else
快,而且代碼整潔。
-
深入理解計(jì)算機(jī)系統(tǒng)
- 組織代碼結(jié)構(gòu),善用 CPU 緩存,數(shù)據(jù)段/代碼段連續(xù)可以提高 CPU 緩存命中率;
- 極簡(jiǎn)函數(shù)時(shí),盡量 inline 展開(kāi),減少函數(shù)調(diào)用棧的開(kāi)銷;
- 消除不必要的存儲(chǔ)器引用,如 for 循環(huán)中 *dest = *dest - nums[i],可用中間變量替換 *dest,for 循環(huán)后再賦值給 *dest,可減少 for 內(nèi)的一次讀寫操作;
- 簡(jiǎn)單的循環(huán)展開(kāi),編譯器可以自己完成,優(yōu)化選項(xiàng) O2 及以上,或者命令 -funroll-loops(O2 及以上自帶),可調(diào)用 gcc 進(jìn)行循環(huán)展開(kāi);
- 能用整型不用浮點(diǎn),整數(shù)乘法/加法和浮點(diǎn)加法,只用一個(gè)周期,浮點(diǎn)乘法需要2個(gè)周期。
-
NEON 與 SSE 在寄存器處理數(shù)據(jù)時(shí)有一些區(qū)別。在 NEON 中,通常需要先將要處理的數(shù)據(jù)加載到 NEON 寄存器,然后執(zhí)行 SIMD 操作。這與 SSE 有一些不同,因?yàn)?SSE 寄存器(XMM 寄存器)可以直接與內(nèi)存交互(這一步可能格外耗時(shí),需要特別注意)。在 NEON 中,加載數(shù)據(jù)到 NEON 寄存器通常包括以下步驟:
- 從內(nèi)存加載數(shù)據(jù)到通用寄存器(通常是ARM通用寄存器)。
- 將數(shù)據(jù)從通用寄存器傳送到 NEON 寄存器。
然后,您可以在 NEON 寄存器上執(zhí)行 SIMD 操作,例如矢量加法、矢量乘法等。
這與 SSE 不同,因?yàn)?SSE 寄存器可以直接從內(nèi)存加載數(shù)據(jù),而不需要中間步驟。這可以在 SSE 指令中實(shí)現(xiàn),而不需要將數(shù)據(jù)先加載到通用寄存器中。
總之,NEON 需要額外的步驟來(lái)加載數(shù)據(jù)到寄存器,然后才能進(jìn)行 SIMD 操作,而 SSE 可以更直接地在寄存器中操作數(shù)據(jù)。這是因?yàn)椴煌軜?gòu)和指令集設(shè)計(jì)的差異。
5. 優(yōu)化 NEON 代碼(Armv7-A內(nèi)容,但區(qū)別不大)
5.1 優(yōu)化 NEON 匯編代碼
考慮處理器如何集成 NEON 技術(shù)的實(shí)現(xiàn)定義方面,因?yàn)獒槍?duì)特定處理器優(yōu)化的指令序列可能在不同的處理器上具有不同的時(shí)序特征,即使 NEON 指令周期時(shí)序相同。
為了從手寫的 NEON 代碼中獲得最佳性能,有必要了解一些底層硬件特性。特別是,程序員應(yīng)該意識(shí)到流水線和調(diào)度問(wèn)題、內(nèi)存訪問(wèn)行為和調(diào)度危害。
5.1.1 Cortex-A 處理器之間的 NEON 管道差異
Cortex-A8 和 Cortex-A9 處理器共享相同的基本 NEON 管道,盡管在如何將其集成到處理器管道中存在一些差異。Cortex-A5 處理器包含一個(gè)完全兼容的簡(jiǎn)化 NEON 執(zhí)行管道,但它是為盡可能最小和最低功耗的實(shí)現(xiàn)而設(shè)計(jì)的。
5.1.2 內(nèi)存訪問(wèn)優(yōu)化
TLB(Translation Lookaside Buffer)
是計(jì)算機(jī)系統(tǒng)中的一種硬件緩存,用于加速虛擬地址到物理地址的轉(zhuǎn)換過(guò)程。TLB 的每個(gè)條目稱為 TLB entry(TLB條目)
,它存儲(chǔ)了一組虛擬地址和相應(yīng)的物理地址之間的映射關(guān)系。TLB 通常位于 CPU 內(nèi)部,用于提高內(nèi)存訪問(wèn)的速度。
當(dāng)程序執(zhí)行時(shí),CPU 需要將虛擬地址(由程序使用)轉(zhuǎn)換為物理地址(在內(nèi)存中實(shí)際存儲(chǔ)數(shù)據(jù)的地址)。這個(gè)地址轉(zhuǎn)換通常由操作系統(tǒng)的內(nèi)存管理單元(MMU)
來(lái)執(zhí)行。MMU 將虛擬地址映射到物理地址,并在需要時(shí)將這些映射關(guān)系存儲(chǔ)在 TLB 中,以便以后的訪問(wèn)可以更快地完成,而無(wú)需再次執(zhí)行昂貴的地址轉(zhuǎn)換操作。
TLB entry 通常包括以下信息:
- 虛擬地址(Virtual Address):程序使用的地址。
- 物理地址(Physical Address):在內(nèi)存中實(shí)際存儲(chǔ)數(shù)據(jù)的地址。
- 標(biāo)志位(Flags):包括權(quán)限信息(例如,讀、寫、執(zhí)行權(quán)限)和其他控制信息。
當(dāng) CPU 需要訪問(wèn)內(nèi)存中的數(shù)據(jù)時(shí),它首先查看 TLB 來(lái)查找虛擬地址和物理地址之間的映射關(guān)系。如果找到了匹配的 TLB entry,那么物理地址將用于訪問(wèn)內(nèi)存,這將顯著提高內(nèi)存訪問(wèn)速度。如果沒(méi)有找到匹配的 TLB entry,CPU 將向 MMU 請(qǐng)求執(zhí)行地址轉(zhuǎn)換,并將結(jié)果存儲(chǔ)在 TLB 中以供將來(lái)使用。
總之,TLB entry 是 TLB 中存儲(chǔ)的虛擬地址到物理地址映射的單元,用于加速計(jì)算機(jī)內(nèi)存訪問(wèn)的過(guò)程。這有助于提高系統(tǒng)的性能和效率。
L1 和 L2 通常是指計(jì)算機(jī)的緩存層次結(jié)構(gòu)中的兩個(gè)不同級(jí)別的緩存:
L1 Cache(一級(jí)緩存)
:
L1 緩存是最接近 CPU 核心的緩存級(jí)別,通常位于 CPU 內(nèi)部或非??拷?CPU 核心。它是一個(gè)小而快速的緩存,用于存儲(chǔ)最常用的數(shù)據(jù)和指令,以提高 CPU 的性能。
L1 緩存分為兩個(gè)部分,分別是指令緩存(Instruction Cache)
和數(shù)據(jù)緩存(Data Cache)
。指令緩存存儲(chǔ) CPU 指令,而數(shù)據(jù)緩存存儲(chǔ)處理數(shù)據(jù)。
由于其靠近 CPU 核心的位置,L1 緩存通常具有非常低的訪問(wèn)延遲。L2 Cache(二級(jí)緩存)
:
L2 緩存位于 L1 緩存之上,通常在 CPU 內(nèi)部或者與 CPU 核心相對(duì)較近,但比 L1 緩存大。
它也用于存儲(chǔ)數(shù)據(jù)和指令,但比 L1 緩存更大,能夠容納更多的數(shù)據(jù)。
L2 緩存的訪問(wèn)延遲通常比 L1 緩存稍高,但仍然比主內(nèi)存的訪問(wèn)延遲要低。
有些計(jì)算機(jī)架構(gòu)具有多個(gè) L2 緩存層,通常是 L2 和 L3,L3 通常比 L2 更大,但訪問(wèn)延遲更高。
這兩個(gè)緩存級(jí)別的存在是為了提高計(jì)算機(jī)的性能。L1 緩存專注于存儲(chǔ)最常用的數(shù)據(jù)和指令,因此它們可以更快地被 CPU 核心訪問(wèn)。如果數(shù)據(jù)不在 L1 緩存中,CPU 將在 L2 緩存中查找,如果還不在,那么將從主內(nèi)存中獲取數(shù)據(jù)。通過(guò)在多個(gè)緩存層之間進(jìn)行數(shù)據(jù)訪問(wèn),計(jì)算機(jī)可以更有效地管理內(nèi)存訪問(wèn),從而提高整體性能。
NEON 單元很可能會(huì)處理大量數(shù)據(jù),例如數(shù)字圖像。一個(gè)重要的優(yōu)化是確保算法以最適合緩存的方式訪問(wèn)數(shù)據(jù)。這樣可以從 L1 和 L2 緩存中獲得最大的命中率(hit rate)
??紤]活動(dòng)內(nèi)存位置的數(shù)量也很重要。在 Linux 下,每個(gè) 4KB 的頁(yè)面都需要一個(gè)單獨(dú)的 TLB 條目。Cortex-A9 處理器有多個(gè) 32 32 32 個(gè)元素的 micro-TLB 和一個(gè) 128 128 128 個(gè)元素的主 TLB,之后它將開(kāi)始使用 L1 緩存來(lái)加載頁(yè)表?xiàng)l目(page table entry)。一種典型的優(yōu)化是安排算法以適當(dāng)?shù)拇笮√幚韴D像數(shù)據(jù),以最大限度地提高緩存和 TLB 命中率。
支持 交錯(cuò)(interleaving)
和 反交錯(cuò)(de-interleaving)
的指令可以為性能改進(jìn)提供很大的空間。VLD1
從內(nèi)存加載寄存器,沒(méi)有去交錯(cuò)。然而,其他 VLDn
操作使我們能夠加載、存儲(chǔ)和反交錯(cuò)包含兩個(gè)、三個(gè)或四個(gè)相同大小的 8 8 8、 16 16 16 或 32 32 32 位元素的結(jié)構(gòu)。VLD2
加載兩個(gè)或四個(gè)寄存器,去交錯(cuò)的偶數(shù)和奇數(shù)元素。例如,這可以用于分割左通道和右通道立體聲音頻數(shù)據(jù),如下圖所示。類似地,VLD3
可用于將 RGB 像素分割為單獨(dú)的通道,相應(yīng)地,VLD4
可用于 ARGB 或 CMYK 圖像。
上圖顯示了用 VLD2.16
( 16 16 16 字節(jié)) 從 R1 指向的內(nèi)存中加載兩個(gè) NEON 寄存器。這在第一個(gè)寄存器中產(chǎn)生 4 4 4 個(gè) 16 16 16 位元素,在第二個(gè)寄存器中產(chǎn)生 4 4 4 個(gè) 16 16 16 位元素,相鄰的成對(duì)左值和右值被分隔到每個(gè)寄存器中。