鍍金池/ 教程/ iOS/ 編譯器
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

編譯器

編譯器做些什么?

本文主要探討一下編譯器主要做些什么,以及如何有效的利用編譯器。

簡單的說,編譯器有兩個(gè)職責(zé):把 Objective-C 代碼轉(zhuǎn)化成低級代碼,以及對代碼做分析,確保代碼中沒有任何明顯的錯(cuò)誤。

現(xiàn)在,Xcode 的默認(rèn)編譯器是 clang。本文中我們提到的編譯器都表示 clang。clang 的功能是首先對 Objective-C 代碼做分析檢查,然后將其轉(zhuǎn)換為低級的類匯編代碼:LLVM Intermediate Representation(LLVM 中間表達(dá)碼)。接著 LLVM 會執(zhí)行相關(guān)指令將 LLVM IR 編譯成目標(biāo)平臺上的本地字節(jié)碼,這個(gè)過程的完成方式可以是即時(shí)編譯 (Just-in-time),或在編譯的時(shí)候完成。

LLVM 指令的一個(gè)好處就是可以在支持 LLVM 的任意平臺上生成和運(yùn)行 LLVM 指令。例如,你寫的一個(gè) iOS app, 它可以自動(dòng)的運(yùn)行在兩個(gè)完全不同的架構(gòu)(Inter 和 ARM)上,LLVM 會根據(jù)不同的平臺將 IR 碼轉(zhuǎn)換為對應(yīng)的本地字節(jié)碼。

LLVM 的優(yōu)點(diǎn)主要得益于它的三層式架構(gòu) -- 第一層支持多種語言作為輸入(例如 C, ObjectiveC, C++ 和 Haskell),第二層是一個(gè)共享式的優(yōu)化器(對 LLVM IR 做優(yōu)化處理),第三層是許多不同的目標(biāo)平臺(例如 Intel, ARM 和 PowerPC)。在這三層式的架構(gòu)中,如果你想要添加一門語言到 LLVM 中,那么可以把重要精力集中到第一層上,如果想要增加另外一個(gè)目標(biāo)平臺,那么你沒必要過多的考慮輸入語言。在書 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 命令觀察:

% clang -ccc-print-phases hello.m

0: input, "hello.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, assembler
3: assembler, {2}, object
4: linker, {3}, image
5: bind-arch, "x86_64", {4}, image

本文我們將重點(diǎn)關(guān)注第一階段和第二階段。在文章 Mach-O Executables 中,Daniel 會對第三階段和第四階段進(jìn)行闡述。

預(yù)處理

每當(dāng)編源譯文件的時(shí)候,編譯器首先做的是一些預(yù)處理工作。比如預(yù)處理器會處理源文件中的宏定義,將代碼中的宏用其對應(yīng)定義的具體內(nèi)容進(jìn)行替換。

例如,如果在源文件中出現(xiàn)下述代碼:

#import <Foundation/Foundation.h>

預(yù)處理器對這行代碼的處理是用 Foundation.h 文件中的內(nèi)容去替換這行代碼,如果 Foundation.h 中也使用了類似的宏引入,則會按照同樣的處理方式用各個(gè)宏對應(yīng)的真正代碼進(jìn)行逐級替代。

這也就是為什么人們主張頭文件最好盡量少的去引入其他的類或庫,因?yàn)橐氲臇|西越多,編譯器需要做的處理就越多。例如,在頭文件中用:

@class MyClass;

代替:

#import "MyClass.h"

這么寫是告訴編譯器 MyClass 是一個(gè)類,并且在 .m 實(shí)現(xiàn)文件中可以通過 import MyClass.h 的方式來使用它。

假設(shè)我們寫了一個(gè)簡單的 C 程序 hello.c:

#include <stdio.h>

int main() {
  printf("hello world\n");
  return 0;
}

然后給上面的代碼執(zhí)行以下預(yù)處理命令,看看是什么效果:

clang -E hello.c | less

接下來看看處理后的代碼,一共是 401 行。如果將如下一行代碼添加到上面代碼的頂部::

#import <Foundation/Foundation.h>

