三段式LLVM編譯器


三段式LLVM編譯器

目錄

概述

LLVM技術生態之編譯器

一、傳統編譯器的設計

二、傳統編譯器模式的實現

三、LLVM的三段式實現

四、LLVM's Code Representation:LLVM IR

1、IR的表現形式

2、IR的格式文件類型

3、IR文件的編譯處理流程

4、簡單的IR布局

5、Llvm IR 編程

五、LLVM 與 GCC有什么區別

LLVM技術生態之JIT

一、JIT概述

二、為什么要使用JIT

三、JIT生成代碼時的代碼優化技術

1、語言無關的優化技術之一:公共子表達式消除

2、語言相關的優化技術之一:數組邊界檢查消除

3、最重要的優化技術之一:方法內聯

4、最前沿的優化技術之一:逃逸分析

四、JIT運行的簡單原理

四、Llvm JIT與C++ Template模板有什么不同

業界不同領域使用LLVM的方式

一、作為編譯器使用

二、作為內存計算引擎使用

結論

參考資料

概述

本文主要從幾個方面來講解Llvm相關內容,Llvm是什么、傳統編譯器的設計、傳統編譯器的實現、Llvm的編譯器如何實現的、LLVM IR是什么、JIT簡單的實現原理、業界多領域是如何使用Llvm的等多個方面來描述LLVM。LLVM命名源自於底層虛擬機(Low Level Virtual Machine)的縮寫。不是一個類似於VMware這種虛擬機項目,是類似於GCC一樣的編譯器框架。說到編譯器框架就不得不提一提傳統的編譯器。

LLVM技術生態之編譯器

一、 傳統編譯器的設計

 

 圖1. 傳統的三段式設計

二、 傳統編譯器采用三段式設計:

前端: 前端組件解析程序源代碼,檢查語法錯誤,生成一個基於語言特性的AST(Abstract Syntax Tree)表示輸入代碼。

優化器:優化器組件接收到前端生成的AST,進行優化處理。

后端:把優化器優化后的AST,翻譯成機器能識別的語言。

二、傳統編譯器模式的實現

這種模式的優點在於當編譯器決定支持多種語言或者多種目標設備的時候,如果編譯器在優化器這里采用普通的代碼表示時,前端可以使用任意的語言來進行編譯,后端也可以使用任意的目標設備來匯編。如下圖:

 

 

 圖2. 傳統編譯器三段式實現

使用這種設計,使編譯器支持一種新的語言,需要實現一個新的前端,但是優化器及后端,都可以復用,不用改變。實現支持新的語言,需要從最初的前端設計開始,支持N種設備和M種源代碼語言,一共需要N*M種編譯方式。

這種三段式設計的另一優點是編譯器提供了一個非常寬泛的語法集,即對於開源編譯器項目來說,會有更多的人參與,自然就提升了項目的質量。這是為什么一些開源的編譯器通常更為流行。

最后一個優點是實現一個編譯器前端,相對於優化器與后端是完全不同。將分離開來對於專注於設計前端來提升編譯器的多用性(支持多種語言)來說相對容易點。

三、LLVM的三段式實現

在基於LLVM的編譯器中,前端的作用是解析、驗證和診斷代碼錯誤,將解析后的代碼翻譯為LLVM IR(通常是這么做,通過生成AST然后將AST轉為LLVM IR)。翻譯后的IR代碼經過一系列的優化過程與分析后,代碼得到改善,並將其送到代碼生成器去產生原生的機器碼。過程如下圖所示。這是非常直觀的三段式設計的實現過程,但是這簡單的描述當然是省去了一些細節的實現。

 

圖3. LLVM三段式實現

四、LLVM's Code Representation:LLVM IR

1、IR的表現形式

LLVM中最重要的設計模塊:LLVM IR(LLVM Intermediate Representation),在編譯器中表示代碼的一種形式。設計在編譯器的優化模塊中,作為主導中間層的分析與轉換。經過特殊設計,包括支持輕量級的Runtime優化、過程函數的優化,整個程序的分析和代碼完全重構和翻譯等。其中最重要的,定義了清晰的語義。參考如下的.ll文件:

