這是一篇圍繞 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]