在 Big Endian 模式下使用 ARM NEON 指令¶
簡介¶
為 Big Endian ARM 處理器產生程式碼大部分都很簡單。然而,NEON 載入和儲存指令有一些有趣的特性,使得在 Big Endian 模式下產生程式碼的決策不太明顯。
本文旨在說明 NEON 載入和儲存指令的問題,以及 LLVM 中已實作的解決方案。
在本文中,「向量」一詞指的是 ARM ABI 所謂的「短向量」,它是可以放入 NEON 暫存器中的一系列項目。此序列的長度可以是 64 或 128 位元,並且可以構成 8、16、32 或 64 位元的項目。本文通篇都指的是 A64 指令,但幾乎也適用於 A32/ARMv7 指令集。在 A32 中傳遞向量的 ABI 格式與 A64 略有不同。除此之外,概念都是相同的。
範例:C 語言層級內建函數 -> 組合語言¶
首先說明 C 語言層級的 ARM NEON 內建函數如何降級為指令可能會有幫助。
這個簡單的 C 函數採用一個包含四個整數的向量,並將第零個通道設定為值「42」
#include <arm_neon.h>
int32x4_t f(int32x4_t p) {
return vsetq_lane_s32(42, p, 0);
}
arm_neon.h 內建函數會盡可能產生「泛型」IR(也就是一般的 IR 指令,而不是 llvm.arm.neon.*
內建函數呼叫)。上述程式碼會產生
define <4 x i32> @f(<4 x i32> %p) {
%vset_lane = insertelement <4 x i32> %p, i32 42, i32 0
ret <4 x i32> %vset_lane
}
然後變成以下簡單的組合語言
f: // @f
movz w8, #0x2a
ins v0.s[0], w8
ret
問題¶
主要問題在於向量在記憶體和暫存器中的表示方式。
首先,回顧一下。項目的「位元組序」只會影響其在記憶體中的表示方式。在暫存器中,數字只是一串位元,在 AArch64 通用暫存器的情況下是 64 位元。然而,記憶體是一連串可定址的 8 位元單位。因此,任何大於 8 位元的數字都必須分割成 8 位元的區塊,而位元組序描述了這些區塊在記憶體中的排列順序。
「Little Endian」排列方式將最低有效位元組放在最前面(記憶體位址最低)。「Big Endian」排列方式將*最高*有效位元組放在最前面。這表示從 Big Endian 記憶體載入項目時,記憶體中最低的 8 位元必須放在最高有效的 8 位元中,依此類推。
LDR
與 LD1
¶

圖1 使用 LDR
進行大端序向量載入。¶
向量是同時運算的連續項目序列。要載入 64 位元向量,需要從記憶體中讀取 64 位元。在小端序模式下,我們可以透過執行 64 位元載入來做到這一點 - LDR q0, [foo]
。然而,如果我們嘗試在大端序模式下執行此操作,由於位元組交換,通道索引最終會被交換!記憶體中佈局的第零個項目成為向量中的第 n 個通道。

