編譯器做些什么?
本文主要探討一下編譯器主要做些什么恩掷,以及如何有效的利用編譯器帚桩。
簡單的說特漩,編譯器有兩個(gè)職責(zé):把 Objective-C 代碼轉(zhuǎn)化成低級(jí)代碼吧雹,以及對(duì)代碼做分析,確保代碼中沒有任何明顯的錯(cuò)誤涂身。
現(xiàn)在雄卷,Xcode 的默認(rèn)編譯器是 clang。本文中我們提到的編譯器都表示 clang蛤售。clang 的功能是首先對(duì) Objective-C 代碼做分析檢查丁鹉,然后將其轉(zhuǎn)換為低級(jí)的類匯編代碼:LLVM Intermediate Representation(LLVM 中間表達(dá)碼)。接著 LLVM 會(huì)執(zhí)行相關(guān)指令將 LLVM IR 編譯成目標(biāo)平臺(tái)上的本地字節(jié)碼悴能,這個(gè)過程的完成方式可以是即時(shí)編譯 (Just-in-time)揣钦,或在編譯的時(shí)候完成。
LLVM 指令的一個(gè)好處就是可以在支持 LLVM 的任意平臺(tái)上生成和運(yùn)行 LLVM 指令漠酿。例如冯凹,你寫的一個(gè) iOS app, 它可以自動(dòng)的運(yùn)行在兩個(gè)完全不同的架構(gòu)(Inter 和 ARM)上,LLVM 會(huì)根據(jù)不同的平臺(tái)將 IR 碼轉(zhuǎn)換為對(duì)應(yīng)的本地字節(jié)碼炒嘲。
LLVM 的優(yōu)點(diǎn)主要得益于它的三層式架構(gòu) -- 第一層支持多種語言作為輸入(例如 C, ObjectiveC, C++ 和 Haskell)宇姚,第二層是一個(gè)共享式的優(yōu)化器(對(duì) LLVM IR 做優(yōu)化處理),第三層是許多不同的目標(biāo)平臺(tái)(例如 Intel, ARM 和 PowerPC)夫凸。在這三層式的架構(gòu)中浑劳,如果你想要添加一門語言到 LLVM 中,那么可以把重要精力集中到第一層上夭拌,如果想要增加另外一個(gè)目標(biāo)平臺(tái)魔熏,那么你沒必要過多的考慮輸入語言。在書The Architecture of Open Source Applications中 LLVM 的創(chuàng)建者 (Chris Lattner) 寫了一章很棒的內(nèi)容:關(guān)于LLVM 架構(gòu)鸽扁。
在編譯一個(gè)源文件時(shí)道逗,編譯器的處理過程分為幾個(gè)階段。要想查看編譯hello.m源文件需要幾個(gè)不同的階段献烦,我們可以讓通過 clang 命令觀察:
本文我們將重點(diǎn)關(guān)注第一階段和第二階段滓窍。在文章Mach-O Executables中,Daniel 會(huì)對(duì)第三階段和第四階段進(jìn)行闡述巩那。
預(yù)處理
每當(dāng)編源譯文件的時(shí)候吏夯,編譯器首先做的是一些預(yù)處理工作此蜈。比如預(yù)處理器會(huì)處理源文件中的宏定義,將代碼中的宏用其對(duì)應(yīng)定義的具體內(nèi)容進(jìn)行替換噪生。
例如裆赵,如果在源文件中出現(xiàn)下述代碼:
#import
預(yù)處理器對(duì)這行代碼的處理是用 Foundation.h 文件中的內(nèi)容去替換這行代碼,如果 Foundation.h 中也使用了類似的宏引入跺嗽,則會(huì)按照同樣的處理方式用各個(gè)宏對(duì)應(yīng)的真正代碼進(jìn)行逐級(jí)替代战授。
這也就是為什么人們主張頭文件最好盡量少的去引入其他的類或庫,因?yàn)橐氲臇|西越多桨嫁,編譯器需要做的處理就越多植兰。例如,在頭文件中用:
@classMyClass;
代替:
#import"MyClass.h"
這么寫是告訴編譯器 MyClass 是一個(gè)類璃吧,并且在 .m 實(shí)現(xiàn)文件中可以通過 importMyClass.h的方式來使用它楣导。
假設(shè)我們寫了一個(gè)簡單的 C 程序hello.c:
然后給上面的代碼執(zhí)行以下預(yù)處理命令,看看是什么效果:
clang -Ehello.c| less
接下來看看處理后的代碼畜挨,一共是 401 行筒繁。如果將如下一行代碼添加到上面代碼的頂部::
#import
再執(zhí)行一下上面的預(yù)處理命令,處理后的文件代碼行數(shù)暴增至 89,839 行巴元。這個(gè)數(shù)字比某些操作系統(tǒng)的總代碼行數(shù)還要多毡咏。
幸好,目前的情況已經(jīng)改善許多了:引入了模塊 - modules功能逮刨,這使預(yù)處理變得更加的高級(jí)呕缭。
自定義宏
我們來看看另外一種情形定義或者使用自定義宏,比如定義了如下宏:
#define MY_CONSTANT 4
那么禀忆,凡是在此行宏定義作用域內(nèi)臊旭,輸入了MY_CONSTANT,在預(yù)處理過程中MY_CONSTANT都會(huì)被替換成4箩退。我們定義的宏也是可以攜帶參數(shù)的离熏, 比如:
#defineMY_MACRO(x)x
鑒于本文的內(nèi)容所限,就不對(duì)強(qiáng)大的預(yù)處理做更多戴涝、更全面的展開討論了滋戳。但是還是要強(qiáng)調(diào)一點(diǎn),建議大家不要在需要預(yù)處理的代碼中加入內(nèi)聯(lián)代碼邏輯啥刻。
例如奸鸯,下面這段代碼,這樣用沒什么問題:
但是如果換成這么寫:
用clang max.c編譯一下可帽,結(jié)果是:
largest:201
i:202
用clang -E max.c進(jìn)行宏展開的預(yù)處理結(jié)果是如下所示:
本例是典型的宏使用不當(dāng)娄涩,而且通常這類問題非常隱蔽且難以 debug 。針對(duì)本例這類情況,最好使用static inline:
這樣改過之后蓄拣,就可以輸出正常的結(jié)果 (i:201)扬虚。因?yàn)檫@里定義的代碼是內(nèi)聯(lián)的 (inlined),所以它的效率和宏變量差不多球恤,但是可靠性比宏定義要好許多辜昵。再者,還可以設(shè)置斷點(diǎn)咽斧、類型檢查以及避免異常行為堪置。
基本上,宏的最佳使用場(chǎng)景是日志輸出张惹,可以使用__FILE__和__LINE__和 assert 宏舀锨。
詞法解析標(biāo)記
預(yù)處理完成以后,每一個(gè).m源文件里都有一堆的聲明和定義诵叁。這些代碼文本都會(huì)從 string 轉(zhuǎn)化成特殊的標(biāo)記流雁竞。
例如钦椭,下面是一段簡單的 Objective-C hello word 程序:
利用 clang 命令clang -Xclang -dump-tokens hello.m來將上面代碼的標(biāo)記流導(dǎo)出:
仔細(xì)觀察可以發(fā)現(xiàn)拧额,每一個(gè)標(biāo)記都包含了對(duì)應(yīng)的源碼內(nèi)容和其在源碼中的位置。注意這里的位置是宏展開之前的位置彪腔,這樣一來侥锦,如果編譯過程中遇到什么問題,clang 能夠在源碼中指出出錯(cuò)的具體位置德挣。
解析
接下來要說的東西比較有意思:之前生成的標(biāo)記流將會(huì)被解析成一棵抽象語法樹 (abstract syntax tree -- AST)恭垦。由于 Objective-C 是一門復(fù)雜的語言,因此解析的過程不簡單格嗅。解析過后番挺,源程序變成了一棵抽象語法樹:一棵代表源程序的樹。假設(shè)我們有一個(gè)程序hello.m:
當(dāng)我們執(zhí)行 clang 命令clang -Xclang -ast-dump -fsyntax-only hello.m之后屯掖,命令行中輸出的結(jié)果如下所示::
在抽象語法樹中的每個(gè)節(jié)點(diǎn)都標(biāo)注了其對(duì)應(yīng)源碼中的位置玄柏,同樣的,如果產(chǎn)生了什么問題贴铜,clang 可以定位到問題所在處的源碼位置粪摘。
延伸閱讀
靜態(tài)分析
一旦編譯器把源碼生成了抽象語法樹,編譯器可以對(duì)這棵樹做分析處理绍坝,以找出代碼中的錯(cuò)誤徘意,比如類型檢查:即檢查程序中是否有類型錯(cuò)誤。例如:如果代碼中給某個(gè)對(duì)象發(fā)送了一個(gè)消息轩褐,編譯器會(huì)檢查這個(gè)對(duì)象是否實(shí)現(xiàn)了這個(gè)消息(函數(shù)椎咧、方法)。此外把介,clang 對(duì)整個(gè)程序還做了其它更高級(jí)的一些分析勤讽,以確保程序沒有錯(cuò)誤竹宋。
類型檢查
每當(dāng)開發(fā)人員編寫代碼的時(shí)候,clang 都會(huì)幫忙檢查錯(cuò)誤地技。其中最常見的就是檢查程序是否發(fā)送正確的消息給正確的對(duì)象蜈七,是否在正確的值上調(diào)用了正確的函數(shù)。如果你給一個(gè)單純的NSObject*對(duì)象發(fā)送了一個(gè)hello消息莫矗,那么 clang 就會(huì)報(bào)錯(cuò)飒硅。同樣,如果你創(chuàng)建了NSObject的一個(gè)子類Test, 如下所示:
@interfaceTest:NSObject@end
然后試圖給這個(gè)子類中某個(gè)屬性設(shè)置一個(gè)與其自身類型不相符的對(duì)象作谚,編譯器會(huì)給出一個(gè)可能使用不正確的警告三娩。
一般會(huì)把類型分為兩類:動(dòng)態(tài)的和靜態(tài)的。動(dòng)態(tài)的在運(yùn)行時(shí)做檢查妹懒,靜態(tài)的在編譯時(shí)做檢查雀监。以往,編寫代碼時(shí)可以向任意對(duì)象發(fā)送任何消息眨唬,在運(yùn)行時(shí)会前,才會(huì)檢查對(duì)象是否能夠響應(yīng)這些消息。由于只是在運(yùn)行時(shí)做此類檢查匾竿,所以叫做動(dòng)態(tài)類型瓦宜。
至于靜態(tài)類型,是在編譯時(shí)做檢查岭妖。當(dāng)在代碼中使用 ARC 時(shí)临庇,編譯器在編譯期間,會(huì)做許多的類型檢查:因?yàn)榫幾g器需要知道哪個(gè)對(duì)象該如何使用昵慌。例如假夺,如果 myObject 沒有 hello 方法,那么就不能寫如下這行代碼了:
[myObject hello]
其他分析
clang 在靜態(tài)分析階段斋攀,除了類型檢查外已卷,還會(huì)做許多其它一些分析。如果你把 clang 的代碼倉庫 clone 到本地蜻韭,然后進(jìn)入目錄lib/StaticAnalyzer/Checkers悼尾,你會(huì)看到所有靜態(tài)檢查內(nèi)容。比如ObjCUnusedIVarsChecker.cpp是用來檢查是否有定義了肖方,但是從未使用過的變量闺魏。而ObjCSelfInitChecker.cpp則是檢查在 你的初始化方法中中調(diào)用self之前,是否已經(jīng)調(diào)用[self initWith...]或[super init]了俯画。編譯器還進(jìn)行了一些其它的檢查析桥,例如在lib/Sema/SemaExprObjC.cpp的 2,534 行,有這樣一句:
Diag(SelLoc,diag::warn_arc_perform_selector_leaks);
這個(gè)會(huì)生成嚴(yán)重錯(cuò)誤的警告 “performSelector may cause a leak because its selector is unknown” 。
代碼生成
clang 完成代碼的標(biāo)記泡仗,解析和分析后埋虹,接著就會(huì)生成 LLVM 代碼。下面繼續(xù)看看hello.c:
要把這段代碼編譯成 LLVM 字節(jié)碼(絕大多數(shù)情況下是二進(jìn)制碼格式)娩怎,我們可以執(zhí)行下面的命令:
clang -O3-emit-LLVMhello.c-c-o hello.bc
接著用另一個(gè)命令來查看剛剛生成的二進(jìn)制文件:
llvm-dis < hello.bc | less
輸出如下:
在上面的代碼中搔课,可以看到main函數(shù)只有兩行代碼:一行輸出string,另一行返回0截亦。
再換一個(gè)程序爬泥,拿five.m為例,對(duì)其做相同的編譯崩瓤,然后執(zhí)行LLVM-dis < five.bc | less:
拋開其他的不說袍啡,單看main函數(shù):
上面代碼中最重要的是第 4 行,它創(chuàng)建了一個(gè)NSNumber對(duì)象却桶。第 7 行境输,給這個(gè) number 對(duì)象發(fā)送了一個(gè)description消息。第 8 行颖系,將description消息返回的內(nèi)容打印出來嗅剖。
優(yōu)化
要想了解 LLVM 的優(yōu)化內(nèi)容,以及 clang 能做哪些優(yōu)化集晚,我們先看一個(gè)略微復(fù)雜的 C 程序:這個(gè)函數(shù)主要是遞歸計(jì)算階乘:
先看看不做優(yōu)化的編譯情況窗悯,執(zhí)行下面命令:
clang -O0-emit-llvm factorial.c-c-o factorial.bc && llvm-dis < factorial.bc
重點(diǎn)看一下針對(duì)階乘部分生成的代碼:
看一下%9標(biāo)注的那一行区匣,這行代碼正是遞歸調(diào)用階乘函數(shù)本身偷拔,實(shí)際上這樣調(diào)用是非常低效的,因?yàn)槊看芜f歸調(diào)用都要重新壓棧亏钩。接下來可以看一下優(yōu)化后的效果莲绰,可以通過這樣的方式開啟優(yōu)化 -- 將-03標(biāo)志傳給 clang:
clang -O3-emit-llvm factorial.c-c-o factorial.bc && llvm-dis < factorial.bc
現(xiàn)在階乘計(jì)算相關(guān)代碼編譯后生成的代碼如下:
即便我們的函數(shù)并沒有按照尾遞歸的方式編寫,clang 仍然能對(duì)其做優(yōu)化處理姑丑,讓該函數(shù)編譯的結(jié)果中只包含一個(gè)循環(huán)蛤签。當(dāng)然 clang 能對(duì)代碼進(jìn)行的優(yōu)化還有很多方面树碱【J可以看以下這個(gè)比較不錯(cuò)的 gcc 的優(yōu)化例子ridiculousfish.com。
延伸閱讀
LLVM blog: posts tagged 'optimization'
LLVM blog: vectorization improvements
LLVM blog: greedy register allocation
如何在實(shí)際中應(yīng)用這些特性
剛剛我們探討了編譯的全過程胰坟,從標(biāo)記到解析留拾,從抽象語法樹到分析檢查戳晌,再到匯編。讀者不禁要問痴柔,為什么要關(guān)注這些沦偎?
使用 libclan g或 clang 插件
之所以 clang 很酷:是因?yàn)樗且粋€(gè)開源的項(xiàng)目、并且它是一個(gè)非常好的工程:幾乎可以說全身是寶。使用者可以創(chuàng)建自己的 clang 版本豪嚎,針對(duì)自己的需求對(duì)其進(jìn)行改造搔驼。比如說,可以改變 clang 生成代碼的方式侈询,增加更強(qiáng)的類型檢查舌涨,或者按照自己的定義進(jìn)行代碼的檢查分析等等。要想達(dá)成以上的目標(biāo)扔字,有很多種方法泼菌,其中最簡單的就是使用一個(gè)名為libclang的C類庫。libclang 提供的 API 非常簡單啦租,可以對(duì) C 和 clang 做橋接哗伯,并可以用它對(duì)所有的源碼做分析處理。不過篷角,根據(jù)我的經(jīng)驗(yàn)焊刹,如果使用者的需求更高,那么 libclang 就不怎么行了恳蹲。針對(duì)這種情況虐块,推薦使用Clangkit,它是基于 clang 提供的功能嘉蕾,用 Objective-C 進(jìn)行封裝的一個(gè)庫贺奠。
最后,clang 還提供了一個(gè)直接使用 LibTooling 的 C++ 類庫错忱。這里要做的事兒比較多儡率,而且涉及到 C++,但是它能夠發(fā)揮 clang 的強(qiáng)大功能以清。用它你可以對(duì)源碼做任意類型的分析儿普,甚至重寫程序。如果你想要給 clang 添加一些自定義的分析掷倔、創(chuàng)建自己的重構(gòu)器 (refactorer)眉孩、或者需要基于現(xiàn)有代碼做出大量修改,甚至想要基于工程生成相關(guān)圖形或者文檔勒葱,那么 LibTooling 是很好的選擇浪汪。
自定義分析器
開發(fā)者可以按照Tutorial for building tools using LibTooling中的說明去構(gòu)造 LLVM ,clang 以及 clan g的附加工具凛虽。需要注意的是死遭,編譯代碼是需要花費(fèi)一些時(shí)間的,即時(shí)機(jī)器已經(jīng)很快了涩维,但是在編譯期間殃姓,我還是可以吃頓飯的袁波。
接下來,進(jìn)入到 LLVM 目錄蜗侈,然后執(zhí)行命令cd ~/llvm/tools/clang/tools/篷牌。在這個(gè)目錄中,可以創(chuàng)建自己獨(dú)立的 clang 工具踏幻。例如枷颊,我們創(chuàng)建一個(gè)小工具,用來檢查某個(gè)庫是否正確使用该面。首先將樣例工程克隆到本地夭苗,然后輸入make。這樣就會(huì)生成一個(gè)名為example的二進(jìn)制文件隔缀。
我們的使用場(chǎng)景是:假如有一個(gè)Observer類, 代碼如下所示:
接下來题造,我們想要檢查一下每當(dāng)這個(gè)類被調(diào)用的時(shí)候,在target對(duì)象中是否都有對(duì)應(yīng)的action方法存在猾瘸〗缗猓可以寫個(gè) C++ 函數(shù)來做這件事(注意,這是我第一次寫 C++ 程序牵触,可能不那么嚴(yán)謹(jǐn)):
上面的這個(gè)方法首先查找消息表達(dá)式淮悼, 以O(shè)bserver作為接收者,observerWithTarget:action:作為 selector揽思,然后檢查 target 中是否存在相應(yīng)的方法袜腥。雖然這個(gè)例子有點(diǎn)兒刻意,但如果你想要利用 AST 對(duì)自己的代碼庫做某些檢查钉汗,按照上面的例子來就可以了羹令。
clang的其他特性
clang還有許多其他的用途。比如儡湾,可以寫編譯器插件(例如特恬,類似上面的檢查器例子)并且動(dòng)態(tài)的加載到編譯器中。雖然我沒有親自實(shí)驗(yàn)過徐钠,但是我覺得在 Xcode 中應(yīng)該是可行的。再比如役首,也可以通過編寫 clang 插件來自定義代碼樣式(具體可以參見編譯過程)尝丐。
另外,如果想對(duì)現(xiàn)有的代碼做大規(guī)模的重構(gòu)衡奥, 而 Xcode 或 AppCode 本身集成的重構(gòu)工具無法達(dá)你的要求爹袁,你完全可以用 clang 自己寫個(gè)重構(gòu)工具。聽起來有點(diǎn)兒可怕矮固,讀讀下面的文檔和教程失息,你會(huì)發(fā)現(xiàn)其實(shí)沒那么難譬淳。
最后,如果是真的有這種需求盹兢,你完全可以引導(dǎo) Xcdoe 使用你自己編譯的 clang 邻梆。再一次,如果你去嘗試绎秒,其實(shí)這些事兒真的沒想象中那么復(fù)雜浦妄,反而會(huì)發(fā)現(xiàn)許多個(gè)中樂趣。
延伸閱讀
X86_64 Assembly Language Tutorial
Custom clang Build with Xcode (I)和(II)
LLVM blog: What every C programmer should know (I),(II)和(III)