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