圖2 使用 LD1
進行大端序向量載入。請注意,通道保留了正確的順序。¶
因此,指令 LD1
執行向量載入,但不會在整個 64 位元上執行位元組交換,而是在向量內的個別項目上執行位元組交換。這表示寄存器內容與在小端序系統上的內容相同。
似乎 LD1
應該足以在大端序機器上執行向量載入。然而,這兩種方法各有利弊,這使得選擇哪種寄存器格式變得不那麼簡單。
有兩個選項
向量寄存器的內容與使用
LDR
指令載入時的內容相同。向量寄存器的內容與使用
LD1
指令載入時的內容相同。
因為 LD1 == LDR + REV
,同樣地 LDR == LD1 + REV
(在大端序系統上),我們可以使用另一種類型的載入加上 REV
指令來模擬其中一種載入。因此,我們不是要決定使用哪些指令,而是要決定使用哪種格式(這將會影響哪種指令最適合使用)。
請注意,在本節中,我們只提到了載入。儲存與其關聯的載入具有完全相同的問題,因此為了簡潔起見,已將其省略。
注意事項¶
LLVM IR 通道排序¶
LLVM IR 具有第一類向量類型。在 LLVM IR 中,向量的第零個元素位於最低記憶體位址。最佳化器在某些方面依賴此屬性,例如在將向量連接在一起時。其目的是使陣列和向量具有相同的記憶體佈局 - [4 x i8]
和 <4 x i8>
在記憶體中的表示方式應相同。如果沒有此屬性,最佳化器將必須巧妙地處理許多特殊情況。
使用 LDR
指令會破壞這個通道順序屬性。這並非完全排除使用 LDR
的可能性,但我們必須採取以下兩種措施之一:
在每個
LDR
指令之後插入一個REV
指令來反轉通道順序。停用所有依賴通道佈局的優化,並且對於每個對單個通道的訪問(
insertelement
/extractelement
/shufflevector
),都反轉通道索引。
ARM 程序呼叫標準 (AAPCS)¶
ARM 程序呼叫標準 (AAPCS) 定義了在寄存器中傳遞向量函數的 ABI。它規定:
當一個短向量在寄存器和記憶體之間傳輸時,它被視為一個不透明的物件。也就是說,一個短向量存儲在記憶體中,就好像它是使用單個
STR
指令存儲整個寄存器一樣;一個短向量使用相應的LDR
指令從記憶體中載入。在小端序系統中,這意味著元素 0 將始終包含短向量中地址最低的元素;在大端序系統中,元素 0 將包含短向量中地址最高的元素。—ARM 64 位架構程序呼叫標準 (AArch64),4.1.2 短向量
使用 ABI 定義的 LDR
和 STR
指令至少比 LD1
和 ST1
指令有一個優勢。LDR
和 STR
指令不關心向量中各個通道的大小。LD1
和 ST1
指令則不然——通道大小編碼在指令中。這在跨 ABI 邊界時非常重要,因為需要知道被呼叫方期望的通道寬度。請看以下程式碼:
<callee.c>
void callee(uint32x2_t v) {
...
}
<caller.c>
extern void callee(uint32x2_t);
void caller() {
callee(...);
}
如果 callee
將其簽章更改為 uint16x4_t
(在寄存器內容中是等效的),如果我們使用 LD1
指令傳遞,則在更新和重新編譯 caller
之前,此程式碼會損壞。
有一種觀點認為,如果兩個函數的簽章不同,則行為應該是不確定的。但是,可能存在一些函數不關心向量的通道佈局,並且如果沒有一個跨 ABI 邊界的通用格式,則無法將向量視為不透明值(僅載入和存儲它)。
因此,為了保持 ABI 相容性,我们需要在函數呼叫中使用 LDR
通道佈局。
對齊¶
在嚴格對齊模式下,LDR qX
要求其地址為 128 位元組對齊,而 LD1
只要求其對齊方式與通道大小相同。如果我們統一使用 LDR
,我們仍然需要在某些地方使用 LD1
來避免對齊錯誤(LD1
的結果需要使用 REV
反轉)。
然而,大多數操作系統並未啟用對齊錯誤,因此這通常不是問題。
總結¶
下表總結了針對上述兩個解決方案中提到的每個屬性,需要發出的指令。
|
|
|
---|---|---|
通道排序 |
|
|
AAPCS |
|
|
嚴格模式的對齊 |
|
|
兩種方法都不完美,選擇其中一種歸結為兩害相權取其輕。有人認為,通道排序問題必須改變與目標無關的編譯器通道,並且會導致通道索引反轉的奇怪 IR。有人認為這比支援 LD1
所需進行的更改更糟糕,因此選擇了 LD1
作為標準向量載入指令(依此類推,ST1
用於向量儲存)。
實作¶
實作分為 3 個部分
斷言
LDR
和STR
指令,以便永遠不允許選擇它們來產生向量載入和儲存。單通道向量除外 [1] - 根據定義,這些向量不可能出現通道排序問題,因此可以使用LDR
/STR
。為建立
REV
指令的位元轉換建立程式碼產生模式。確保建立適當的位元轉換,以便向量值作為單元素向量(這與使用
LDR
載入它們相同)在呼叫邊界傳遞。
位元轉換¶

LD1
解決方案的主要問題是處理位元轉換(或位元轉型或重新解釋轉型)。這些是虛擬指令,只會改變編譯器對資料的解釋,而不會改變底層資料本身。一個要求是,如果載入資料然後再次儲存(稱為「往返」),則儲存後的記憶體內容應與載入前相同。如果載入向量並在儲存前將其位元轉換為不同的向量類型,則目前往返將會中斷。
以下列程式碼序列為例
%0 = load <4 x i32> %x
%1 = bitcast <4 x i32> %0 to <2 x i64>
store <2 x i64> %1, <2 x i64>* %y
這會產生類似右圖中的代碼序列。不匹配的 LD1
和 ST1
會導致存儲的數據與加載的數據不同。
當我們看到從類型 X
到類型 Y
的位元轉換時,我們需要做的是更改數據的寄存器內表示形式,使其*如同*它剛剛由類型 Y
的 LD1
加載一樣。

從概念上講,這很簡單——我們可以插入一個 REV
來撤銷類型 X
的 LD1
(將寄存器內表示形式轉換為與由 LDR
加載時相同),然後插入另一個 REV
來更改表示形式,使其如同由類型 Y
的 LD1
加載一樣。
對於前面的示例,這將是
LD1 v0.4s, [x]
REV64 v0.4s, v0.4s // There is no REV128 instruction, so it must be synthesizedcd
EXT v0.16b, v0.16b, v0.16b, #8 // with a REV64 then an EXT to swap the two 64-bit elements.
REV64 v0.2d, v0.2d
EXT v0.16b, v0.16b, v0.16b, #8
ST1 v0.2d, [y]
事實證明,這些 REV
對在幾乎所有情況下都可以壓縮成一個 REV
。對於上面的示例,REV128 4s
+ REV128 2d
實際上是 REV64 4s
,如右圖所示。