再執(zhí)行一下上面的預(yù)處理命令,處理后的文件代碼行數(shù)暴增至 89,839 行。這個(gè)數(shù)字比某些操作系統(tǒng)的總代碼行數(shù)還要多。

幸好,目前的情況已經(jīng)改善許多了:引入了模塊 - modules功能,這使預(yù)處理變得更加的高級。

自定義宏

我們來看看另外一種情形定義或者使用自定義宏,比如定義了如下宏:

#define MY_CONSTANT 4

那么,凡是在此行宏定義作用域內(nèi),輸入了 MY_CONSTANT,在預(yù)處理過程中 MY_CONSTANT 都會被替換成 4。我們定義的宏也是可以攜帶參數(shù)的, 比如:

#define MY_MACRO(x) x

鑒于本文的內(nèi)容所限,就不對強(qiáng)大的預(yù)處理做更多、更全面的展開討論了。但是還是要強(qiáng)調(diào)一點(diǎn),建議大家不要在需要預(yù)處理的代碼中加入內(nèi)聯(lián)代碼邏輯。

例如,下面這段代碼,這樣用沒什么問題:

#define MAX(a,b) a > b ? a : b

int main() {
  printf("largest: %d\n", MAX(10,100));
  return 0;
}

但是如果換成這么寫:

#define MAX(a,b) a > b ? a : b

int main() {
  int i = 200;
  printf("largest: %d\n", MAX(i++,100));
  printf("i: %d\n", i);
  return 0;
}

clang max.c 編譯一下,結(jié)果是:

largest: 201
i: 202

clang -E max.c 進(jìn)行宏展開的預(yù)處理結(jié)果是如下所示:

int main() {
  int i = 200;
  printf("largest: %d\n", i++ > 100 ? i++ : 100);
  printf("i: %d\n", i);
  return 0;
}

本例是典型的宏使用不當(dāng),而且通常這類問題非常隱蔽且難以 debug 。針對本例這類情況,最好使用 static inline:

#include <stdio.h>
static const int MyConstant = 200;

static inline int max(int l, int r) {
   return l > r ? l : r;
}

int main() {
  int i = MyConstant;
  printf("largest: %d\n", max(i++,100));
  printf("i: %d\n", i);
  return 0;
}

這樣改過之后,就可以輸出正常的結(jié)果 (i:201)。因?yàn)檫@里定義的代碼是內(nèi)聯(lián)的 (inlined),所以它的效率和宏變量差不多,但是可靠性比宏定義要好許多。再者,還可以設(shè)置斷點(diǎn)、類型檢查以及避免異常行為。

基本上,宏的最佳使用場景是日志輸出,可以使用 __FILE____LINE__ 和 assert 宏。

詞法解析標(biāo)記

預(yù)處理完成以后,每一個(gè) .m 源文件里都有一堆的聲明和定義。這些代碼文本都會從 string 轉(zhuǎn)化成特殊的標(biāo)記流。

例如,下面是一段簡單的 Objective-C hello word 程序:

int main() {
  NSLog(@"hello, %@", @"world");
  return 0;
}

利用 clang 命令 clang -Xclang -dump-tokens hello.m 來將上面代碼的標(biāo)記流導(dǎo)出:

int 'int'        [StartOfLine]  Loc=<hello.m:4:1>
identifier 'main'        [LeadingSpace] Loc=<hello.m:4:5>
l_paren '('             Loc=<hello.m:4:9>
r_paren ')'             Loc=<hello.m:4:10>
l_brace '{'      [LeadingSpace] Loc=<hello.m:4:12>
identifier 'NSLog'       [StartOfLine] [LeadingSpace]   Loc=<hello.m:5:3>
l_paren '('             Loc=<hello.m:5:8>
at '@'          Loc=<hello.m:5:9>
string_literal '"hello, %@"'            Loc=<hello.m:5:10>
comma ','               Loc=<hello.m:5:21>
at '@'   [LeadingSpace] Loc=<hello.m:5:23>
string_literal '"world"'                Loc=<hello.m:5:24>
r_paren ')'             Loc=<hello.m:5:31>
semi ';'                Loc=<hello.m:5:32>
return 'return'  [StartOfLine] [LeadingSpace]   Loc=<hello.m:6:3>
numeric_constant '0'     [LeadingSpace] Loc=<hello.m:6:10>
semi ';'                Loc=<hello.m:6:11>
r_brace '}'      [StartOfLine]  Loc=<hello.m:7:1>
eof ''          Loc=<hello.m:7:2>

