1. 寫在前面
現在出去面試,啟動優化是繞不開的,到底我們的
APP
該如何去進行優化呢 ?在優化之前我們必須要先了解
LLVM
,那什麼是
LLVM
呢?
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIyVGduV2YfNWawNyZuBnL3EDNzcDZwI2MllTZ4MjZ5ImN0QzY4kTZiRmZ4YWM1EzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
在介紹
LLVM
之前,先來認識一下
解釋型語言
和
編譯型語言
。
我們編寫的是偏向于我們人類直接的語言,我們非常輕松的就了解了,但是對于
源代碼
(CPU)而言,簡直就是個天書,計算機是無法直接運作的。計算機隻能識别某些特定的
計算機硬體
,是以我們的代碼在程式真正運作之前必須将源代碼
二進制指令
成二進制指令。源代碼轉換成二進制指令,不同的程式設計語言有不同的規定。
轉換
解釋型語言
有的程式設計語言可以一邊執行一邊轉換,不會生成可執行檔案再去執行,這種程式設計語言稱為
解釋型語言
,使用的轉換工具稱為
解釋器
,比如
Python
、
JavaScript
、
PHP
等。
下面就舉個例子,使用指令建立立一個
vim
檔案,字尾為
python
,寫入代碼
.py
,通過
print("hello world!")
指令,解釋這段代碼,列印一下
python
這句話。
hello world !
我看可以看到解釋型語言,它是邊解釋邊執行,不可脫離解釋器環境運作。電腦自帶了
MAC
環境,無需另外手動配置環境。
Python
編譯型語言
有的程式設計語言要轉換成
二進制指令
,也就是生成一個可執行程式這種程式設計語言稱為
編譯型語言
,使用的轉換工具稱為
編譯器
,比如
C
語言、
C++
、
OC
等。
編譯型語言也同樣舉個例子,建立立一個 C
檔案,寫入如下代碼:
#include<stdio.h>
int main (int argc,char *agrv[])
{
printf("hello world\n");
return 0;
}
通過
clang hello.c
指令,進行編譯處理,會生成一個可執行檔案,如下圖中紅色的
a.out
檔案。
這個可執行檔案,可以直接運作,通過
./a.out
即可運作,如圖中也可以正常輸出
hello world
這句話。
編譯型語言是先整體編譯,再執行,運作速度快,任意改動需重新編譯,可脫離編譯環境運作。
小結:
- 解釋型語言:讀到相應代碼就直接執行。
- 編譯型語言:先将代碼編譯成計算機可以識别的二進制檔案,才能執行。
擴充:
通過 open /usr/bin
指令可以檢視,電腦上安裝的一些系統軟體。
/usr
不是
user
的縮寫,其實
us
r是
Unix Software Resource
的縮寫, 也就是
Unix
作業系統軟體資源所放置的目錄,而不是使用者的資料;所有系統預設的軟體都會放置到
/us
r, 系統安裝完時,這個目錄會占用最多的硬碟容量。
在該目錄下可以看到,有我們的編譯器,還有
clang
解釋器,如下圖所示:
Python
MacOS
系統 預設安裝的是
python2
的環境,輸入
python
,按下
enter
Enter鍵,可以檢視:
警告:不推薦使用,為了與舊軟體相容
Python 2.7
中才包含了此版本。
macOS
的未來版本将不包含
macOS
Python 2.7
。
相反,建議您從終端内過渡到使用
。
“python3”
如果你是
python
的開發者,那麼日常使用的是
python3
,可以在終端中輸入
python3
檢視是否支援:
可以看到我的電腦是支援的,我這裡的版本是
Python 3.7.7
的版本,如果你的電腦沒有支援,可以去
python
官網下載下傳。
2. LLVM
LLVM簡介
LLVM
是構架編譯器(
compiler
)的架構系統,以
C++
編寫而成,用于優化以任意程式語言編寫的程式的
編譯時間
(
compile-time
)、
連結時間
(
link-time
)、
運作時間
(
run-time
)以及
空閑時間
(
idle-time
),對開發者保持開放,并相容已有腳本。
LLVM
計劃啟動于
2000
年,最初由美國
UIUC
大學的
ChrisLattner
博士主持開展。
2006
年
ChrisLattner
加盟
AppleInc
并緻力于
LLVM
在
Apple
開發體系中的應用。
Apple
也是
LLVM
計劃的主要資助者。目前
LLVM
已經被蘋果
IOS
開發工具、
Xilinx Vivado
、
Facebook
、
Google
等各大公司采用。
傳統編譯器設計
我們先來看看傳統編譯器設計是怎麼樣的,如下圖所示:
- 編譯器前端(Frontend)
編譯器前端的任務是
解析源代碼
。它會進行:
詞法分析
,
文法分析
,
語義分析
,檢查源代碼是否存在錯誤,然後建構
抽象文法樹
(Abstract Syntax Tree, AST),
LLVM
的前端還會生成
中間代碼
(intermediate representation,IR)。
- 優化器(Optimizer)
優化器負責進行各種優化,改善代碼的運作時間,例如消除備援計算等。
- 後端(Backend)/代碼生成器(CodeGenerator)
将代碼映射到目标指令集,生成機器語言,并且進行機器相關的代碼優化。
iOS的編譯器架構
ObjectiveC/C/C++
使用的編譯器前端是
Clang
,
Swift
是
Swift
,後端都是
LLVM
。
LLVM的設計
當編譯器決定支援多種源語言或多種硬體架構時,
LLVM
最重要的地方就來了。
其他的編譯器如
GCC
是非常成功的一款編譯器,但由于它是作為整體應用程式設計的,是以它的用途受到了很大的限制。
LLVM
設計的最重要方面是,使用通用的代碼表示形式
(IR)
,它是用來在編譯器中表示
代碼
的形式。是以
LLVM
可以為任何程式設計語言獨立編寫前端,并且可以為任意硬體架構
獨立
編寫後端。
- Clang
對于我們的開發人員來說,看得見摸得着的,接觸最多的就是我們的
Clang
。
Clang
是
LLVM
項目中的一個子項目。它是基于
LLVM
架構的
輕量級編譯器
,誕生之初是為了替代
GCC
,提供更快的編譯速度。它是負責編譯
C
、
C++
、
Objecte- C
語言的編譯器,它屬于整個
LLVM
架構中的,編譯器前端。對于開發者來說,研究
Clang
可以給我們帶來很多好處。
3. 編譯流程
那麼我們寫一段代碼,來測試一下,看看編譯流程是什麼樣子的。
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
編譯的各個階段
通過一下指令,可以列印源碼的編譯階段。
clang -ccc-print-phases main.m
- 0:
:找到源檔案。輸入檔案
- 1:
:這個過程處理包括宏的替換,頭檔案的導入。預處理階段
- 2:
:進行詞法分析、文法分析、檢測文法是否正确,最終生成編譯階段
。IR
- 3:
:這裡後端
會通過一個一個的LLVM
(可以了解為一個節點)去優化,每個Pass
做一些事情,最終生成Pass
。彙編代碼
- 4:彙編代碼
。生成目标檔案
- 5:
:連結需要的動态庫和靜态庫,生成相應的連結
可執行檔案。鏡像
- 6:根據不同的系統架構,生成對應的可執行檔案。
上面已經知道了編譯的流程了,那麼我們一步一步去看看各個階段是什麼樣子的。
#import <stdio.h>
#define B 50
int main(int argc, const char * argv[]) {
int a = 10;
int c = 20;
printf("%d",a + c + B);
return 0;
}
預處理階段
執行如下指令
clang -E main.m >> main1.m
執行完畢後,我們可以在
main1.m
的檔案中,可以看到頭檔案的導入和
宏的替換
。
詞法分析
編譯階段-詞法分析
預處理
完成後就會進行
詞法分析
,這裡會把代碼切成一個個
Token
,比如大小括号,等于号還有字元串等。
#import <stdio.h>
#define B 50
typedef int JP_INT;
int main(int argc, const char * argv[]) {
JP_INT a = 10;
JP_INT c = 20;
printf("%d",a + c + B);
return 0;
}
clang -fmodules-fsyntax-only -Xclang -dump-tokens main.m
指令運作之後,進行了詞法分析,每一行的代碼都分開了,切成一個個
Token
。
文法分析
詞法分析完成之後就是
文法分析
,它的任務是驗證文法是否正确。在
詞法分析
的基礎上将
單詞序列組合成
各類
文法短語
,如“
程式
”,“
語句
”,“
表達式
”等等,然後将所有節點組成
抽象文法樹
(AbstractSyntaxTree,AST)。
文法分析
其目的就是對源程式進行分析判斷,在結構上是否正确。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
函數方法聲明,範圍是第FunctionDecl
行第10
個字元開始 到第1
行第15
個字元結束。第1
行第10
個字元開始,名稱叫5
,傳回值是main
類型,第一個參數的類型是int
,第二個參數的類型是int
。這裡為什麼是const char **
呢?因為數組的名稱就是一個指針,const char **
等于const char ** argv
。const char * argv[]
iOS底層探索之LLVM(一)——初識LLVM -
參數,目前行的第ParmVarDecl
個字元到第10
個字元是14
類型所占有,第int
個字元是參數14
。argc
-
複合語句,目前行第CompoundStmt
個字元到,第41
行代碼的第15
個字元,也就是1
包裹的範圍。{}
- 這兩句代碼
對應的是下面這個JP_INT a = 10; JP_INT c = 20;
iOS底層探索之LLVM(一)——初識LLVM -
調用表達式, 代碼中的CallExpr
函數的列印文法分析如下圖printf
包括iOS底層探索之LLVM(一)——初識LLVM
函數的指針,告訴我們函數的類型和傳回值的類型;第一個參數"%d",第二個參數是一個printf
加運算的結果,是由+
和a
相加之和,再與c
進行相加得到。50
-
傳回ReturnStmt
-
變量聲明VarDecl
-
字元串字面量StringLiteral
-
整型字面量IntegerLiteral
-
二進制運算符BinaryOperator
補充:如果導入的頭檔案找不到,可以指定SDK
clang isysroot/Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己的 sdk路徑) -fmodules -fsyntax-only -Xclang -ast-dump main.m
中間代碼IR
完成以上步驟後就開始生成中間代碼
IR
(intermediate representation)了,代碼生成器(
Code Generation
)會将
文法樹
自頂向下周遊逐漸翻譯成
LLVM IR
。
#import <stdio.h>
//#define B 50
//typedef int JP_INT;
int JPTest(int a,int b) {
return a + b + 1;
}
int main(int argc, const char * argv[]) {
int c = JPTest(1, 2);
printf("%d",c);
return 0;
}
通過下面指令可以生成
.ll
的文本檔案,檢視
IR
代碼,如下。
clang -S -fobjc-arc -emit-llvm main.m
從圖中可以看到,生成了一個
.ll
的檔案,使用
VS Code
打開如下:
JPTest
方法的生成的
IR
代碼解讀如下:
ObjectiveC
代碼在這一步會進行
runtime
的橋接:
property
合成,
ARC
處理等。
IR
的基本文法
@: 全局辨別
% : 局部辨別
alloca: 開辟空間
align: 記憶體對齊
i32: 32個bit,4個位元組
store: 寫入記憶體
load: 讀取資料
call: 調用函數
ret: 傳回
以上生成的代碼是沒有經過優化的,我們可以手動的開啟編譯器的優化,在
XCode
裡面可以進行設定的。
IR的優化
LLVM
的優化級别分别是-O0-O1-O2-O3-Os(第一個是大寫英文字母
O
)
使用終端的指令,也是可以優化的,那麼現在去優化一下,剛剛的代碼。
clang -Os -S -fobjc-arc -emit-llvm main.m -o main1.ll
從上面的對比圖,可以看出優化之後,
JPTest
和
mian
代碼都少了很多,在
mian
函數裡面并沒有看到調用
JPTest
函數,而是
printf
直接列印了
c
的結果
4
,這就是優化的強大之處,如下:
優化之後,直接就算出來結果了,這優化還是很給力的哈!優化等級也不是越高就越好。在
XCode
裡面的優化選項裡面
release
環境下預設的優化就是最好的了,蘋果肯定是給你最好的優化啊。
- 小結:
編譯流程:首先是
預處理
,對輸入代碼的宏進行展開;然後是
詞法分析
,會分成一個一個的
token
;再是
文法分析
,會生成
AST文法樹
;再就會生成
IR
代碼,交給優化器去處理優化代碼。
- bitCode
這是
xcode7
以後開啟
bitcode
蘋果會做進一步的優化,生成
bc
的中間代碼。我們通過優化後的
IR
代碼生成
bc
代碼,這也是一個中間代碼,目的是會根據
CPU
的不同架構生成不同大小的包(App Store 商店下載下傳)。
clang -emit-llvm -c main.ll -o main.bc
生成彙編代碼
- 生成彙編代碼
我們通過最終的
.bc
或者
.ll
代碼生成彙編代碼
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
- 生成的彙編比較
圖中是三種不同字尾生成的彙編代碼
-
直接生成的彙編是IR
行,計算優化了55
-
生成的IR
在生成彙編,在bc
的基礎上沒有進一步的優化了,還是IR
行55
- 原始的
代碼直接生成的彙編就是main
行了62
生成彙編代碼的時候也是可以再次進行優化的,那麼我們用上面生成的
.bc
試一下,開啟優化最大,看看生成的彙編是有多少行呢?
clang -Os -S -fobjc-arc main.bc -o main3.s
我們把優化等級調到最高,生成的彙編代碼就行了,比上面的
47
行少了
55
行,也就是說生成的
8
或者
IR
的時候,優化并沒有停止,每一個節點上面都有可能再次優化。
bc
生成目标檔案(彙編器)
目标檔案的生成,是
彙編器
以彙編代碼作為
輸入
,将彙編代碼轉換為
機器代碼
,最後輸出
目标檔案
(object-file),這個階段就是屬于編譯器後端的工作了。
clang -fmodules -c main.s -o main.o
通過指令,檢視下
nm
中的符号
main.o
xcrun nm -nm main.o
-
是一個是_printf
的。undefined external
-
表示在目前檔案暫時找不到符号undefined
_printf
-
表示這個符号是外部可以通路的。external
生成可執行檔案(連結)
連接配接器把編譯産生的
.o
檔案和(dylib .a)檔案,生成一個
mach-o
檔案(可執行檔案)。
clang main.o -o main
- 檢視連結之後的符号
- 現在列印的資訊就多了,
和_JPTest
也還在,偏移位址也有了,也就是說在執行檔案中的位置就确定了。_main
- 現在的外部函數除了
還有_printf
,這是為什麼呢?dyld_stub_binder
-
是在dyld_stub_binder
裡面,當我們的執行檔案dyld
進入的記憶體之後,外部的符号就會立刻馬上和mach-o
進行綁定,這個過程是dyld_stub_binder
強制綁定的。dyld
- 連結和綁定是兩個概念:連結是我要知道你外部的符号在哪個動态庫裡面,就是做個标記,我要知道去哪個動态庫裡面找到你。
- 綁定是在執行的時候,把動态庫
裡面的和你這個外部調用的libSystem
進行綁定,綁定是在執行期,連結是在編譯期。_printf
以上就是
LLVM
大緻的工作流程,接下來将介紹如何寫一個自己的
Clang
插件。
4. 寫在後面
關注我,更多内容持續輸出
- CSDN
- 掘金
- 簡書
🌹 喜歡就點個贊吧👍🌹
🌹 覺得有收獲的,可以來一波 收藏+關注,以免你下次找不到我😁🌹
🌹歡迎大家留言交流,批評指正, 轉發
請注明出處,謝謝合作!🌹