iOS 開發者應該知道的 ARM 結構


iOS 開發者應該知道的 ARM 結構

這是一篇圍繞 iOS 來介紹 ARM 結構的文章,用詞簡單,邏輯清楚,偶見幽默。非開發者也值得一讀,權當增長知識。

我在寫「NEON on iPhone 入門」的時候,曾以為讀者已經比較了解 iOS 設備的處理器知識。然而,看過網上的一些討論,我才發現,原來這些知識並不普及,我的錯。此外,我覺得了解這些東西對 iPhone 編程有益(不僅僅針對喜歡 NEON 的人),即便你用的是 Objective-C,雖然,不了解也無礙工作,但這些知識會讓你成為一個更好的 iPhone 程序員。

基礎

到目前為止,所有的 iOS 設備都使用 ARM 結構處理器,它和台式機上的 x86 和 PowerPC 有些不同,然而絕對不是「特殊」或「小眾」的產品。幾乎所有的手機(不只是智能手機)都基於 ARM,例如幾乎所有的 iPod,幾乎所有的 MP3 播放器,PDA 和 Pocket PC 更不用說了。任天堂從 GBA 開始轉入 ARM,它甚至還侵入圖形計算器的地盤,出現在一些德儀和惠普的計算器中。如果你還想繼續溯本逐源,那麼牛頓用的也是 ARM(蘋果是 ARM 的早期投資者)。而且上面只說了一些小玩意,還有無數的 ARM 處理器運行在嵌入式系統中。

ARM 處理器因為低功耗和小尺寸而聞名,它的性能在同等功耗的產品中也很出色。這種結構(至少在 iOS 平台)使用小端(Little-endian)排序,就像 x86。它和 MIPS、PowerPC 一樣,屬於 32 位 RISC 結構。請注意,模擬器並不運行 ARM 代碼,軟件會被編譯成 x86 可以運行的指令。因此接下來的內容適用於目標設備,而非模擬器。

ARMv7,ARM11,Cortex A8 和 A4,天哪!

多年來,ARM 結構演化出幾個不同的版本,每一版都增加了新指令,在提升的同時保持了後向兼容的能力。初代 iPhone 使用了 ARMv6 結構的處理器(ARM 第六版的簡稱),而最新的 iPhone 4 支持 ARMv7。所以,編譯代碼的時候,依目標版本的指令集不同,生成不同的指令。匯編程序也一樣,代碼中使用的指令必須兼容特定的版本。最後,生成機器碼,對應 ARMv6 或 ARMv7(或者 ARMv5 和 v4,不過 ARMv6 是 iOS 開發的底線,所以這兩者就不用考慮了)。目標文件和可執行文件有標注自己對應的版本,可以通過運行 otool -vh foo.o 來查看。

不過呢,「初代 iPhone 4 搭載了 ARMv6 處理器」這種說法是錯誤的,因為 ARMv6 不是指特定的處理器,而是處理器可以運行的指令集。初代 iPhone 使用了 ARM 11 核心(確切說是 ARM1176JZF-S,不過這不重要,只要記得它是 ARM 11 家族的成員就行了),正如剛才提到的,這款處理器采用 ARMv6 指令集。之後的 iOS 設備仍采用 ARM11,直到 iPhone 3GS 發布,蘋果開始盡數轉向 Cortex A8 處理器核心(盡管尚不確定,但 iPhone 4 很可能用的就是 A8 )。這個核心采用了 ARMv7 指令集,或這麼說,它支持 ARMv7。

我已經說過,不要在程序裡植入設備判斷代碼,然後通過已知信息偵測設備所支持的 ARM 結構。這種代碼極不可靠,而且運行在(軟件完成後才發布的)新設備上會導致中斷。所以請別這麼做,否則我發誓,我會跑到你家裡廢了你。以上知識是為了讓你粗略了解,有些設備支持 ARMv7,有些設備支持 ARMv6。至於如何偵測,我馬上會談到。

不過,你可能會想「iPad 和 iPhone 4 用的是 A4,不是 Cortex A8 吧?」不然,A4 其實是一個完整的單片系統(SOC),其中不只有 Cortex A8 內核,還包括了圖形硬件、音視頻編碼加速器和其他數字模塊。單片系統和處理器是兩個很不相同的概念,處理器在硅片上甚至不佔主要空間。