define i32 @add1(i32 %a, i32 %b) {

entry:

  %tmp1 = add i32 %a, %b

  ret i32 %tmp1

}

define i32 @add2(i32 %a, i32 %b) {

entry:

  %tmp1 = icmp eq i32 %a, 0

  br i1 %tmp1, label %done, label %recurse

recurse:

  %tmp2 = sub i32 %a, 1

  %tmp3 = add i32 %b, 1

  %tmp4 = call i32 @add2(i32 %tmp2, i32 %tmp3)

  ret i32 %tmp4

done:

  ret i32 %b

}

上述這段代碼對應的是下面這段C代碼,提供兩種方式返回一個整型變量:

 

unsigned int add1(unsigned int a, unsigned int b) {

  return a+b;

}

// Perhaps not the most efficient way to add two numbers.

unsigned int add2(unsigned int a, unsigned int b) {

  if (a == 0) return b;

  return add2(a-1, b+1);

}

從這個例子中可以看出,LLVM IR 是一種底層的類RISC虛擬指令集。正如真正的RISC指令集一樣,提供了一系列線性的簡單指令:加、減、比較以及分支結構。這些指令在三種地址形式中:即通過對一些輸入的計算,得出的結果存在不同的寄存器中。LLVM IR提供了標簽支持,通常看起來像是一種奇怪的匯編語言一樣。

和大多數RISC指令集不同的是,LLVM使用一種簡單的類型系統來標記強類型(i32表示32位整型,i32**表示指向32位整型的指針),而一些機器層面的細節都被抽象出去了。例如函數調用使用call作標記,而返回使用ret標記。此外還有個不同是LLVM IR不直接像匯編語言那樣直接使用寄存器,使用無限的臨時存儲單元,使用%符號來標記這些臨時存儲單元。

2、IR的格式文件類型

形式實現一個階乘,再在main函數中調用中這個階乘:

// factorial.c

int factorial(int n) {

    if(n>=1) {

        return n * factorial(n-1);

    }

    return 1;

}

// main.cpp

extern "C" int factorial(int);

int main(int argc, char** argv) {

    return factorial(2) * 7 == 42;

}

注:這里的extern "C"是必要的,為了支持C++的函數重載和作用域的可見性的規則,編譯器會對函數進行name mangling, 如果不加extern "C",下文中生成的main.ll文件中factorial的函數名會被mangling成類似_Z9factoriali的樣子,鏈接器便找不到要鏈接的函數。

LLVM IR有兩種等價的格式:

一種是.bc(Bitcode)文件

一種是.ll文件,.ll文件是Human-readable的格式。

可以使用下面的命令得到這兩種格式的IR文件

$ clang -S -emit-llvm factorial.c # factorial.ll

$ clang -c -emit-llvm factorial.c # factorial.bc

當然LLVM也提供了將代碼文本轉為二進制文件格式的工具:llvm-as,將.ll文件轉為.bc格式文件,llvm-dis將.bc文件轉為.ll文件。

 

 

$ llvm-as factorial.ll # factorial.bc

$ llvm-dis factorial.bc # factorial.ll

 對於cpp文件,只需將clang命令換成clang++即可。

$ clang++ -S -emit-llvm main.cpp # main.ll

$ clang++ -c -emit-llvm main.cpp # main.bc

3、IR文件的編譯處理流程

llvm編譯代碼pipeline, 利用不同高級語言對應的前端(這里C/C++的前端都是clang)將transform成LLVM IR,進行優化,鏈接后,傳給不同target的后端transform成target-specific的二進制代碼。IR是LLVM的power所在,看下面這條command:

llvm-link factorial.bc main.bc -o linked.bc # lined.bc

llvm-link將兩個IR文件鏈接起來,factorial.bc是C轉成的IR,main.bc是C++轉成的IR,到了IR這個level,高級語言間的差異消失了!可以相互鏈接。

進一步可以將鏈接得到的IR,轉成target相關的code。

