LLVM二三事

南華Coder發表於2020-04-06

一、LLVM簡介

1、概念

  • LLVM有狹義和廣義之分
    • 廣義的LLVM: LLVM編譯器架構,包括編譯前端(Frontend)、優化器(Optimizer)和編譯後端(Backend)三大部分;
    • 狹義的LLVM:LLVM架構的編譯後端,主要負責,程式碼優化、目的碼生成等;
  • LLVM專案已經成長為一個十分龐大的專案,有很多子專案,在這裡就不討論了;我們只關注:LLVM編譯器架構編譯後端LLVM

2、LLVM架構

  • LLVM架構也是採用經典的編譯器架構(三段式)設計:

    • 前端 (Frontend) :對原始碼做詞法分析、語法分析、語義分析、生成中間程式碼
    • 優化器 (Optimizer) :用於中間程式碼優化
    • 後端 (Backend) :根據中間程式碼,用於生成對應的 CPU架構的機器碼

三段式設計

  • LLVM架構示意如下:

LLVM編譯器架構

  • Objective C/C/C++語言使用的編譯器前端是 ClangSwift是 Swift 語言的編譯器,完整的表示是:Swift Compiler

  • 從LLVM架構示意圖,也能明白這種三段式架構的優勢:

    • 增加新的語言(如Swift),只需要實現新的 Frontend即可,Optimizer 和 Backend 可以重用;
    • 新增新的 CPU 架構時(如ARM),也只需要實現新的 Backend即可;

3、更多

二、LLVM處理過程

​ 大致處理過程:預處理 -> 詞法分析 -> Token -> 語法分析 -> AST -> 中間程式碼生成 -> LLVM IR -> 優化 -> 生成彙編程式碼 -> Link -> 目標檔案