如果不懂得如何利用,即使設備支持 ARMv7 也無濟於事。當然應用新的指令集也沒有問題,但如果總是這麼做,早先的設備就無法運行你寫的代碼了,我猜,這也許不是你想要的結果。那麼,應該如何偵測設備所支持的結構呢?— 只有確定它是否支持 ARMv7 才能好好利用啊。答案是:沒必要知道。相反,把代碼編譯兩次,一次針對 ARMv6,另一次針對 ARMv7,接著把這兩個可執行文件打包成一坨肥碩無比的二進制文件。好了,運行的時候,設備會自己決定打開哪一個更好。是的,Mach-O 不僅可以用來組合完全不同的 CPU 結構(例如 PowerPC 和 Intel),或者相同結構的 32 位和 64 位版本,它還可以對付同一種結構的 2 個變體,用 Mach-O 的術語來說,這叫 CPU 子類。從程序員的角度看,這麼做的結果是:編譯時決定一切。針對 ARMv6 編譯的代碼只運行在 ARMv6 設備上,同理,針對 ARMv7 編譯的代碼只運行在 ARMv7(或者更好)的設備上。

如果你讀過了我寫的 NEON 那帖,你也許會記得我推薦過一種在運行時(Runtime)中偵測和選擇結構的方法。如果再去看,你會發現我已經把那部分移走了,現在,我不建議那麼做,因為雖然這的確有用,但不能確保(或者說,所需技巧太復雜而不能確保不出錯)在將來的 ARMv8 處理器上能夠穩定運行。文檔中是否有相關 API 的狀態不重要(不在 iOS 的手冊頁中),如果你想在 ARMv6 上運行又希望利用 ARM7v,就用我剛才講過的辦法。

補充一點:在 iOS 環境下,ARM 結構不一定能反映處理器的型號。例如,對應 ARMv6 的 iOS 代碼需要浮點指令的支持(VFPv2,准確的說),對 ARMv6 而言,雖然這是可選項,不過自從第一代 iPhone 發布以來就已經存在。所以,如果在 iOS 開發(例如編譯器 -arch 設置或一個可執行文件的 CPU 子類)中提到了 ARMv6,就表示需要硬件浮點的支持。這對 ARMv7 和 NEON 也一樣:雖然 NEON 實際上是 ARMv7-A 配置的一個可選項,但是因為它出現在所有支持 ARMv7 的 iOS 設備中,所以,提到 iOS NEON 即部分提到 ARMv7。

條件執行

ARM 結構一個實用的功能是,大多數指令可以有條件地執行 — 如果條件不滿足,則指令無效。這可以縮短過程,讓區塊(Blocks)部署地更為有效。通常的辦法是,如果區塊不符合條件則跳過,但是通過把判斷指令植入塊內,省去了該步驟。

如果這僅僅是編譯器用來提高代碼效率的手段,我就不會在這裡提到它了。雖然,這的確是它的一個功用,但之所以提到是因為,在調試(Debugging)時,它可能會令人吃驚。事實上,有時你會發現,調試器會進入狀態為假的條件區塊(if block,例如早期的錯誤回報),或者進入 if-else 的兩個分支。這是因為,雖然代碼盡數經過處理器,但是一部分沒有實際執行,即條件執行。另外,如果你把斷點置入這樣的條件區塊中,即使狀態為假,它仍有可能執行。

話雖如此,但是在我有限的測試中,編譯器似乎拒絕在調試配置中生成條件執行指令。因此它應該只發生在調試優化後的代碼的時候,不幸的是,有時候你沒得選擇,只能這麼做。

Thumb

Thumb 指令集是 ARM 指令集的一個子集,經過壓縮,因此指令只有 16bits(所有 ARM 指令的大小都是 32bits,它仍然是 32 位結構,只是佔用的空間少了)這不是一個全然不同的結構,而應將其視作常見 ARM 指令和功能的縮寫。它的優點,顯然是大為縮小代碼尺寸,節約內存和緩存,以及代碼帶寬。雖然更適用於內存緊張的微控制器型應用程序,但是在 iOS 設備中,它仍然有用處,也因為如此,Xcode 默認在 iOS 項目中打開這項功能。雖然代碼尺寸因此減少很多,但是不可能達到 50%,因為有時候完成一個 ARM 指令需要對應的兩個 Thumb 指令。ARM 和 Thumb 指令不能隨意混合,處理器需要針對二者切換不同的模式,而這只能在調用或從函數返回時發生。