llc --march=x86-64 linked.bc # linked.s

下圖展示了完整的build過程

 

 

 4、簡單的IR布局

 

 4.1 Target information

以linked.ll為例進行解析,文件的開頭是

; ModuleID = 'linked.bc'

source_filename = "llvm-link"

target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"

target triple = "x86_64-unknown-linux-gnu"

;后面的注釋指明了module的標識,source_filename是表明這個module是從什么文件編譯得到的(如果打開main.ll會發現這里的值是main.cpp),如果該modules是通過鏈接得到的,這里的值就會是llvm-link。

Target information的主要結構如下:

 

 

 4.2 函數定義的主要結構

看一下函數factorial的定義

; Function Attrs: noinline nounwind optnone ssp uwtable

define i32 @factorial(i32 %val) #0 {

  %2 = alloca i32, align 4

  %3 = alloca i32, align 4

  store i32 %0, i32* %3, align 4

  %4 = load i32, i32* %3, align 4

  %5 = icmp sge i32 %4, 1

  br i1 %5, label %6, label %12

; <label>:6:                                      

; preds = %1

  %7 = load i32, i32* %3, align 4

  %8 = load i32, i32* %3, align 4

  %9 = sub nsw i32 %8, 1

  %10 = call i32 @factorial(i32 %9)

  %11 = mul nsw i32 %7, %10

  store i32 %11, i32* %2, align 4

  br label %13

; <label>:12:                                    

; preds = %1

  store i32 1, i32* %2, align 4

  br label %13

; <label>:13:                                    

; preds = %12, %6

  %14 = load i32, i32* %2, align 4

  ret i32 %14

}

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

​!llvm.module.flags = !{!0, !1, !2}

!llvm.ident = !{!3}

!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 10, i32 14]}

!1 = !{i32 1, !"wchar_size", i32 4}

!2 = !{i32 7, !"PIC Level", i32 2}

!3 = !{!"Apple LLVM version 10.0.1 (clang-1001.0.46.4)"}

前面已經提到,;表示單行注釋的開始。define i32 @factorial(i32 %val) #0指出了該函數的attribute group,其中第一個i32是返回值類型,對應C語言中的int;%factorial是函數名;第二個i32是形參類型,%val是形參名。llvm中的標識符分為兩種類型:全局的和局部的。全局的標識符包括函數名和全局變量,會加一個@前綴,局部的標識符會加一個%前綴。一般地,可用標識符對應的正則表達式為[%@][-a-zA-Z$._][-a-zA-Z$._0-9]*。

#0指出了該函數的attribute group。在文件的下面,會找到類似這樣的代碼

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

因為attribute group可能很包含很多attribute且復用到多個函數,所以IR使用attribute group ID(即#0)的形式指明函數的attribute,這樣既簡潔又清晰。

在一對花括號里的就是函數體,函數體是由一系列basic blocks(BB)組成的,這些BB形成了函數的控制流圖(Control Flow Graph, CFG)。每個BB都有一個label,label使得該BB有一個符號表的入口點,在函數factorial中,這些BB的label就是entry、if.then、if.end,BB總是以terminator instruction(e.g. ret、br、callbr)結尾的。

4.3. IR是一個強類型語言

看一下函數main的定義

; Function Attrs: noinline norecurse optnone ssp uwtable

define i32 @main(i32, i8**) #0 {

  %3 = alloca i32, align 4

  %4 = alloca i32, align 4

  %5 = alloca i8**, align 8

  store i32 0, i32* %3, align 4

  store i32 %0, i32* %4, align 4

  store i8** %1, i8*** %5, align 8

  %6 = call i32 @factorial(i32 2)

  %7 = mul nsw i32 %6, 7

  %8 = icmp eq i32 %7, 42

  %9 = zext i1 %8 to i32

  ret i32 %9

}