1、編譯前端(Clang)處理

  • 預處理(preprocessor):替進行標頭檔案引入,巨集替換,註釋處理,條件編譯(#ifdef)等操作
  • 詞法分析(lexical anaysis):詞法分析器讀入原始檔的字元流,將他們組織稱有意義的詞素(lexeme)序列,對於每個詞素,此法分析器產生詞法單元(token)作為輸出。
  • 語法分析(semantic analysis):詞法分析產生的詞法單元Token流會被解析成一顆抽象語法樹(abstract syntax tree - AST);有了抽象語法樹,Clang就可以對這個樹進行分析,找出程式碼中的錯誤。比如型別不匹配,亦或Objective-C中向target傳送了一個未實現的訊息。
  • CodeGen(中間程式碼生成):遍歷語法樹,生成LLVM IR(中間)程式碼。LLVM IR是編譯前端的輸出,編譯後端的輸入。

2、IR處理

3、編譯後端處理

  • 生成彙編程式碼:LLVM對IR進行優化後,會針對不同CPU架構生成不同的目的碼,最後以彙編程式碼的格式輸出;

  • 彙編:彙編器以彙編程式碼作為輸入,將彙編程式碼轉換為機器程式碼,最後輸出目標檔案(Object File),

  • 連結:連結動態庫,o檔案,生成一個Mach-O格式的可執行檔案,Mach-O檔案的更多瞭解可見: Mach-O檔案周邊二三事

三、Xcode 編譯優化介紹

​ 一般而言,在Xcode下編譯專案,選擇的是Debug模式;提示Xcode編譯速度,主要是解決此類場景下問題

1、檢視Xcode編譯時間

  • 關閉Xcode
  • 開啟終端,輸入如下命令後,
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES
複製程式碼
  • 重啟Xcode,Build,可以檢視編譯時間

2、Xcode編譯過程

  • 建立app_name.app的資料夾;
  • Entitlements.plist寫入到DerivedData裡,處理打包的時候需要的資訊(如bundle-identifier);
  • 建立一些輔助檔案,比如各種.hmap,這是headermap檔案;
  • 執行CocoaPods的編譯前指令碼:檢查Manifest.lock檔案;
  • 編譯.m檔案,生成.o檔案。
  • 連結動態庫,o檔案,生成一個Mach-O格式的可執行檔案。
  • 編譯assets,編譯storyboard,連結storyboard
  • 拷貝動態庫Logger.framework,並且對其簽名
  • 執行CocoaPods編譯後指令碼:拷貝CocoaPods Target生成的Framework
  • 對app_name.App簽名,並驗證
  • 生成app_name.app

3、編譯的常規優化

  • 使用forward declaration(前向宣告):即用@class class_name,而不是#import "class_name.h",這樣能減少編譯時間;

  • 常用標頭檔案放到預編譯檔案裡:Xcode的pch檔案是預編譯檔案,這裡的內容在執行Xcode Build之前就已經被預編譯,並且引入到每一個.m檔案裡了。

  • Debug時Build Active Architecture Only設定為YES:只編譯出支援當前CPU架構的安裝包;

  • 提高編譯執行緒數:Xcode預設的編譯執行緒數,就是CPU的核心數,可適當增加編譯執行緒數來提高編譯速度

    # 獲取當前核心數:  
    sysctl -n hw.ncpu  
    
    # 設定編譯執行緒數:  
    defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 12 
    
    # 獲取編譯執行緒數:  
    defaults read com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks  
    複製程式碼
  • Debug時, Debug Information Format改為 DWARF,不需要dSYM 檔案,有一定的效果;

  • Debug時,Link-Time Optimization設定為NO:不使用LTO特性

  • Enable Index-While-Building Functionality設定為NO: 預設開啟,Xcode 編譯時會建立程式碼索引,影響編譯速度;關閉後,在編譯時就不會進行索引,而是在空閒時間建立程式碼索引(自動補全Code、查詢定義)

4、補充Link-Time Optimization(LTO)特性

  • LTO主要是對連結過程的一個優化,開啟LTO有三個好處:

    • 多餘程式碼去除(Dead code elimination):LTO在link時候,發現跨多檔案的無用程式碼;

    • 跨過程優化(Interprocedural analysis and optimization):最終不會執行的程式碼,在二進位制結果檔案中不出現;

    • 內聯優化(Inlining optimization):彙編中不使用 “call func_name” 語句,直接將外部方法內的語句“複製”到呼叫者的程式碼段內。好處是不用進行呼叫函式前的壓棧、呼叫函式後的出棧操作,提高執行效率與棧空間利用率。

  • LTO有MonilithicIncremental兩個方式選擇,Release下,建議選擇IncrementalDebug禁用LTO,因為LTO對符號剝離有點影響,可能會影響斷點的單步執行;

  • 選擇Incremental這個優化方式,會有link cache,使二次編譯的速度更快(只編譯和連結少數修改過的檔案),另一方面它還可能減小Code Size;而Monilithic 方式並不支援多執行緒和增量連結

  • 開啟 Incremental 的選項後,如果出現duplicate symbols錯誤,可以看下程式碼,有可能是全域性變數使用不規範,應該:在定義時,必須使用 static 關鍵字或者單獨在 .m 檔案中定義,.h 檔案中只能宣告變數,而不應該定義變數;

5、總結

  • 瞭解Xcode的編譯過程,使用常規的優化手段,對提升編譯速度有一定的效果(哈哈,我說的是一定);
  • 但是如果需要進一步優化編譯速度,需要完善基礎能力建設,保障需要的程式碼用原始碼,不使用的用二進位制;這涉及到很多挑戰,包括但不限於:專案元件化、Pod 庫自動出二進位制、Build前切換Pod庫二進位制和原始碼能力等;
  • 當然還有種有錢任性的做法,裝置升級,體驗過:13.3英寸Mac 升級到15.4英寸Mac(記憶體16GB、磁碟521GB、CPU:2.6 GHz ),編譯速度帶來質的提升(just a joke)。

四、補充Clang小知識

1、補充Clang的優勢

Clang比GCC有更好的效能(更快速度、更省記憶體),主要有:

  • 編譯速度快,佔用記憶體小,並且相容 GCC。
  • Clang可以發現顯示出問題所在的行和具體位置,並且可以確切地說明出現這個問題的原因,並指出錯誤的型別是什麼
  • 模組化設計:Clang採用基於庫的模組化設計,易於IDE整合及其他用途的重用
  • 診斷資訊可讀性強:在編譯過程中,Clang建立並保留了大量詳細的後設資料(metadata),有利於除錯和錯誤報告;
  • 設計清晰簡單,容易理解,易於擴充套件增強。

2、Clang提供的能力

  • LibClang:LibClang 可以訪問 Clang 的上層高階抽象的能力,比如獲取所有 Token、遍歷語法樹、程式碼補全等。
  • Clang Plugin:允許在AST 上做些操作,這些操作可以被整合到編譯中,成為編譯的一部分(影響編譯過程)。
  • LibTooling:可以完全控制 Clang AST,通過 LibTooling 能夠編寫獨立執行的語言分析和檢查工具

3、 常見的OC靜態分析工具

  • OCLint:預設支援70+檢查規則,可定製、能夠幫助發現問題

  • Clang Static Analyzer(Clang 靜態分析器): C++ 開發的,分析 C、C++ 和 Objective-C 的開源工具

  • Infer:Facebook 開源的靜態分析工具,可以檢查出 C、Java 和 Objective-C 空指標訪問、資源洩露以及記憶體洩露等問題。

參考文章

LLVM 初探

LLVM & Clang 入門

深入淺出iOS編譯

不改程式碼,Link-Time Optimization提高iOS程式碼效率 + 彙編程式碼原理分析

有贊iOS-基於二進位制的編譯提效策略

相關文章