當目標平台是 ARMv6 的時候,編譯 Thumb 指令面臨著很大的權衡取舍。ARMv6 的 Thumb 代碼可以訪問的寄存器較少,缺乏條件指令,特別是,它不能使用浮點硬件,例如浮點加法、減法、乘法等等。使用浮點 Thumb 代碼必須調用系統函數,沒錯,聽起來就像速度很慢的感覺。基於這個原因,針對 ARMv6 時,我建議禁用 Thumb 模式,但倘若你執意如此,請確保先分析代碼。如果某些部分速度很慢,至少先試著禁用那部分 Thumb(很容易,在 Xcode 中使用命令行參數, -mno-thumb)。請記住,浮點運算在 iOS 中非常普遍,因為 Quartz 和 Core Animation 使用浮點坐標系統。

當目標變成了 ARMv7 的時候,所有這些缺點就消失了:ARMv7 包含 Thumb-2,它是 Thumb 指令的擴展集,增加了條件執行和可以訪問所有 ARM 寄存器以及硬件浮點與 NEON 的 32 位 Thumb 指令。用 Thumb-2 縮減代碼的代價幾乎沒有,所以最好是開著(如果關掉了請重新打開)。在 Xcode 的條件生成選項中,對 ARMv7 打開,對 ARMv6 關閉。

你也許在網上聽到人們說,代碼需要「互通」(Interworking)才能使用 Thumb,除非你想寫匯編代碼,否則不必擔心,因為 iOS 平台的所有代碼都是互通的。當顯示匯編的時候,Shark 可能難以判斷函數是 ARM 還是 Thumb。如果你看到無效或無意義的指令,最好互相對調一下。

對齊

iOS 支持非對齊訪問,然而比起對齊訪問,它的速度更慢,建議不要使用。在某些特殊情況下(涉及加載/存儲多個指令,如果你有興趣的話),非對齊訪問的速度可能比對齊訪問慢上百倍,因為處理器無法處理,而且必須請求操作系統的協助(參考此文,這和 PowerPC 上導致非對齊雙精度浮點數變得超慢是同一個現象)。所以,要小心,而且,對齊仍然重要。

除法

這家伙總讓每一個人吃驚。打開 ARM 結構手冊(如果你還沒有,請看「NEON on iPhone 入門」的結構概覽那節),找到整數除法指令。去吧,我等你。找不到?正常正常,根本沒有的。是的,ARM 結構不支持硬件整數除法,必須通過軟件執行。如果你編譯下面的代碼:


int ThousandDividedBy(int divisor)
{
return 1000/divisor;
}

在匯編代碼中,你會看到編譯器插入了一個調用函數的「___divsi3」— 這是一個系統函數,用來執行軟件除法(注意,除數不能恆定,否則除法可能會被轉換為乘法)。這意味著,在 ARM 上,整數除法實際代表了操作系統的性能。

「不過,」看完手冊歸來,你也許會說:「你錯啦!裡面有 ARM 除法指令,甚至還有兩個呢!在這裡,sdiv 和 udiv!」不好意思給您頗涼水啦,這些指令只可用於 ARMv7-R 和 ARMv7-M 配置(分別指實時和嵌入式環境 — 例如馬達的微控制器和手表),iOS 設備用的 ARMv7-A 不支持,很抱歉!

GCC

GCC 生成的 ARM 代碼質量之糟已不是秘密。在其他一些基於 ARM 的平台上,專業開發者使用 ARM 自家提供的工具鏈 — RVDS。不過,RVDS 不支持 OSX 用的 Mach-O 運行時,只支持 ELF 運行時,所以在 iOS 平台上沒轍。但至少還有 GCC 的替代品,比如現在可以用 LLVM。雖然我沒怎麼測試,但是當使用 LLVM 的時候,至少看到了 64 位整數碼的顯著改進(這一點,GCC 在 ARM 上尤其弱)。假以時日,LLVM 全面超越 GCC 可以指望。

你瞧,現在你是更好的 iOS 開發者了!

[原文鏈接;作者: Pierre Lebeaupin]

引用 apple4.us
較新的 較舊