LLVM的IR是一個強類型語言,每一條指令都顯式地指出了實參的類型,例如mul nsw i32 %6, 7表明要將兩個i32的數值相乘,icmp eq i32 %7, 42, icmp eq 表明要將兩個i32的數據類型進行相等比較。此外,還很容易推斷出返回值的類型,比如i32的數相乘的返回值就是i32類型,比較兩個數值的相等關系的返回值就是i1類型。

強類型不但使得IR很human readable,也使得在優化IR時不需要考慮隱式類型轉換的影響。在main函數的結尾,zext i1 %8 to i32將%8從1位整數擴展成了32位的整數(即做了一個類型提升)。

如果把最后兩行用以下代碼替代

ret i32 %8

這段IR就變成illegal的,檢查IR是否合法可以使用opt -verify <filename>命令

$ opt -verify linked.ll

opt: linked.ll:45:11: error: '%8' defined with type 'i1' but expected 'i32'

  ret i32 %8

 4.3 terminator instruction介紹

ret指令

語法

ret <type> <value>       ; Return a value from a non-void function

ret void                 ; Return from void function

概述

ret用來將控制流從callee返回給caller

Example

ret i32 5                       ; Return an integer value of 5

ret void                        ; Return from a void function

ret { i32, i8 } { i32 4, i8 2 } ; Return a struct of values 4 and 2

br指令

語法

br i1 <cond>, label <iftrue>, label <iffalse>

br label <dest>         

; Unconditional branch

​概述

br用來將控制流轉交給當前函數中的另一個BB。

​Example

Test:

  %cond = icmp eq i32 %a, %b

  br i1 %cond, label %IfEqual, label %IfUnequal

IfEqual:

  ret i32 1

IfUnequal:

  ret i32 0

switch指令

​語法

switch <intty> <value>, label <defaultdest> [ <intty> <val>, label <dest> ... ]

​概述

switch根據一個整型變量的值,將控制流交給不同的BB。

Example

; Emulate a conditional br instruction

%Val = zext i1 %value to i32

switch i32 %Val, label %truedest [ i32 0, label %falsedest ]

; Emulate an unconditional br instruction

switch i32 0, label %dest [ ]

; Implement a jump table:

switch i32 %val, label %otherwise [ i32 0, label %onzero

                                    i32 1, label %onone

                                    i32 2, label %ontwo ]

 4.4 unreachable 介紹

語法

unreachable

​概述

unreachable告訴optimizer控制流時到不了這塊代碼,就是說這塊代碼是dead code。

​Example

在展示unreachable的用法的之前,先看一下undef的用法。undef表示一個未定義的值,只要是常量可以出現的位置,都可以使用undef。(此Example標題下的代碼為偽代碼)

​%A = or %X, undef

%B = and %X, undef

or指令和and指令分別是執行按位或和按位與的操作,由於undef的值是未定義的,因此編譯器可以隨意假設值來對代碼進行優化,譬如說假設undef的值都是0

​%A = %X

%B = 0

可以假設undef的值是-1

​%A = -1

%B = %X

也可以假設undef的兩處值是不同的,譬如第一處是0,第二處是-1

​%A = -1

%B = 0

為什么undef的值可以不同呢?這是因為undef對應的值是沒有確定的生存期的,當需要一個undef的值的時候,編譯器會從可用的寄存器中隨意取一個值拿過來,因此並不能保證其值隨時間變化具有一致性。下面可以看unreachable的例子了

​%A = sdiv undef, %X

%B = sdiv %X, undef

sdiv指令是用來進行整數/向量的除法運算的,編譯器可以假設undef的值是0,因為一個數除以0是未定義行為,因此編譯器可以認為其是dead code,將其優化成

​%A = 0

unreachable

4.5 靜態單一賦值介紹

在IR中,每個變量都在使用前都必須先定義,且每個變量只能被賦值一次(如果套用C++的術語,就是說每個變量只能被初始化,不能被賦值),所以稱IR是靜態單一賦值的。舉個例子的,假如想返回a*b+c的值,覺得可能可以這么寫

%0 = mul i32 %a, %b

%0 = add i32 %0, %c

ret i32 %0

但是這里%0被賦值了兩次,是不合法的,需要把修改成這樣

