# Arm Programmer's Guide VII - Introducing NEON 學習筆記
# 1. 前言
此筆記為學習 [ARM® Cortex™-A Series Programmer's Guide
Version: 4.0 中第七章 Introducing NEON ](https://developer.arm.com/documentation/den0013/d/Introducing-NEON?lang=en) 的心得筆記。主旨在解釋 Armv7 Aarch32 下的 NEON 功能。
# 2. 概述
NEON 常常也被稱為 Advanced SIMD architecture extensions,是 Arm 處理器內的一種可選擇性實作的硬體擴展,**提供 32 個 64 BIT 的寄存器,並且這些寄存器是和 VFP (v3) 共用的**。它主要用來提供 [SIMD](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Introducing_NEON_Study_note#3-SIMD-Single-Intruction-Multiple-Data) 擴展指令集,在特定應用 (像是影音處理、物理模擬等等) 下能提供很高的效能優化。
# 3. SIMD (Single Intruction Multiple Data)
顧名思義,SIMD 能使用單一指令去同時對多筆資料做處理。根據指令和資料的數量關係,總共有四種分類:
* **SISD (Single Instruction Single Data)**
傳統的單一指令只處理單一一筆資料。舉例來說: $C = A + B$
* **MISD (Multiple Instruction Single Data)**
多筆指令同時對單一資料做運算。舉例來說,同時對單一資料 $X$ 做以下三種計算:
$Y1 = X + 5$
$Y2 = X \times 2$
$Y3 = X - 3$
MISD 常用於一些需要特別小心、不能出錯所以需要反覆錯誤檢測的應用,想是飛行器控制。
* **MIMD (Multiple Instruction Multiple Data)**
多指令同時處理多筆資料,其實就是多核架構下的正常行為。舉例來說,兩個核心同時對兩筆資料執行以下指令:
核心1: $C1 = A + B$
核心2: $C2 = X \times Y$
* **SIMD (Single Instruction Multiple Data)**
單一指令同時對多筆資料做運算。舉例來說,用一個單一指令完成以下三筆計算:
$C1 = A + B$
$C2 = C + D$
$C3 = E + F$
接下來將解釋 SIMD 實際上是怎麼做單指令多資料運算的。
以 32 BIT 的核心來說,處理大量的 16 BIT 或 8 BIT 資料用 SISD 的方法是相對沒有效率的。SIMD 採用的方法是在一個較長的寄存器中放入多筆較短的資料 (或稱為元素 Element),也就是說將 4 個 8 BIT 或 2 個 16 BIT 資料放進 32 BIT 寄存器中,並將其作為操作數 (Operand) 讓單一 SIMD 指令進行多資料運算。下圖圖解 SIMD 的加法如何將兩個 32 BIT 寄存器 R1 和 R2 視為各 4 筆 8 BIT 資料,並將他們平行地做相加進 R0 中 (也就是 ```UADD8 R0, R1, R2```,指令的細節會在[稍後](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Introducing_NEON_Study_note#5-NEON-Registers)提到):

:::info
1. 由上圖可以得知每一對的實際獨立資料操作被稱為 Lane。
2. SIMD 操作中每一個 Lane 都是獨立的,以上圖例子來說,任何 Lane 在加法後發生的 Overflow 並不會影響到其他的 Lane。
:::
雖然上面是以 32 BIT 寄存器來舉例,但事實上 NEON 本身的設計是針對 128 BIT 的寄存器操作數來做設計的,所以它最多能一次對 16 個 8 BIT 資料來做加法運算。
:::info
前面有提到 NEON 使用的是 32 個 64 bit 寄存器,但這邊又說它是針對 128 BIT 來做設計的可能會讓人覺得有點矛盾,但這其實是因為 NEON 有將 2 個 64 BIT 寄存器視為一個 128 BIT 寄存器的能力,這在[稍後](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Introducing_NEON_Study_note#5-NEON-Registers)也會再次提到。
:::
# 4. NEON and VFP
雖然 NEON 和 VFP (Vector Float Point, 細節可以參考前一篇 [Floating-Point 學習筆記](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Floating_Point_Study_note)) 都能對浮點數做處理,也共用同一組寄存器組,但他們都是獨立的硬體擴展,可以擇一實作,他們之間主要有以下的差異:
1. NEON 只能一次對多筆資料操作 (只能對向量 Vector 操作),而 VFP 是針對單一浮點數做運算。
2. NEON 不支援 64 bit 的雙精度浮點數 (Double precision floating-point) 的運算,但是 VFP 支援。
3. NEON 不支援一些較複雜的浮點數運算,像是平方根或是除法,但是 VFP 支援。
# 5. NEON Registers
NEON 提供的寄存器組可以被視為 32 個 64 BIT 寄存器 (D0 ~ D31) 或者 16 個 128 BIT 寄存器 (Q0 ~ Q15),其中 Qn 是由 D{2n} 和 D{2n+1} 組成,如下圖所示:

他們可以被拆成多個 8 BIT、16 BIT 或 32 BIT 的資料 (或稱元素 Element):

其中 NEON 也可以將單一資料視為標量 (Scalar),並只針對該向量特定標量做操作。
# 6. NEON Instructions
NEON 的指令一定是 V 開頭,其格式如下: (被大括弧 {} 包起來的是選填的項目,表示可以為空。)
**```V{<mod>}<op>{<shape>}{<cond>}{.<dt>}{<dest>}, src1, src2```**
* \<op> 為操作指令,像是 ADD, SUB... 等等。
* \<cond> 為配合 IT 指令的 Condition。
* \<dest> 為目的寄存器。
* \<src1> 和 \<src2> 分別為第一和第二個操作數源 (Source operand)。
## 6.1 \<mod>
* **Q** (Ex: VQADD)
帶有 Q 的指令會進行飽和運算 (Saturating arithmetic),也就是說,結果數值會被限制在該資料型態的最大值和最小值之中。[FPSCR (Floating-Point Status and Control Register)](https://developer.arm.com/documentation/ddi0601/2024-12/AArch32-Registers/FPSCR--Floating-Point-Status-and-Control-Register?lang=en) 的 QC BIT 會指出是否有飽和的情況發生。
* **H** (Ex: VHADD)
帶有 H 的指令會將結果除二 (也就是做右移 1 的動作),可以用來計算兩數平均時使用。
* **D**
帶有 D 的指令會先將結果乘上 2 然後再進行飽和處理。很常在兩個 Q15 格式的小數進行乘法時用到。
當對兩個 Q15 小數相乘會想得一樣是 Q15 格式的結果。但 Q15 相乘其實會先得到一個 Q30 的小數 (有 31 個 BIT),若要將它轉回 Q15 (16 BIT) 的格式,就需要先將他乘上 2 變成 32 BIT 的格式,才有辦法轉換型態回 16 BIT 的 Q15 格式 (也就是捨棄低 16 BIT)。
:::info
所謂的 Q15 格式小數是使用 16 bit 來表示一個小數,其中最高位元為 Sign Bit,用來表示正負,而剩下的 15 個位元則是表示 1.m 的 m。舉例來說,若有一個 Q15 小數為 0110 0000 0000 0000,則因最高位元為 0,所以其為正數,剩下的 15 位表示該數為 $1.11_2$,換算成 10 進位就是 $1+1\times2^{-1}+1\times2^{-2}=1.75$。
同樣道理,Q30 小數為最高位元為 Sign Bit,剩下 30 BIT 用來表示 1.m 的 m 的格式。
:::
* **R** (Ex: VRHADD)
帶有 R 的指令會將結果四捨五入至整數位,相當於將結果先加上 0.5 再做小數點以下的無條件捨去。
## 6.2 \<shap>
\<shap> 用來形容輸入寄存器和輸出寄存器之間的尺寸關係。
* **無 (Normal)**
輸入和輸出的尺寸一樣。如果輸入是兩個 64 BIT 寄存器,輸出就是一個 64 BIT 寄存器。
* **L (Long)**
輸出的尺寸會是輸入的兩倍,但是維持同樣的資料型態 (見下一段落 [6.3 <.dt>](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Introducing_NEON_Study_note#63-ltdtgt))。也就是說,輸入需為兩個 doubleword (64 BIT) 寄存器,而輸出會是一個 quadword (128 BIT) 寄存器。

舉例: ```VMULL.S16 Q2, D8, D9```
* **W (Wide)**
輸出和第一個輸入寄存器的尺寸是第二個輸數寄存器的兩倍,但是維持同樣的資料型態。也就是說,輸入需為一個 quadword (128 BIT) 寄存器、和一個 doubleword (64 BIT) 寄存器,而輸出會是一個 quadword (128 BIT) 寄存器。

* **N (Narrow)**
輸出的尺寸會是輸入的一半,但是維持同樣的資料型態。也就是說,輸入需為兩個 quadword (128 BIT) 寄存器,而輸出會是一個 doubleword (64 BIT) 寄存器。

## 6.3 \<.dt>
dt 是 Data Type 的縮寫,用來指定每一個 Element 的型態和尺寸。通常由一個單一子母開頭指定資料型態,然後後面接著數字表示資料尺寸 (可以為 8, 16, 32, 64 或 128,單位為 BIT)。
資料型態:
* **I (Integer)** 表示整數 (沒有明確指定是有符號還是無符號)。
* **U (Unsigned)** 表示無符號整數。
* **S (Signed)** 表示有符號整數。
* **f (Float)** 表示浮點數。
所以 \<.dt> 實際例子可以是 S16、U32、f8 等等。
---
上一篇: [Arm Programmer's Guide VI - Floating-Point 學習筆記](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Floating_Point_Study_note)
下一篇: [Arm Programmer's Guide VIII - Caches 學習筆記](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Caches_Study_note)