仔細(xì)觀察可以發(fā)現(xiàn),每一個(gè)標(biāo)記都包含了對應(yīng)的源碼內(nèi)容和其在源碼中的位置。注意這里的位置是宏展開之前的位置,這樣一來,如果編譯過程中遇到什么問題,clang 能夠在源碼中指出出錯(cuò)的具體位置。

解析

接下來要說的東西比較有意思:之前生成的標(biāo)記流將會被解析成一棵抽象語法樹 (abstract syntax tree -- AST)。由于 Objective-C 是一門復(fù)雜的語言,因此解析的過程不簡單。解析過后,源程序變成了一棵抽象語法樹:一棵代表源程序的樹。假設(shè)我們有一個(gè)程序 hello.m

#import <Foundation/Foundation.h>

@interface World
- (void)hello;
@end

@implementation World
- (void)hello {
  NSLog(@"hello, world");
}
@end

int main() {
   World* world = [World new];
   [world hello];
}

當(dāng)我們執(zhí)行 clang 命令 clang -Xclang -ast-dump -fsyntax-only hello.m 之后,命令行中輸出的結(jié)果如下所示::

@interface World- (void) hello;
@end
@implementation World
- (void) hello (CompoundStmt 0x10372ded0 <hello.m:8:15, line:10:1>
  (CallExpr 0x10372dea0 <line:9:3, col:24> 'void'
    (ImplicitCastExpr 0x10372de88 <col:3> 'void (*)(NSString *, ...)' <FunctionToPointerDecay>
      (DeclRefExpr 0x10372ddd8 <col:3> 'void (NSString *, ...)' Function 0x1023510d0 'NSLog' 'void (NSString *, ...)'))
    (ObjCStringLiteral 0x10372de38 <col:9, col:10> 'NSString *'
      (StringLiteral 0x10372de00 <col:10> 'char [13]' lvalue "hello, world"))))

@end
int main() (CompoundStmt 0x10372e118 <hello.m:13:12, line:16:1>
  (DeclStmt 0x10372e090 <line:14:4, col:30>
    0x10372dfe0 "World *world =
      (ImplicitCastExpr 0x10372e078 <col:19, col:29> 'World *' <BitCast>
        (ObjCMessageExpr 0x10372e048 <col:19, col:29> 'id':'id' selector=new class='World'))")
  (ObjCMessageExpr 0x10372e0e8 <line:15:4, col:16> 'void' selector=hello
    (ImplicitCastExpr 0x10372e0d0 <col:5> 'World *' <LValueToRValue>
      (DeclRefExpr 0x10372e0a8 <col:5> 'World *' lvalue Var 0x10372dfe0 'world' 'World *'))))

在抽象語法樹中的每個(gè)節(jié)點(diǎn)都標(biāo)注了其對應(yīng)源碼中的位置,同樣的,如果產(chǎn)生了什么問題,clang 可以定位到問題所在處的源碼位置。

延伸閱讀

靜態(tài)分析

一旦編譯器把源碼生成了抽象語法樹,編譯器可以對這棵樹做分析處理,以找出代碼中的錯(cuò)誤,比如類型檢查:即檢查程序中是否有類型錯(cuò)誤。例如:如果代碼中給某個(gè)對象發(fā)送了一個(gè)消息,編譯器會檢查這個(gè)對象是否實(shí)現(xiàn)了這個(gè)消息(函數(shù)、方法)。此外,clang 對整個(gè)程序還做了其它更高級的一些分析,以確保程序沒有錯(cuò)誤。

類型檢查

每當(dāng)開發(fā)人員編寫代碼的時(shí)候,clang 都會幫忙檢查錯(cuò)誤。其中最常見的就是檢查程序是否發(fā)送正確的消息給正確的對象,是否在正確的值上調(diào)用了正確的函數(shù)。如果你給一個(gè)單純的 NSObject* 對象發(fā)送了一個(gè) hello 消息,那么 clang 就會報(bào)錯(cuò)。同樣,如果你創(chuàng)建了 NSObject 的一個(gè)子類 Test, 如下所示:

@interface Test : NSObject
@end

然后試圖給這個(gè)子類中某個(gè)屬性設(shè)置一個(gè)與其自身類型不相符的對象,編譯器會給出一個(gè)可能使用不正確的警告。

一般會把類型分為兩類:動(dòng)態(tài)的和靜態(tài)的。動(dòng)態(tài)的在運(yùn)行時(shí)做檢查,靜態(tài)的在編譯時(shí)做檢查。以往,編寫代碼時(shí)可以向任意對象發(fā)送任何消息,在運(yùn)行時(shí),才會檢查對象是否能夠響應(yīng)這些消息。由于只是在運(yùn)行時(shí)做此類檢查,所以叫做動(dòng)態(tài)類型。

至于靜態(tài)類型,是在編譯時(shí)做檢查。當(dāng)在代碼中使用 ARC 時(shí),編譯器在編譯期間,會做許多的類型檢查:因?yàn)榫幾g器需要知道哪個(gè)對象該如何使用。例如,如果 myObject 沒有 hello 方法,那么就不能寫如下這行代碼了:

[myObject hello]

其他分析

clang 在靜態(tài)分析階段,除了類型檢查外,還會做許多其它一些分析。如果你把 clang 的代碼倉庫 clone 到本地,然后進(jìn)入目錄 lib/StaticAnalyzer/Checkers,你會看到所有靜態(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è)會生成嚴(yán)重錯(cuò)誤的警告 “performSelector may cause a leak because its selector is unknown” 。

代碼生成

clang 完成代碼的標(biāo)記,解析和分析后,接著就會生成 LLVM 代碼。下面繼續(xù)看看hello.c

#include <stdio.h>

int main() {
  printf("hello world\n");
  return 0;
}

要把這段代碼編譯成 LLVM 字節(jié)碼(絕大多數(shù)情況下是二進(jìn)制碼格式),我們可以執(zhí)行下面的命令:

clang -O3 -emit-LLVM hello.c -c -o hello.bc

接著用另一個(gè)命令來查看剛剛生成的二進(jìn)制文件:

llvm-dis < hello.bc | less

輸出如下:

; ModuleID = '<stdin>'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.8.0"

@str = private unnamed_addr constant [12 x i8] c"hello world\00"

; Function Attrs: nounwind ssp uwtable
define i32 @main() #0 {
  %puts = tail call i32 @puts(i8* getelementptr inbounds ([12 x i8]* @str, i64 0, i64 0))
  ret i32 0
}

; Function Attrs: nounwind
declare i32 @puts(i8* nocapture) #1

attributes #0 = { nounwind ssp uwtable }
attributes #1 = { nounwind }

在上面的代碼中,可以看到 main 函數(shù)只有兩行代碼:一行輸出string,另一行返回 0。

再換一個(gè)程序,拿 five.m 為例,對其做相同的編譯,然后執(zhí)行 LLVM-dis < five.bc | less:

#include <stdio.h>
#import <Foundation/Foundation.h>

int main() {
  NSLog(@"%@", [@5 description]);
  return 0;
}

拋開其他的不說,單看 main 函數(shù):

define i32 @main() #0 {
  %1 = load %struct._class_t** @"\01L_OBJC_CLASSLIST_REFERENCES_$_", align 8
  %2 = load i8** @"\01L_OBJC_SELECTOR_REFERENCES_", align 8, !invariant.load !4
  %3 = bitcast %struct._class_t* %1 to i8*
  %4 = tail call %0* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to %0* (i8*, i8*, i32)*)(i8* %3, i8* %2, i32 5)
  %5 = load i8** @"\01L_OBJC_SELECTOR_REFERENCES_2", align 8, !invariant.load !4
  %6 = bitcast %0* %4 to i8*
  %7 = tail call %1* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to %1* (i8*, i8*)*)(i8* %6, i8* %5)
  tail call void (%1*, ...)* @NSLog(%1* bitcast (%struct.NSConstantString* @_unnamed_cfstring_ to %1*), %1* %7)
  ret i32 0
}