%0 = mul i32 %a, %b

%1 = add i32 %0, %c

ret i32 %1

4.6 SSA

SSA可以簡化編譯器的優化過程,譬如說,考慮這段代碼

d1: y := 1

d2: y := 2

d3: x := y

很容易可以看出第一次對y賦值是不必要的,在對x賦值時使用的y的值時第二次賦值的結果,但是編譯器必須要經過一個定義可達性(Reaching definition)分析才能做出判斷。編譯器是怎么分析呢?首先先介紹幾個概念:

變量x的定義是指一個會給x賦值或可能給x賦值的語句,譬如d1就是對y的一個定義

當一個變量x有新的定義后 ,舊的的定義會被新的定義kill掉,譬如d2就kill掉了d1。

一個定義d到達點p是指存在一條d到p路徑,在這條路徑上,d沒有被kill掉

t1是t2的reaching definition是指存在一條t1到t2路徑,沿着這條路徑走就可以得到t1要賦值的變量的值,而不需要額外的信息。

按照上面的代碼寫法,編譯器是很難判斷d3的reaching definition的。因為d3的reaching definition可能是d1,也可能是d2,要搞清楚d1和d2誰kill了誰很麻煩。但是,如果代碼是SSA的,則代碼就會長成這樣

d1: y1 := 1

d2: y2 := 2

d3: x := y2

編譯發現x是由y2賦值得到,而y2被賦值了2,且x和y2都只能被賦值一次,顯然得到x的值的路徑就是唯一確定的,d2就是d3的reaching definition。

SSA帶來的問題

假設想用IR寫一個用循環實現的factorial函數

int factorial(int val) {

  int temp = 1;

  for (int i = 2; i &lt;= val; ++i)

  temp *= i;

  return temp;

}

5、Llvm IR 編程

使用LLVM IR編程要涉及到Module, Function, BasicBlock, Instruction, ExecutionEngine等概念。下面對這些概念進行一個簡單的說明。

 

 

 可以將LLVM中的Module類比為C程序中的源文件。一個C源文件中包含函數和全局變量定義、外部函數和外部函數聲明,一個Module中包含的內容也基本上如此,只不過C源文件中是源碼來表示,Module中是用IR來表示。

Function是LLVM JIT操作的基本單位。Function被Module所包含。LLVM的Function包含函數名、函數的返回值和參數類型。Function內部則包含BasicBlock。

BasicBlock與編譯技術中常見的基本塊(basic block)的概念是一致的。BasicBlock必須以跳轉指令結尾。

Instruction就是LLVM IR的最基本單位。Instruction被包含在BasicBlock中。

 

 

 ExecutionEngine是用來運行IR的。運行IR有兩種方式:解釋運行和JIT生成機器碼運行。相應的ExecutionEngine就有兩種:Interpreter和JIT。ExecutionEngine的類型可以在創建ExecutionEngine時指定。

LLVM IR編程基本流程

創建一個Module

在Module中添加Function

在Function中添加BasicBlock

在BasicBlock中添加指令

創建一個ExecutionEngine

使用ExecutionEngine來運行IR

LLVM IR編程示例與說明

創建Module

Module創建時需要一個context,通常使用global context。在例子中,Module的name被設置為test。

LLVMContext & context = llvm::getGlobalContext();

Module* module = new Module("test", context);

在Module中添加Function

在Module中添加Function的方法比較多,這里介紹一種比較簡潔的方法。下面的代碼生成了一個函數void foo(void)。

Constant* c = module->getOrInsertFunction("foo",

/*ret type*/                           Type::getVoidTy(context),

/*args*/                               Type::getVoidTy(context),

/*varargs terminated with null*/       NULL);

