---
# System prepended metadata

title: Arm Programmer's Guide VI - Floating-Point 學習筆記
tags: [Arm, programmer's guide]

---

# Arm Programmer's Guide VI - Floating-Point 學習筆記
# 1. 前言

此筆記為學習 [ARM® Cortex™-A Series Programmer's Guide
Version: 4.0 中第六章 Floating-Point ](https://developer.arm.com/documentation/den0013/d/Floating-Point?lang=en) 的心得筆記，主旨在解釋 Armv7 Aarch32 是如何處理浮點數的 (也就是小數點後值非零的非整數)。另外原文的第四篇 [Introduction to Assembly Language](https://developer.arm.com/documentation/den0013/d/Introduction-to-Assembly-Language?lang=en) 和第五篇 [ARM/Thumb Unified Assembly Language Instrctions](https://developer.arm.com/documentation/den0013/d/ARM-Thumb-Unified-Assembly-Language-Instructions?lang=en) 主要內容都在解釋各組合語言指令的功能，較少觀念上的知識，故不會有相關的學習筆記。

# 2. IEEE-754 標準

IEEE 是電機電子工程師學會 (Institute of Electrical and Electronics Engineers) 的縮寫，該學會負責定義資訊科技領域的一些通用規範，而 IEEE-754 則是用來定義浮點數的運算標準。在電腦科學發展的早期，各家電腦使用不同的方式來記錄浮點數，這使跨電腦之間的資料交換有著非常大的不便，統一的標準因應而生。而 **Arm 的硬體浮點數系統就是遵循 IEEE-754 實作的**。(參考來源: [維基百科](https://zh.wikipedia.org/zh-tw/IEEE_754))

# 3. 浮點數儲存格式
## 3.1 Normal number (正常數)

浮點數通常能用 32 bit (Single Precision)、64 bit (Double Precision) 或 16 bit (Half Precision) 來表示，他總共將所有 bit 分成三個部分，以 Single Precision 的 float 來說:

![image](https://hackmd.io/_uploads/Hyb7P-NnJl.png)

簡單來說，IEEE-754 是以二為底的科學記號方式來記錄浮點數，也就是說

$\symbfit{浮點數 = m \times 2^e ,\ where \ 1 \leq m < 2\ and\ -127 \leq e \leq 128}$

而 m 和 n 基本上 (但不完全) 分別對應上圖的 Exponent (指數) 和 Mantissa (尾數)，以下為各欄位更詳細的說明:
* **【S】**: 
BIT[31] 為 Sign bit，當 0 時，此數為負數，相反則為正數。
* **【Exponent 指數】**: 
位置為 BIT[30:23] 總共 8 個 bit。其值不會直接等於上面公式的 e，而是 $e = Exponent - 127$，舉例來說，當 Exponent 值為 128 的時候，e 為 1。換句話說，e 的範圍為 128 ~ -127。
* **【Mantissa 尾數】**: 
位置為 BIT[22:0] 總共 23 個 bit。其值也不會直接等於上面公式的 m，由於 $1 < m \leq 2$，所以 m 一定是一個 1.XX 的數字，而 Mantissa 表示的就是小數點後的部分。舉例來說，當 Mantissa 值為 b111 0000 0000 0000 0000 0000 時，m 為 $1.111_2$，換成十進制的話就是 $1+1\times2^{-1}+1\times2^{-2}+1\times2^{-3}=1.875$。

所以如果有一個 float 值為 0xC2300000，那麼我們可以這樣將它轉換成二為底的科學符號小數:
1. BIT[31] 為 1，表示 Sign bit 為 1，此數為負數。
2. BIT[30:23] 為 0x84，表示 Exponent 為 132，$e = 132 - 127 = 5$。
3. BIT[22:0] 為 0x300000，表示 Mantissa 為 b011 0000 0000 0000 0000 0000，m 為 $1.011_2$，換成十進制為 $1+0\times2^{-1}+1\times2^{-2}+1\times2^{-3}=1.375$
4. 綜合上述三點，可以得到結論，Float 0xC0300000 換成二為底的科學符號小數為 $-1.375\times2^5$。

## 3.2 Denormal number (反常值)

由上述可以得知，當 $m=1$ 且 $e = -127$ 時，可以得到最小的正常數 $2^{-127}$。但為了能進一步表達更小的數字，$e = -127$ (也就是 Exponent 為 0) 的情況被從 Normal number (正常數) 中獨立出來稱為 Denormal number (反常值)。因此正常數最小的值變成 $2^{-126}$，比他更小的數字將交由反常值來表達。反常值的格式如下:

$\symbfit{Denormal\ number = m \times 2^{-126} ,\ where \ 0 \leq m < 1}$

其實和 Normal number 的定義差不多，只是因為 Exponent 的值是固定的 (固定為 0)，而且反常值的目的就是用來呈現比正常值最小值還小的數字，所以正常值最小值為 $2^{-126}$，反常值的 e 就固定是 -126。並利用大於 0 但是比 1 還小的 m 來紀錄更多更小的數字。
反常值的 m 一樣表示的是小數點後的數字，但是和正常數不一樣的是，反常值小數點前的數字為 0，而不是 1。舉例來說，當 Exponent 為 0，Mantissa 為 b011 0000 0000 0000 0000 0000 時，m 值為 $0.011_2$，換成十進制就是 $0+0\times2^{-1}+1\times2^{-2}+1\times2^{-3}=0.375$

事實上，反常值在實務上並不常用，常常會因為效能的原因，直接忽略極小的反常值，並將它視為 0。Arm 的  VFP 可以選擇是否要精確地使用反常值 (Full denormal support)，或是直接將之視為 0 (Flush-to-zero mode)。

## 3.3 其他保留值

除了 Exponent 為 0、Mantissa 不為 0 時被保留做為反常值外，還有許多情況下該 float 的值不遵循正常數的公式，而是被保留做為指定用途，以下為完整表格:

| Exponent | Mantissa | 代表的值  |
| -------- | -------- | -------- |
| 0        | 0        | $\pm$ 0  |
| 0        | != 0     | Denormal number 反常值 |
| 255 (最大值) | 0     | $\pm\infty$ |
| 255 (最大值) | != 0  | NaN (Not-A-Number) |
| 其他     | 任何值    | Normal number 正常數 |


## 3.4 Loss of precision

由於 m 只使用 23 個 bit 來表達，任何需要用到超過 23 bit 來表達的 int，在轉換成 Float 值後都會因為無法精確表達而發生失準 (Loss of precision) 的情況。也就是說，如果你把這樣的 int 轉成 float 後再轉回 int，你會得到和原本的 int 不一樣的值。舉例來說，將 int 0x7FFFFF01 轉成 float 再轉回 int 的過程如下:

1. 將 int 0x7FFFFF01 換成二為底的科學記號表達:
![image](https://hackmd.io/_uploads/HyYbusShJg.png)
2. m (也就是小數點後的部分) 需要 31 個 bit 來表達，超過了 Mantissa 的欄位長度 23，所以若將其無條進位或捨去可以得知，其實際值會夾在以下兩個 float 值之間:
![image](https://hackmd.io/_uploads/HJ4ctiS3yl.png)
很明顯左邊的數字是更接近實際數字的，所以得到，int 0x7FFFFF01 換成 float32 為 0x4EFFFFFF。
    :::info
    這邊選擇較接近實際數字的方式其實是可以選擇的，也就是所謂的捨入演算法 (Round Algorithms)，IEEE 754 提供了四種捨入方法:
    1. 朝最接近的值捨入，也就是這邊例子使用的方法。(當實際值距離兩個可能值距離一樣時，也可以選擇朝向高位元較少 0 者，或者朝向遠離 0 者。)
    2. 朝 0 捨入，也可以視為無條件捨去。
    3. 朝 +$\infty$ 捨入。
    4. 朝 -$\infty$ 捨入。
    :::
3. 再將 float32 0x4EFFFFFF 換為 int。根據上圖左邊得知 S 為 0、e 為 30、m 為 1. 後面接著 23 個 1，所以其值為 $1.11111111111111111111111\times2^{30}=$ 0x7FFFFF00，相比於最一開始的值 0x7FFFFF01 會發現最低位的 8 bit 被無條件捨去了。

# 4. Arm VFP (Vector Float Point)

VFP 是 ARM 的一個很實用的選配硬體單元，可以有 16 或 32 個 Double-Word 寄存器，分別稱為 VFPv3-D32 和 VFPv3-D16，他們也可以選擇擴充 16 bit 精度的浮點數 (half-precision floating-point)。
而 VFPv4 則內建 16 bit 精度浮點數，而且再進行混合加法和乘法 (FMA, Fused Multiply-Add) 的計算時，只有在最後結果是進行捨入以提升精度，而非像傳統運算先計算乘法並且將結果捨入後，再計算加法並且再做一次捨入。

VFP 也提供一些用於控制或記錄資訊的寄存器，他們能夠透過 VMRS/VMSR 來存取，舉例來說:
```clike=
VMRS r4, FPSID    // 將 FPSID 讀出並存入 r4 
VMSR FPSCR, r0    // 將 r0 寫入 FPSCR
```
這邊介紹 VFP 提供的寄存器用途:
1. **【Floating-Point System ID Register (FPSID)】**
提供像是 v1、v2 還是 v3 等較高層級的 VFP 資訊。細節請參考: [FPSID, Floating-Point System ID register](https://developer.arm.com/documentation/ddi0601/2024-12/AArch32-Registers/FPSID--Floating-Point-System-ID-register?lang=en)。
2. **【Floating-Point Status and Control Register (FPSCR)】**
提供 Floating-Point 系統的資訊與控制，像是如同 CPSR (Current Program Status Register) 中的 NZCV BIT、是否使用 Denormal number 等資訊與設定都在此寄存器中。細節請參考 [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)。
    :::info
    此為 User mode 唯一可以存取的寄存器。
    :::
    相較於整數的指令可以直接覆寫 APSR/CPSR 中的 NZCV 旗標，浮點數的運算只會影響 FPSCR 中的 NZCV 旗標，所以如果要將其結果傳遞給至 APSR/CPSR 給整數環境使用，就需使用 VMRS，請見以下範例:
    ```clike=
    VCMP d0, d1
    VMRS APSR_nzcv, FPSCR
    BNE lable
    ```
3. **【Floating-Point Exception Register (FPEXC)】**
可以用來控制整個 FPV 系統的開關，以及提供是否發生異常等資訊。細節請參考: [FPEXC: Floating-Point Exception Control register](https://developer.arm.com/documentation/ddi0601/2024-12/AArch32-Registers/FPEXC--Floating-Point-Exception-Control-register?lang=en)。
5. **【Media and VFP Feature Registers 0 and 1 (MVFR0 and MVFR1)】**
用來提供像是 VFP 是否支援平方根功能、是否支援 Double-Precision 操作等資訊。細節請參考: [MVFR0, Media and VFP Feature Register 0](https://developer.arm.com/documentation/ddi0601/2024-12/AArch32-Registers/MVFR0--Media-and-VFP-Feature-Register-0?lang=en) 和 [MVFR1, Media and VFP Feature Register 1](https://developer.arm.com/documentation/ddi0601/2024-12/AArch32-Registers/MVFR1--Media-and-VFP-Feature-Register-1?lang=en)。

# 5. 啟用 VFP

啟用 VFP 大致有以下三個步驟
1. 將 FPEXC 中的 EN bit 設成 1。
2. 如果要在 Normal World 中使用 VFP，那需要在 CP15.NSACR ([Non-Secure Access Control Register](https://developer.arm.com/documentation/ddi0601/2024-12/External-Registers/GICR-NSACR--Non-secure-Access-Control-Register?lang=en)) 中去啟用 Normal World 對 CP10 和 CP11 的存取。
CP10 控制 VFP 的存取權限、CP11 則控制 NEON (Advanced SIMD) 的存取權限。
    :::info
    NEON 也會使用到浮點數運算，所以 VFP 和 NEON 統稱為 FPU (Float Point Unit)。
    :::
3. 啟用 CP15.CPACR ([Architectural Feature Access Control Register](https://developer.arm.com/documentation/ddi0601/2024-12/AArch32-Registers/CPACR--Architectural-Feature-Access-Control-Register?lang=en)) 中的 CP10 (BIT[21:20]) 和 CP11 (BIT[23:22])。

# 6. 其他

## 6.1 Context Switch with VFP

在做 Context Switch 的時候 VFP 的值也需要一併的被保存和恢復，但為了提升效能，這僅限於 VFP 有被啟用的情況。

## 6.2 Floating-point optimization

1. 應盡量避免在 Cortex-A9 核心上混用 VFP 和 NEON 指令，會占用大量的效能。
2. 對 VFP 寄存器的 MOV 存取、VFP寄存器和一般寄存器之間的資料轉移通常不會被優化，所以應儘量在要求高效能的程式片段中使用。

---
    
上一篇: [Arm Programmer's Guide III - Processor Modes and Registers 學習筆記](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Processor_Modes_and_Registers_Study_note)
下一篇: [Arm Programmer's Guide VII - Introducing NEON 學習筆記](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Introducing_NEON_Study_note)