---
# System prepended metadata

title: Arm Programmer's Guide VII - Introducing NEON 學習筆記
tags: [Arm, chihanlin, programmer's guide, SIMD, NEON]

---

# 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)提到):
![image](https://hackmd.io/_uploads/ByrZCJr6kg.png)

:::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} 組成，如下圖所示:
![image](https://hackmd.io/_uploads/S10zLZS61g.png)
他們可以被拆成多個 8 BIT、16 BIT 或 32 BIT 的資料 (或稱元素 Element):
![image](https://hackmd.io/_uploads/ByqF8WB61g.png)
其中 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) 寄存器。
![image](https://hackmd.io/_uploads/Sy3XDzS6yx.png)
舉例: ```VMULL.S16 Q2, D8, D9```
* **W (Wide)**
輸出和第一個輸入寄存器的尺寸是第二個輸數寄存器的兩倍，但是維持同樣的資料型態。也就是說，輸入需為一個 quadword (128 BIT) 寄存器、和一個 doubleword (64 BIT) 寄存器，而輸出會是一個 quadword (128 BIT) 寄存器。
![image](https://hackmd.io/_uploads/HJSawfBpyx.png)
* **N (Narrow)**
輸出的尺寸會是輸入的一半，但是維持同樣的資料型態。也就是說，輸入需為兩個 quadword (128 BIT) 寄存器，而輸出會是一個 doubleword (64 BIT) 寄存器。
![image](https://hackmd.io/_uploads/rJHwKMBTJe.png)

## 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)