Function* foo = cast<Function>(c); /* cast is provided by LLVM

foo->setCallingConv(CallingConv::C);

到目前為止,還沒有添加BasicBlock,函數foo僅僅是一個函數原型。第6行設置foo遵循C函數調用的規則。LLVM中的函數支持多種調用規則,通常使用C的調用規則即可。更多調用規則可以參考llvm::CallingConv::ID。

在Function中添加BasicBlock

創建BasicBlock可以使用BasicBlock類的靜態函數Create。

BasicBlock* block = BasicBlock::Create(context, "entry", foo);

第三個參數foo表示將block插入到Function foo中。

在BasicBlock中添加指令

下面介紹一個在BasicBlock中添加指令的簡潔方法。這個方法使用了一個工廠類IRBuilder的實例builder。

首先,初始化builder。

IRBuilder<> builder(block);

這里將block作為參數的指令,插入到block中。

接下來的一段代碼開始向block中插入代碼。含義包含在注釋中。

//Create three constant integer x, y, z.

Value *x = ConstantInt::get(Type::getInt32Ty(context), 3);

Value *y = ConstantInt::get(Type::getInt32Ty(context), 2);

Value *z = ConstantInt::get(Type::getInt32Ty(context), 1);

//addr = &value

/* we will check the value of 'value' and see

** whether the function we construct is running correctly.

*/

long value = 10;

Value * addr = builder.CreateIntToPtr(

    ConstantInt::get(Type::getInt64Ty(context), (uint64_t)&value),

    Type::getInt64PtrTy(context),

    "addr"

);

// mem = [addr]

Value* mem = builder.CreateLoad(addr, "mem");

// tmp = 3*mem

Value* tmp = builder.CreateBinOp(Instruction::Mul,

                                 x, mem, "tmp");

// tmp2 = tmp+2

Value* tmp2 = builder.CreateBinOp(Instruction::Add,

                                  tmp, y, "tmp2");

// tmp3 = tmp2-1

Value* tmp3 = builder.CreateBinOp(Instruction::Sub,

                                  tmp2, z, "tmp3");

// [addr] = mem

builder.CreateStore(tmp3, addr);

// ret

builder.CreateRetVoid();

 

通過LLVM的IR生成一個Module test,這個Module中包含一個Function foo,而foo中包含一個BasicBlock entry。

展示已經生成的IR

可以使用Module的dump方法先展示目前的成果。

module->dump();

輸出結果

; ModuleID = 'test'

define void @foo(void) {

entry:

  ; the number '140735314124408' maybe different on your machine.

  %mem = load i64* inttoptr (i64 140735314124408 to i64*)

  %tmp = mul i32 3, i64 %mem

  %tmp2 = add i32 %tmp, 2

  %tmp3 = sub i32 %tmp2, 1

; the number '140735314124408' maybe different on your machine.

  store i32 %tmp3, i64* inttoptr (i64 140735314124408 to i64*)

  ret void

}

創建ExecutionEngine

接下來就要使用ExecutionEngine來生成代碼了。

創建一個JIT類型的ExecutionEngine,為了便於觀察IR生成的機器碼,設置為不優化。

InitializeNativeTarget();

    ExecutionEngine *ee = EngineBuilder(module).setEngineKind(EngineKind::JIT)

        .setOptLevel(CodeGenOpt::None).create();

生成機器指令。

JIT生成機器指令以Function為單位。

void * fooAddr = ee->getPointerToFunction(foo);

std::cout <<"address of function 'foo': " << std::hex << fooAddr << std::endl;

如果用gdb跟蹤函數執行,待輸出fooAddr后,用x/i命令,可查看foo對應的機器指令。

例如,X86_64機器上輸出為:

0x7ffff7f6d010:  movabs $0x7fffffffe2b0,%rax

0x7ffff7f6d01a:  mov    $0x3,%ecx

0x7ffff7f6d01f:  mov    (%rax),%edx

0x7ffff7f6d021:  imul   %ecx,%edx

0x7ffff7f6d024:  add    $0x2,%edx

0x7ffff7f6d02a:  sub    $0x1,%edx

0x7ffff7f6d030:  mov    %edx,(%rax)

0x7ffff7f6d032:  retq

運行機器指令

使用類型轉換將fooAddr轉換成一個函數fooFunc,然后調用。

//Run the function