上面代碼中最重要的是第 4 行,它創(chuàng)建了一個(gè) NSNumber 對象。第 7 行,給這個(gè) number 對象發(fā)送了一個(gè) description 消息。第 8 行,將 description 消息返回的內(nèi)容打印出來。

優(yōu)化

要想了解 LLVM 的優(yōu)化內(nèi)容,以及 clang 能做哪些優(yōu)化,我們先看一個(gè)略微復(fù)雜的 C 程序:這個(gè)函數(shù)主要是遞歸計(jì)算 階乘

#include <stdio.h>

int factorial(int x) {
   if (x > 1) return x * factorial(x-1);
   else return 1;
}

int main() {
  printf("factorial 10: %d\n", factorial(10));
}

先看看不做優(yōu)化的編譯情況,執(zhí)行下面命令:

clang -O0 -emit-llvm factorial.c  -c -o factorial.bc && llvm-dis < factorial.bc

重點(diǎn)看一下針對 階乘 部分生成的代碼:

define i32 @factorial(i32 %x) #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 %x, i32* %2, align 4
  %3 = load i32* %2, align 4
  %4 = icmp sgt i32 %3, 1
  br i1 %4, label %5, label %11

; <label>:5                                       ; preds = %0
  %6 = load i32* %2, align 4
  %7 = load i32* %2, align 4
  %8 = sub nsw i32 %7, 1
  %9 = call i32 @factorial(i32 %8)
  %10 = mul nsw i32 %6, %9
  store i32 %10, i32* %1
  br label %12

; <label>:11                                      ; preds = %0
  store i32 1, i32* %1
  br label %12

; <label>:12                                      ; preds = %11, %5
  %13 = load i32* %1
  ret i32 %13
}

看一下 %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)代碼編譯后生成的代碼如下:

define i32 @factorial(i32 %x) #0 {
  %1 = icmp sgt i32 %x, 1
  br i1 %1, label %tailrecurse, label %tailrecurse._crit_edge

tailrecurse:                                      ; preds = %tailrecurse, %0
  %x.tr2 = phi i32 [ %2, %tailrecurse ], [ %x, %0 ]
  %accumulator.tr1 = phi i32 [ %3, %tailrecurse ], [ 1, %0 ]
  %2 = add nsw i32 %x.tr2, -1
  %3 = mul nsw i32 %x.tr2, %accumulator.tr1
  %4 = icmp sgt i32 %2, 1
  br i1 %4, label %tailrecurse, label %tailrecurse._crit_edge

tailrecurse._crit_edge:                           ; preds = %tailrecurse, %0
  %accumulator.tr.lcssa = phi i32 [ 1, %0 ], [ %3, %tailrecurse ]
  ret i32 %accumulator.tr.lcssa
}

即便我們的函數(shù)并沒有按照尾遞歸的方式編寫,clang 仍然能對其做優(yōu)化處理,讓該函數(shù)編譯的結(jié)果中只包含一個(gè)循環(huán)。當(dāng)然 clang 能對代碼進(jìn)行的優(yōu)化還有很多方面??梢钥匆韵逻@個(gè)比較不錯(cuò)的 gcc 的優(yōu)化例子ridiculousfish.com

延伸閱讀

如何在實(shí)際中應(yīng)用這些特性

剛剛我們探討了編譯的全過程,從標(biāo)記到解析,從抽象語法樹到分析檢查,再到匯編。讀者不禁要問,為什么要關(guān)注這些?

使用 libclan g或 clang 插件

之所以 clang 很酷:是因?yàn)樗且粋€(gè)開源的項(xiàng)目、并且它是一個(gè)非常好的工程:幾乎可以說全身是寶。使用者可以創(chuàng)建自己的 clang 版本,針對自己的需求對其進(jìn)行改造。比如說,可以改變 clang 生成代碼的方式,增加更強(qiáng)的類型檢查,或者按照自己的定義進(jìn)行代碼的檢查分析等等。要想達(dá)成以上的目標(biāo),有很多種方法,其中最簡單的就是使用一個(gè)名為 libclang 的C類庫。libclang 提供的 API 非常簡單,可以對 C 和 clang 做橋接,并可以用它對所有的源碼做分析處理。不過,根據(jù)我的經(jīng)驗(yàn),如果使用者的需求更高,那么 libclang 就不怎么行了。針對這種情況,推薦使用 Clangkit,它是基于 clang 提供的功能,用 Objective-C 進(jìn)行封裝的一個(gè)庫。

最后,clang 還提供了一個(gè)直接使用 LibTooling 的 C++ 類庫。這里要做的事兒比較多,而且涉及到 C++,但是它能夠發(fā)揮 clang 的強(qiáng)大功能。用它你可以對源碼做任意類型的分析,甚至重寫程序。如果你想要給 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。這樣就會生成一個(gè)名為 example 的二進(jìn)制文件。

我們的使用場景是:假如有一個(gè) Observer 類, 代碼如下所示:

@interface Observer
+ (instancetype)observerWithTarget:(id)target action:(SEL)selector;
@end

接下來,我們想要檢查一下每當(dāng)這個(gè)類被調(diào)用的時(shí)候,在 target 對象中是否都有對應(yīng)的 action 方法存在??梢詫憘€(gè) C++ 函數(shù)來做這件事(注意,這是我第一次寫 C++ 程序,可能不那么嚴(yán)謹(jǐn)):

virtual bool VisitObjCMessageExpr(ObjCMessageExpr *E) {
  if (E->getReceiverKind() == ObjCMessageExpr::Class) {
    QualType ReceiverType = E->getClassReceiver();
    Selector Sel = E->getSelector();
    string TypeName = ReceiverType.getAsString();
    string SelName = Sel.getAsString();
    if (TypeName == "Observer" && SelName == "observerWithTarget:action:") {
      Expr *Receiver = E->getArg(0)->IgnoreParenCasts();
      ObjCSelectorExpr* SelExpr = cast<ObjCSelectorExpr>(E->getArg(1)->IgnoreParenCasts());
      Selector Sel = SelExpr->getSelector();
      if (const ObjCObjectPointerType *OT = Receiver->getType()->getAs<ObjCObjectPointerType>()) {
        ObjCInterfaceDecl *decl = OT->getInterfaceDecl();
        if (! decl->lookupInstanceMethod(Sel)) {
          errs() << "Warning: class " << TypeName << " does not implement selector " << Sel.getAsString() << "\n";
          SourceLocation Loc = E->getExprLoc();
          PresumedLoc PLoc = astContext->getSourceManager().getPresumedLoc(Loc);
          errs() << "in " << PLoc.getFilename() << " <" << PLoc.getLine() << ":" << PLoc.getColumn() << ">\n";
        }
      }
    }
  }
  return true;
}

上面的這個(gè)方法首先查找消息表達(dá)式, 以 Observer 作為接收者, observerWithTarget:action: 作為 selector,然后檢查 target 中是否存在相應(yīng)的方法。雖然這個(gè)例子有點(diǎn)兒刻意,但如果你想要利用 AST 對自己的代碼庫做某些檢查,按照上面的例子來就可以了。

clang的其他特性

clang還有許多其他的用途。比如,可以寫編譯器插件(例如,類似上面的檢查器例子)并且動(dòng)態(tài)的加載到編譯器中。雖然我沒有親自實(shí)驗(yàn)過,但是我覺得在 Xcode 中應(yīng)該是可行的。再比如,也可以通過編寫 clang 插件來自定義代碼樣式(具體可以參見 編譯過程)。

另外,如果想對現(xiàn)有的代碼做大規(guī)模的重構(gòu), 而 Xcode 或 AppCode 本身集成的重構(gòu)工具無法達(dá)你的要求,你完全可以用 clang 自己寫個(gè)重構(gòu)工具。聽起來有點(diǎn)兒可怕,讀讀下面的文檔和教程,你會發(fā)現(xiàn)其實(shí)沒那么難。

最后,如果是真的有這種需求,你完全可以引導(dǎo) Xcdoe 使用你自己編譯的 clang 。再一次,如果你去嘗試,其實(shí)這些事兒真的沒想象中那么復(fù)雜,反而會發(fā)現(xiàn)許多個(gè)中樂趣。

延伸閱讀