内联汇编 - 从头开始
FROM: http://www.ibm.com/developerworks/cn/aix/library/au-inline_assembly/
對于 C/C++ 程序員來說,內聯匯編并不是一個新特性,它可以幫助我們充分利用計算能力。然而,大多數程序員很少有機會實際使用該特性。事實上,內聯匯編只為特定的要求提供服務,在涉及先進的高層編程語言時尤其如此。
本文介紹了 IBM Power 處理器架構的兩個場景。使用本文提供的示例,我們可以發現在什么地方應用了內聯匯編。
場景 1:更好的庫
C/C++ 編程語言支持邏輯運算。在本例中,用戶使用位 作為基本單位。用戶編寫了一個算法來計算一個 32 位變量所占用的位數。
代碼 A: 計算被占用的位數
01 inline int bit_taken(int data) 02 { 03 int taken = 0; 04 while (data) { 05 data = (data >> 1); 06 taken++; 07 } 08 return taken; 09 }此代碼顯示了如何配合使用循環和移位運算。如果用戶采用最高優化級別(-O3 適用于 gcc,-O5 適用于 xlc)編譯代碼,那么用戶可能會發現某些優化(如展開,常量數據傳播,等等)都是自動完成的,它們可以生成世界上速度最快的代碼。但算法的基本思路并沒有發生改變。
清單 A:cntlzw 的描述
cntlzw(CountLeading Zeros Word) 指令
目的
將來自源通用寄存器的前導零的數量放進一個通用寄存器。
cntlzw 指令能夠獲得前導零的數量。我們以數字 15 為例,其二進制表示為 0000, 0000, 0000, 0000, 0000, 0000, 0000, 1111,cntlzw 會告訴大家,總共有 28 個前導零。經過重新考慮后,用戶決定簡化其算法,如代碼 B 所示。
代碼 B:計算內聯匯編所占用的位數
01 #ifdef __ARCH_PPC__ 02 inline int bit_taken(int data) 03 { 04 int taken; 05 asm("cntlzw %0, %1\n\t" 06 : "=b" (taken) 07 : "b" (data) 08 ); 09 return sizeof(data) * 8 – taken; 10 } 11 #else ... ... 21 #endif名稱為 __ARCH_PPC__ 的宏只包裝適用于 PowerPC 架構的新代碼。與代碼 A 相比,新的代碼已經刪除了所有循環或移位。然后,使用者可能會高興地看到 bit_taken 的性能有所提高。它在 PowerPC 上運行得更快。而且,應用程序綁定的 bit_taken 甚至表現得更好。
這個故事并不僅說明用戶可以通過豐富的指令改進他的算法,而且還說明了內聯匯編是提高性能的最佳助手。通過將匯編代碼嵌入到 C/C++,可以最大限度地減少用戶修改代碼的工作。
場景 2:原子比較和交換 (CAS)
近日,隨著整個計算機行業將重點轉移到多處理、多線程,不可避免地帶來了更多的元素(如編程中的同步)。要在多線程環境中構成同步原語(如信號量和互斥),我們經常會提到被稱為比較和交換 (CAS) 的原子操作。清單 B 顯示了 CAS 的偽代碼。
清單 B:CAS 的偽代碼
清單 1.
compare_and_swap (*p, oldval, newval):if (*p == oldval)*p = newval;success;elsefail;在清單 B 中,首先比較內存位置 p (*p) 的內容與已知值 oldval(這應該是當前線程中 *p 的值)。只有當它們是相同值時,才會將 newval 寫入 *p。若其他線程之前已經修改了內存位置,那么比較操作會失敗。
為了準確起見,CAS 應該是原子的。原子性超越了 C/C++ 的處理能力,但可以通過使用一小段內聯匯編代碼而得到保證。代碼 C 顯示了面向 PowerPC 架構實現的一個簡單的 CAS。
代碼 C:在 PowerPC 上的簡單 CAS 實現
01 void inline compare_and_swap (volatile int * p, int oldval, int newval) 02 { 03 int fail; 04 __asm__ __volatile__ ( 05 "0: lwarx %0, 0, %1\n\t" 06 " xor. %0, %3, %0\n\t" 07 " bne 1f\n\t" 08 " stwcx. %2, 0, %1\n\t" 09 " bne- 0b\n\t" 10 " isync\n\t" 11 "1: " 12 : "=&r"(fail) 13 : "r"(p), "r"(newval), "r"(oldval) 14 : "cr0"); 15 }此代碼片段實現了清單 B 中的偽代碼,但目前它對于我們來說似乎過于復雜。我們將在介紹完基本的語法后再回頭探討它。
但是,歸納我們所述的內容,在兩個條件下通常需要內聯匯編:
- 代碼優化
若性能要求是至關重要的,那么內聯匯編可能會有所幫助。正如我們從場景 1 中可以看到的,調優編譯器選項不會永遠都是最佳選擇。一個便利的內聯匯編代碼片段可以讓用戶大幅提高程序的性能。
- 硬件操作/OS 服務
C/C++ 在場景 2 中的能力是有限的。編譯器總是需要一些時間來標準化和實現最新的特性。因此,為了使用最新的硬件指令、OS 服務等,我們經常求助于內聯匯編。大部分時間,它都是最佳選擇。
使用內聯匯編可能還有其他原因。但是總的來說,內聯匯編在功能和性能方面都可以作為 C/C++ 的一種補充。
內聯匯編的使用
內聯匯編的語法看上去與 C/C++ 完全不同。一個合理的解釋是,內聯匯編并不是從 C/C++ 程序員的角度進行設計的,而是從一個編譯者/匯編者的角度進行設計的。內聯匯編的一般語句構成如清單 C 所示。
清單 C:內聯匯編代碼塊的組成
__asm__ __volatile__(assembly template: output operand list : input operand list: clobber list);如清單 C 所示,內聯匯編在邏輯上總是由四部分組成:
1. 關鍵字 asm() 或 __asm__()。修飾符 volatile 或 __volatile__:關鍵字 asm 或 __asm__ 用于說明隨后的字符串是內聯匯編代碼塊。volatile 或 __volatile__ 是可選的,可以將它們添加到 asm 后面,禁止某些編譯器的優化。其實,asm 和 __asm__ 幾乎是相同的,惟一的區別是,當預處理程序宏中使用內聯匯編時,asm 在編譯過程中可能會引發警告。volatile 和 __volatile__ 也是如此。
2. 匯編模板:
匯編模板是括號內的第一個部分。它包含匯編指令行,這些指令行都包括在雙引號 ("") 中,以行分隔符(\n\t 或 \n)結束。內聯匯編代碼的語法是相同的,但比一般的匯編代碼簡單得多。這其中有許多原因。例如,它不需要在匯編模板中定義數據,因為它應該始終從 C/C++ 變量引用。而且,很少有必要在匯編模板中(為可執行文件)創建一個分段。一般情況下,除了匯編指令,只允許使用一些本地標簽。(我們稍后會討論它們)。
代碼 D:內聯匯編的匯編模板
__asm__ __volatile__ ("lwarx %0, 0, %1 \n\t" : "=&r"(ret) : "r"(p));代碼 D 顯示了匯編模板的一個示例。
3. 輸入/輸出操作數列表
輸入/輸出列表以冒號 (:) 開始。它們的條目用逗號 (,) 隔開。該列表在匯編模板中指定變量及其約束。以代碼 D 為例,lwarx 設置有效地址,即寄存器值 %1 加上一個立即值 0。它從有效地址讀取一個單詞并存儲到寄存器 %0。在這里,%0 是一個輸出操作數,它存儲結果并被寫入列表。而 %1 是一個輸入。這樣,%0 所引用的 ret 就會放入輸出列表,而 %1 所引用的 p 則會放入輸入列表。
輸入/輸出操作數列表中列出的每個變量:
- 必須有一個約束。例如,=&r (ret) 的約束是 r,這意味著可能將 ret 分配給任何通用寄存器。
- 可以有一個可選的約束修飾符。例如,=&r (ret) 的修飾符是 = 和 &。= 表示該變量是只寫的。& 表示這個變量不能與任何輸入操作數共享相同的寄存器。(早期的亂碼表示在指令使用完輸入操作數之前,已經修改了操作數。因此,它不能與輸入操作數共享寄存器。有關的詳細信息,請參閱 A guide to inline assembly for C and C++
各平臺之間的約束是不同的。通常,產品文檔會提供更多的實際詳細信息。
4. 破壞列表(Clobber list)
亂碼列表通知編譯器,有些寄存器或內存已因內聯匯編塊造成亂碼。亂碼列表看起來類似于輸入/輸出列表(用冒號開始,并以逗號分隔)。但只用寄存器名稱(如 r1、f15)或 內存 充當其條目。
在代碼 C 的示例中,內聯匯編代碼隱式地破壞了條件寄存器字段。因此,cr0 寄存器字段被放入破壞列表。如果用戶認為代碼更換到了一個不確定的內存空間,那么內存也會出現在列表中。我們在后面的章節將再次討論破壞列表。
事實上,并不是所有在清單 C 中顯示的組件都是必需的。一個關鍵字和一個匯編模板就足以構成一個基本的內聯匯編。其他所有部分都是可選的。
現在,我們回到代碼 C,進一步解釋其指令。
lwarx %0, 0, %1
該指令在有效地址 0 + %1 上將內存讀取到寄存器 %0(實際上是 *p)。此外,該指令根據指令 stwcx 預約了以后的驗證。
xor. %0, %3, %0
bne 1f
該指令會比較我們剛加載到 %0 的值和 oldval (%3)。當它們不相等時,則會分支跳轉到標簽 1,這意味著 CAS 運算失敗。
stwcx. %2, 0, %1
bne- 0b
stwcx. 檢查 lwarx 的預約。如果檢測成功,它會將 %2 (newval) 的內容寫入 0 + %1(p) 的有效地址。如果寫入失敗,則會分支跳轉到標簽 0,以便進行重試。
isync
該指令禁止運行 iSync 之后的指令,直到 iSync 前的指令已完成。
表 A 列出了該示例的操作數列表中的所有條目,以及它們對應于代碼 B 的寄存器編號。
表 A:約束、修飾符和代碼 C 的寄存器引用
| "=&r"(fail) | =&r: writable, early clobber, general register | fail | %0 |
| "r"(p) | R: general register | p | %1 |
| "r"(newval) | R: general register | newval | %2 |
| "r"(oldval) | R: general register | oldval | %3 |
我們可以從代碼中看到,在寫回指令 stwcx. 后按照重試步驟進行操作。如果其他線程已經更新了地址 p 保持,那么重試會發現 *p 和 oldval 是不同的。因此,請控制分支跳轉到標簽 1 ,也就是說控制 CAS 失敗。我們可以通過比較變量 fail 和 0 來對此進行判斷。
lwarx 和 stwcx 在 PowerPC 架構中是非常特殊的指令。它們對于組成原子原語至關重要。如果您有興趣,可以從 Power ISA 找到有關的更多信息。[1] 對于分枝設施,文獻 [2] 提供了最好的解釋。
回頁首
常見錯誤
對于可能會犯錯誤的初學者來說,有一些可供他們始終查看的指南。
- 不要忘記行分隔符 (\n\t)
- 不要忘了行的雙引號 ("")
- 不要混淆 () 和 {}。
我們還遇到過一些有趣的錯誤:
代碼 E:在內聯匯編模板內的宏。
01 // This is the intention: 02 __asm__ __volatile__( 03 "stswi %0, %1, 4\n\t" 04 :: "b" (t), "b" (b) 05 ); … 01 // Macro, does not work: 02 #define F 4 03 __asm__ __volatile__( 04 "stswi %0, %1, F\n\t" 05 :: "b" (t), "b" (b) 06 );出于某種原因,用戶可能想將某個 C/C++ 宏應用于內聯匯編模板。具體而言,在上面的示例中,用戶試圖替換一個立即值。然而,編譯器拒絕執行該代碼。事實上,用戶不應該考慮在匯編模板中應用任何 C/C++ 預處理器操作。用戶將 C/C++ 傳入內聯匯編的惟一接口是使用輸入/輸出列表。 代碼 F 顯示了一種可以實現用戶目標的方法。
代碼 F:被引用為立即值的宏
01 #define F 4 02 __asm__ __volatile__( 03 "stswi %0, %1, %2\n\t" 04 : : "b" (t), "b" (b), "i"(F) 05 );在這里,我們使用操作數的一個立即約束來引用宏。然后,通過修改宏定義,用戶可以在全局修改常量。
輸出操作數列表中缺少冒號。
在代碼 G 中,stswi 指令意味著它存儲了 4 個字節,從寄存器 %1 開始(具體來說,如果 %1 被分配到寄存器 r0,則按順序從 r0、r1、r2、r3...讀出字節)到在 0% 的有效地址。
對于內聯匯編代碼,并沒有輸出操作數,因為沒有寄存器存儲結果,也無法寫入該結果。而且,輸入操作數列表中包括所有變量(value 和 base)。
代碼 G:缺少冒號
01 // Require input only: 02 int base[5]; 03 int value = 0x7a; 04 __asm__ __volatile__( 05 "stswi %0,%1,4\n\t" 06 : : "b" (value), "b" (base) 07 ); … 01 // But mistaken as output : 02 __asm__ __volatile__( 03 "stswi %0,%1,4\n\t" 04 : "b" (value), "b" (base) 05 );在后一種代碼中,用戶不幸地漏了一個冒號。現在所有輸出都變成輸入。在這種情況下,編譯器甚至可能沒有發出警告。但是,用戶可能最終會在運行時發現錯誤。雖然這樣的錯誤看起來很小,但它可能會破壞一切。此外,不容易發現該錯誤,因為錯誤總是很難找到的,比如在 C/C++ 代碼中混淆 if (a==1) 和 if (a=1)。因此,初學者應多注意冒號。
我們將很快將會繼續討論這種錯誤的更深層次原因。
回頁首
內聯匯編、編譯器和匯編器
人們在編寫內聯匯編代碼時會發現,最大的挑戰并不是在規范中找出正確的指令,而是讓輸入/輸出/破壞列表正常工作。有可能出現投訴和問題,比如:為什么我們需要這些列表?為什么要有約束和修飾符?等等。
在這里,我們列出了這類問題及其答案。我們希望它可以幫助用戶從實現角度了解有關的更多信息。為簡單起見,我們只專注于引用 C/C++ 變量的指令和寄存器操作數。
Q1:誰處理內聯匯編?編譯器還是匯編器?為什么我在編譯時得到匯編程序錯誤?
A1: 答案是兩者都處理(在大部分的時間)。一般情況下,匯編器會在編譯器支持最新指令之前支持這些指令。因此,編譯器必須調用匯編器來處理任何無法識別的指令。但是,這并不意味著匯編器會處理一切。變量和寄存器之間的關聯是通過編譯器完成的。(請參閱 Q2 及 Q3。) C/C++ 中的內聯匯編語法檢查也由編譯器完成。但是,匯編指令本身不包括在內。因此,如果匯編器在檢查匯編指令時發現問題,那么它會報告錯誤。
Q2:匯編模板中的寄存器如何引用 C++ 變量?
A2:如 Q1 的答案所示,關聯由編譯器完成。在內部,由一個寄存器分配和指派過程將變量映射為寄存器。在完成此過程后,匯編模板會變成一小段真正的匯編代碼。然后,匯編器可以接受并處理它,從而生成最終的二進制代碼。
Q3:我知道寄存器分配和指派會將寄存器與變量相關聯。但是,為什么要提供一個輸入/輸出列表?
A3:事實上,對于寄存器分配和指派,可能會要求編譯器提供輸入,比如約束和活躍度(liveness)。如果沒有內聯匯編,編譯器可以通過內部分析代碼找到這樣的輸入。但是,因為編譯器認為內聯匯編塊里面的指令行為是未知的,所以它要求用戶提供額外的信息來幫助它。
內部的約束可能與硬件有關。例如,為放入通用寄存器的操作數設置約束 r,并為放入浮點寄存器的操作數設置約束 f。此外,有時,某些硬件在某些情況下將禁止某些行為。例如,在 PowerPC 中的約束 b(它禁止使用 r0 寄存器)就屬于這一類。(請參閱 A guide to inline assembly for C and C++ - Basic, intermediate, and advanced concepts,了解有關的更多詳細信息)。從概念上講,用戶應該負責告訴編譯器 數據類型、指令的限制 等信息,因為編譯器對用戶所提供的匯編代碼完全是未知的。
活躍度可能受到許多方面的影響。最重要的一個方面是,是讀取、寫入變量,還是兩者同時進行。輸入/輸出操作數列表和某些約束修飾符可以幫助構建該信息。(例如,約束修飾符 "+" 說明操作數是 read-write,而 "=" 說明它是 write-only。)
整體而言,輸入/輸出列表用于向編譯器提供信息。
整體而言,輸入/輸出列表用于向編譯器提供信息。
Q4:破壞列表又怎么樣呢?我們為什么需要它?
A4:在許多現實世界的平臺上,機器指令可能會隱式地修改寄存器。可以將這視為一種硬件約束。包含寄存器名稱的破壞列表可以讓編譯器知道沒有引用變量的任何其他寄存器是否也被修改了。 而且,如果一個指令不可預知地寫入一個意外的內存位置,編譯器可能不知道它是否修改了任何已經存在于寄存器中的變量。(如果發生這種情況,會從內存重新加載已經存在于寄存器中的變量。)通過放進一個 內存 亂碼,我們通知編譯器要做一些處理,以確保生成的代碼是正確的。(對于內存亂碼, A guide to inline assembly for C and C++ - Basic, intermediate, and advanced concepts 提供了更好的解釋。)
Q5:為什么不建議使用匯編指令?
A5:有時,人們認為內聯匯編可能具備匯編的完整功能。但是,情況并不總是這樣。例如,如果用戶不知道內聯匯編代碼已經嵌入最終可執行文件的代碼段,那么使用匯編指令可能會引起嚴重的問題。
一個典型的示例是,用戶希望在匯編模板中定義一個新的部分 .mysect。編譯器會計算出正確的匯編代碼,并將它傳遞給匯編器。但是,正如匯編語法所說明的那樣,需要定義一個 .mysect 部分,使用當前部分覆蓋它。因此,內聯匯編后面的代碼(這是由編譯器生成的)也會匯編到 .mysect 部分中,而不是 .text(用于代碼)部分。結果,該可執行文件被完全破壞。
總之,使用不屬于編譯器的內聯匯編規范的匯編功能不是一種明智的做法。使用未獲得正式支持的任何內容都可能為您的代碼帶來風險。
現在,讓我們回到丟失冒號的問題。顯然,失敗的根本原因是我們向編譯器提供了不正確的信息(如活動性或約束)。編譯器不會抱怨,因為它不檢查任何列表的正確性(只檢查 C/C++ 語法錯誤)。并且匯編器也會很開心,因為它只處理有合理格式的指令。但事實上,編譯器使用了不正確的信息工作。最后,該代碼會失敗。這個失敗警告了我們,并且用戶要為自己編寫的輸入/輸出/亂碼列表負責,這非常重要。否則,獲得不可用的代碼就并不奇怪了。
回頁首
結束語
雖然學習內聯匯編的語法并不難,但編寫正確的匯編代碼并不僅僅意味著編寫正確的匯編指令和嵌入它們。由于編譯器無法分析內聯匯編塊的內部情況,所以內聯匯編用戶應該向編譯器提供比普通 C/C++ 代碼更多的信息。這可能很容易出錯。無論如何,您可以利用下面的技巧。
- 只編寫一個具有單一功能的較短的內聯匯編塊。
- 查看編譯器文檔中的內聯匯編部分。不要試圖使用不屬于編譯器的內聯匯編規范的匯編功能。
- 仔細選擇指令。弄清楚每一個細節。不要漏掉任何說明,比如約束、副作用等。
- 再次檢查輸入/輸出/亂碼列表,然后再編譯和運行您的代碼。特別要檢查是否正確使用了冒號。
回頁首
致謝
謝謝 IBM CDL Rational Compiler 團隊的同事 Jiang Jian 和 Ji Jinsong。感謝他們對這篇文章的認真審查和意見。
參考資料
學習
- A guide to inline assembly for C and C++ Basic, intermediate, and advanced concepts。這是一篇很好的面向 Power/PowerPC 用戶的內聯匯編教程,它涵蓋了許多高級主題。
- GCC-Inline-Assembly-HOWTO,gcc 的基本內聯匯編的教程。
- 在 IBM AIX 7.1 上安裝 IBM XL C/C++:IBM XL C/C++ 是一個高性能的優化編譯器,旨在服務于 IBM POWER 處理器,并利用處理器的多核和向量特性實現更好的并行應用程序開發。本文將向您講解在 IBM AIX 7.1 操作系統上安裝 XL C/C++ V11 的整個過程。
- IBM XL C/C++ 編譯器中添加的 ISO C11 支持:新的 ISO C 編程語言標準提供了多項功能來幫助提高編程效率、調試和提高性能。IBM XL 編譯器正在分階段支持新的 C 標準,以便您可以利用有用的功能,比如支持復數類型對象初始化的功能、靜態斷言,以及針對沒有返回值的函數的函數特性。
- AIX and UNIX 專區:developerWorks 的“AIX and UNIX 專區”提供了大量與 AIX 系統管理的所有方面相關的信息,您可以利用它們來擴展自己的 UNIX 技能。
- AIX and UNIX 新手入門:訪問“AIX and UNIX 新手入門”頁面可了解更多關于 AIX 和 UNIX 的內容。
- AIX and UNIX 專題匯總:AIX and UNIX 專區已經為您推出了很多的技術專題,為您總結了很多熱門的知識點。我們在后面還會繼續推出很多相關的熱門專題給您,為了方便您的訪問,我們在這里為您把本專區的所有專題進行匯總,讓您更方便的找到您需要的內容。
- AIX and UNIX 下載中心:在這里你可以下載到可以運行在 AIX 或者是 UNIX 系統上的 IBM 服務器軟件以及工具,讓您可以提前免費試用他們的強大功能。
- IBM Systems Magazine for AIX 中文版:本雜志的內容更加關注于趨勢和企業級架構應用方面的內容,同時對于新興的技術、產品、應用方式等也有很深入的探討。IBM Systems Magazine 的內容都是由十分資深的業內人士撰寫的,包括 IBM 的合作伙伴、IBM 的主機工程師以及高級管理人員。所以,從這些內容中,您可以了解到更高層次的應用理念,讓您在選擇和應用 IBM 系統時有一個更好的認識。
討論
- 加入 developerWorks 中文社區。查看開發人員推動的博客、論壇、組和維基,并與其他 developerWorks 用戶交流。
總結
以上是生活随笔為你收集整理的内联汇编 - 从头开始的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深度卷积神经网络CNNs的多GPU并行框
- 下一篇: Caffe + Ubuntu 14.04