std::cout << std::dec << "Before calling foo: value = " << value <<  std::endl;

typedef  (*FuncType)(void);

FuncType fooFunc = (FuncType)fooAddr;

fooFunc();

std::cout << "After calling foo: value = " << value <<  std::endl;

使用value的值來檢驗foo構造的正確性。運行后的輸出

Before calling foo: value = 10

After calling foo: value = 31

經過驗算,foo的功能是正確的。

直接生成並運行機器指令

ExecutionEngine還提供一個接口runFunction直接JIT並運行機器指令。具體做法可以參考LLVM::ExecutionEngine::runFunction的文檔。

五、LLVM 與 GCC有什么區別

有一種說法,gcc編譯器的代碼,很難復用到其它項目中。gcc和基於LLVM實現的編譯器其實都是分為前端、優化器、后端等模塊,為什么gcc就不能被復用呢?

這就是LLVM設計的精髓所在:完全模塊化。就拿優化器來說,典型的優化類型(LLVM優化器中稱為Pass)有代碼重排(expression reassociation)、函數內聯(inliner)、循環不變量外移( loop invariant code motion)等。在gcc的優化器中,這些優化類型是全部實現在一起形成一個整體,要么不用,要么都用;或者可以通過配置只使用其中一些優化類型。而LLVM的實現方式是,每個優化類型自己獨立成為一個模塊,而且每個模塊之間盡可能的獨立,這樣就可以根據需要只選擇需要的優化類型編譯進入程序中而不是把整個優化器都編譯進去。

LLVM實現的方法是用一個類來表示一個優化類型,所有優化類型都直接或者間接繼承自一個叫做Pass的基類,並且大多數都是自己占用一個.cpp文件,並且位於一個匿名命名空間中,這樣別的.cpp文件中的類便不能直接訪問,只提通過一個函數獲取到實例,這樣pass之間就不會存在耦合,如下面代碼所示:

namespace {

  class Hello : public FunctionPass {

  public:

    // Print out the names of functions in the LLVM IR being optimized.

    virtual bool runOnFunction(Function &F) {

      cerr << "Hello: " << F.getName() << "\n";

      return false;

    }

  };

}

​FunctionPass *createHelloPass() { return new Hello(); }

每個.cpp會被編譯成一個目標文件.o文件,然后被打包進入一個靜態鏈接庫.a文件中。當第三方又需要使用到其中一些優化類型,只需要選擇自己需要的。由於這些類型都是自己獨立於.a的一個.o中,因此的只有真正被用到的.o會被鏈接進入目標程序,這就實現了“用多少取多少”的目標,不搞“搭售”。而第三方如果還有自己獨特的優化要求,只要按照同樣的方法實現一個優化即可。

打個比方,如果將優化器比作賣電腦的,那么gcc的優化器相當於賣筆記本,稱為A;而LLVM的優化器相當於賣組裝的台式機的,稱為B。或許自己有了其它合適的部件,就差一顆強勁的CPU。去A店里要么不買,要么就買一個功能齊全的筆記本,A店不允許只買某台筆記本上的一顆芯片;而去B店里可以做到只買一顆芯片。

到這里,終於可以回答LLVM和gcc的區別了:

LLVM本身只是一堆庫,提供的是一種機制(mechanism),一種可以將源代碼編譯的機制,但是本身不能編譯任何代碼。也就是說編譯什么代碼、怎么編譯、怎么優化、怎么生成這些策略(strategy)是由用戶自己定的。例如clang就使用LLVM提供的這些機制制定了編譯C代碼的策略,因此前文中說clang可以稱之為驅動(driver)。還拿電腦做例子:一堆電腦零件本身並不能做任何事情,這么將組裝起來讓工作是使用者的事兒。

 

 

參考資料

A Brief History of Just-In-Time

IBM:深入淺出 JIT 編譯器

<<Recursive functions of symbolic expressions and their computation by machine>>

基於LLVM的內存計算

llvm.org/docs

 

 

參考鏈接:

https://blog.csdn.net/Night_ZW/article/details/108338628


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM