JIT Compiler编译器及指令集

JIT Compiler编译器及指令集

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 LLVM一些编程语法语义特性

LLVM 程序由Module's组成,每个 's 是输入程序的一个翻译单元。每个模块由函数,全局变量和符号表条目组成。模块可与 LLVM 链接器组合在一起,后者合并函数(全局变量)定义,解析前向声明,合并符号表条目。这是“hello world”模块的示例:

; Declare the string constant as a global constant.

@.str=private unnamed_addr constant [13 x i8] c"hello world\0A\00"

 

; External declaration of the puts function

declare i32 @puts(i8* nocapture) nounwind

 

; Definition of main function

define i32 @main() {   ; i32()*

  ; Convert [13 x i8]* to i8*...

  %cast210=getelementptr [13 x i8],[13 x i8]* @.str,i64 0,i64 0

 

  ; Call puts function to write out the string to stdout.

  call i32 @puts(i8* %cast210)

  ret i32 0

}

 

; Named metadata

!0=!{i32 42,null,!"string"}

!foo=!{!0}

这个示例是由全局变量命名为“ .str”,在一个外部声明“ puts”函数,函数定义为“main”,命名为元数据“ foo”。

通常,模块由全局值列表组成(函数和全局变量都是全局值)。全局值由指向内存位置的指针(指向字符数组的指针和指向函数的指针)表示。

命名元数据是元数据的集合。元数据节点(不是元数据字符串)是命名元数据的唯一有效算子。

  1. 命名元数据表示为带有元数据前缀的字符串。元数据名称的规则与标识符相同,不允许引用名称。"\xx"类型转义仍然有效,允许任何字符成为名称的一部分。

句法:

; Some unnamed metadata nodes,which are referenced by the named metadata.
!0=!{!"zero"}
!1=!{!"one"}
!2=!{!"two"}
; A named metadata.
!name=!{!0,!1,!2}

函数类型的返回类型和每个参数,可能有一组与之关联的参数属性。参数属性用于传达有关函数结果,或参数的附加信息。参数属性被认为是函数的一部分,不是函数类型,具有不同参数属性的函数,可具有相同的函数类型。

参数属性是遵循指定类型的简单关键字。如果需要多个参数属性,空格分隔。例如:

declare i32 @printf(i8* noalias nocapture,...)
declare i32 @atoi(i8 zeroext)
declare signext i8 @returns_signed_char()

前缀数据是与函数关联的数据,代码生成器将在函数入口点前面立即发出数据。允许前端将特定语言的运行时元数据与特定函数相关联,通过函数指针可用,允许调用函数指针。

要访问给定函数的数据,程序可将函数指针位转换为指向常量类型的指针,取消引用索引 -1。IR 符号刚好越过前缀数据的末尾。例如,一个用单i32个,注释的函数为例。

define void @f() prefix i32 123 { ... }

前缀数据可引用

%0=bitcast void* () @f to i32*
%a=getelementptr inbounds i32,i32* %0,i32 -1
%b=load i32,i32* %a
define void @f() prologue i8 144 { ... }

prologue属性允许在函数体前面,插入任意代码(编码为字节)。用于启用功能热修补和检测。

为了维护普通函数调用的语义,序言数据必须具有特定的格式。具体,必须以一个字节序列开始,这些字节序列解码为一系列机器指令,对模块的目标有效,这些指令将控制转移到序言数据后的点,不执行任何可见的操作。允许内联和通道推理函数定义的语义,无需推理序言数据。使得序言数据的格式高度依赖于目标。

x86 架构的有效序言数据的一个简单示例,对指令进行编码:i8 144nop

define void @f() prologue i8 144 { ... }

通常可通过编码,跳过元数据的相对分支指令,形成序言数据,如x86_64架构的有效序言数据示例,前两个字节编码:jmp .+10

%0=type <{ i8,i8,i8* }>
 
define void @f() prologue %0 <{ i8 235,i8 8,i8* @md}> { ... }

 

; Target-independent attributes:
attributes #0={ alwaysinline alignstack=4 }
 
; Target-dependent attributes:
attributes #1={ "no-sse" }
 
; Function @f has attributes: alwaysinline,alignstack=4,and "no-sse".
define void @f() #0 #1 { ... }

属性组是由 IR 内的对象引用的属性组。对于保持.ll文件可读很重要,许多函数将使用相同的属性集。在.ll文件对应于单个.c文件的退化情况下,单个属性组将捕获用于构建文件的重要命令行标志。

属性组是模块级对象。要使用属性组,对象引用属性组的 ID(如#37)。一个对象可能引用多个属性组。在这种情况下,不同组的属性将合并。

下面是一个函数的属性组示例,函数始终内联,堆栈对齐为 4,不应使用 SSE 指令:

define void @f() noinline { ... }
define void @f() alwaysinline { ... }
define void @f() alwaysinline optsize { ... }
define void @f() optsize { ... }

去优化算子包的特点是"deopt" 算子包标签。这些算子包代表了所连接的调用站点的替代“安全”延续,可由合适的运行时使用,取消优化指定调用站点的编译帧。最多可有一个"deopt"算子束,附加到调用站点。去优化的确切细节超出了语言参考的范围,通常涉及将编译帧重写为一组解释帧。

从编译器的角度,去优化算子包,所连接的调用点,至少连接到readonly。通读了所有指针类型的算子(即使没有以其它方式转义)和整个可见堆。去优化算子包不捕获算子,除非在去优化期间,在这种情况下控制,不会返回到编译帧。

内联器知道如何通过具有去优化算子包的调用进行内联。就像通过普通调用站点内联涉及组合正常和异常延续一样,通过具有去优化算子包的调用站点,内联需要适当地组合“安全”去优化延续。内联程序通过将父级的去优化延续,添加到内联正文中的每个去优化延续前面,做到这一点。例如内联@f到@g在下面的示例中

define void @f() {
  call void @x()  ;; no deopt state
  call void @y() ["deopt"(i32 10)]
  call void @y() ["deopt"(i32 10),"unknown"(i8* null)]
  ret void
}
 
define void @g() {
  call void @f() ["deopt"(i32 20)]
  ret void
}

导致

define void @g() {
  call void @x()  ;; still no deopt state
  call void @y() ["deopt"(i32 20,i32 10)]
  call void @y() ["deopt"(i32 20,i32 10),"unknown"(i8* null)]
  ret void
}

在每个需要 a 的规范上<abi>:<pref>,指定 <pref>对齐方式是可选的。如果省略,前面的省略,<pref>等于<abi>。

在为给定目标构建数据布局时,LLVM从一组默认规范开始,然后(可能)被datalayout关键字中的规范覆盖。此列表中给出了默认规格:

  • E - big endian
  • p:64:64:64 - 64-bit pointers with 64-bit alignment.
  • p[n]:64:64:64 - Other address spaces are assumed to be the same as the default address space.
  • S0 - natural stack alignment is unspecified
  • i1:8:8 - i1 is 8-bit (byte) aligned
  • i8:8:8 - i8 is 8-bit (byte) aligned
  • i16:16:16 - i16 is 16-bit aligned
  • i32:32:32 - i32 is 32-bit aligned
  • i64:32:64 - i64 has ABI alignment of 32-bits but preferred alignment of 64-bits
  • f16:16:16 - half is 16-bit aligned
  • f32:32:32 - float is 32-bit aligned
  • f64:64:64 - double is 64-bit aligned
  • f128:128:128 - quad is 128-bit aligned
  • v64:64:64 - 64-bit vector is 64-bit aligned
  • v128:128:128 - 128-bit vector is 128-bit aligned
  • a:0:64 - aggregates are 64-bit aligned

用列指令,对每个用列的内存顺序进行编码,允许重重排序。<order-indexes>是分配给引用值的使用的索引的逗号分隔列表。引用值的用列,实时按这些索引排序。

用列指令,可能出现在函数作用域,或全局作用域。不是指令,对 IR 的语义没有影响。当处于函数作用域时,必须出现在最终基本块的终止符后。

如果基本块的地址,通过blockaddress()表达式获取,uselistorder_bb从函数范围外重新排序用列。

Syntax:

 

uselistorder <ty> <value>,{ <order-indexes> }

uselistorder_bb @function,%block { <order-indexes> }

Examples:

 

define void @foo(i32 %arg1,i32 %arg2) {

entry:

  ; ... instructions ...

bb:

  ; ... instructions ...

 

  ; At function scope.

  uselistorder i32 %arg1,{ 1,0,2 }

  uselistorder label %bb,{ 1,0 }

}

 

; At global scope.

uselistorder i32* @global,{ 1,2,0 }

uselistorder i32 7,{ 1,0 }

uselistorder i32 (i32) @bar,{ 1,0 }

uselistorder_bb @foo,%bb,{ 5,1,3,2,0,4 }

概述:

 

函数类型可被认为是一个函数签名。由一个返回类型和一个形参类型列表组成。函数类型的返回类型是 void 类型,或第一类类型——标签元数据类型除外。

句法:

<returntype> (<parameter list>)

... '<parameter list> ' 是逗号分隔的类型说明符列表。参数列表可包括一个 type...,表明函数采用可变数量的参数。可变参数函数使用可变参数处理内部函数访问参数。'<returntype>' 是除标签元数据外的任何类型。

示例

i32 (i32)

函数取一个i32,返回一个i32

float (i16,i32 *) *

指针,接受一个函数i16和一个指针i32,返回float。

i32 (i8*,...)

可变参数函数,至少一个指针到i8(在C炭),返回一个整数。这是printfLLVM 中的签名。

{i32,i32} (i32)

一个函数采用i32,返回一个包含两个值的结构i32

整数将占用的位数由N 值指定。

例示例

i1

一位整数。

i32

一个 32 位整数。

i1942652

一个超过 100 万位的非常大的整数。

浮浮点类型

类型

描述

half

16 位浮点值

float

32 位浮点值

double

64 位浮点值

fp128

128 位浮点值(112 位尾数)

x86_fp80

80 位浮点值 (X87)

ppc_fp128

128 位浮点值(两个 64 位)

指指针类型

概述:

 

指针类型用于指定内存位置。指针通常用于引用内存中的对象。

指针类型可能有一个可选的地址空间属性,用于定义指向对象所在的编号地址空间。默认地址空间是数字零。非零地址空间的语义是特定目标的。

LLVM 不允许指向 void ( void*) 的指针,不允许指向标签 ( label*) 的指针。使用i8* 代替。

句法:

 

<类型>  *

示例

 

 

[4 x i32]*

指针阵列的4个i32值。

i32 (i32*) *

一个指向函数指针,接受一个i32*,返回一个i32。

i32 addrspace(5)*

指向i32位于地址空间#5值的指针

量类型

概述:

 

向量类型是表示元素向量的简单派生类型。当使用单个指令 (SIMD),并行操作多个原始数据时,将使用向量类型。向量类型需要大小(元素数量)和底层原始数据类型。向量类型被认为是一类的

< <# elements> x <elementtype> >

元素个数是一个大于0的常量整数值;elementtype 可是任何整数,浮点数或指针类型。不允许大小为零的向量。

示例:

 

 

<4 x i32>

4 个 32 位整数值的向量。

<8 x float>

8 个 32 位浮点值的向量。

<2 x i64>

2 个 64 位整数值的向量。

<4 x i64*>

4 个指向 64 位整数值的指针的向量。

数数组类型

概述:

 

数组类型是一种非常简单的派生类型,在内存中按顺序排列元素。数组类型需要大小(元素数)和基础数据类型。

句法:

 

 [<# elements> x <elementtype>]

元素的数量是一个常量整数值;elementtype可是具有大小的任何类型。

示例:

 

 

[40 x i32]

40 个 32 位整数值的数组。

[41 x i32]

41 个 32 位整数值的数组。

[4 x i8]

4 个 8 位整数值的数组。

多维数组的一些示例:

[3 x [4 x i32]]

32 位整数值的 3x4 数组。

[12 x [10 x float]]

12x10 单精度浮点值数组。

[2 x [3 x [4 x i16]]]

2x3x4 16 位整数值数组。

对超出静态类型所隐含的数组末尾的索引,没有限制(在某些情况下对超出已分配对象范围的索引有限制)。在具有零长度数组类型的 LLVM 中,实现一维“可变大小数组”寻址。例如,LLVM 中“pascal 样式数组”的实现,可使用类型“{i32,[0 x float]}”。

构类型

概述:

 

结构类型用于表示内存中数据成员的集合。结构的元素可是具有大小的任何类型。

通过使用“ ”指令,获取指向字段的指针,可使用“ load”和“ store”访问内存中的结构getelementptr。使用“ extractvalue”和“ insertvalue”指令,访问寄存器中的结构。

结构可选择是“打包”结构,这表明结构的对齐是一个字节,元素之间没有填充。在非压缩结构中,字段类型之间的填充,按照模块中 DataLayout 字符串的定义插入,这是匹配底层代码生成器所期望的。

结构可是“文字”或“已知”。文字结构是与其它类型(如{i32,i32}*)内联定义的,标识的类型总是在顶层定义一个名称。文字类型因内容而唯一,永远不会是递归或不透明的,无法编写。已知类型可是递归的,可是不透明的,永远不会是唯一的。

句法:

 

% T1 = type  {  <类型 列表>  }      ;  已知的 正常 结构体 类型
% T2 = type  < {  <类型 列表>  } >    ;  已知的 压缩 结构 类型

示例:

 

 

{ i32,i32,i32 }

三个i32值的三元组

{ float,i32 (i32) * }

第一个元素是 float,第二个元素是指向一个函数指针函数接受一个i32,返回一个i32。

<{ i8,i32 }>

已知大小为 5 个字节的打包结构。

不透明结构类型

概述:

 

不透明结构类型,用于表示没有指定主体的命名结构类型。对应于前向声明结构的 C 概念。

句法:

%X = type opaque

%52 = type opaque

 

 

示例:

 

 

opaque

不透明类型。

局变量和函数地址

全局变量函数的地址总是隐式有效(链接时)常量。这些常量在使用全局标识符时显式引用,始终具有指针类型。例如,以下是一个合法的 LLVM 文件:

@X=global i32 17
@Y=global i32 42
@Z=global [2 x i32*] [i32* @X,i32* @Y]

未定义值

字符串 ' undef' 可用于任何需要常量的地方,指示值的用户,可能会收到未指定的位模式。未定义的值可是任何类型(除了“ label”或“ void”),可在任何允许常量的地方使用。

未定义值很有用,向编译器表明,无论使用什么值,程序都是定义良好的。这给了编译器更多的优化自由。以下是一些有效(在伪 IR 中)转换的示例:

  %A=add %X,undef
  %B=sub %X,undef
  %C=xor %X,undef
Safe:
  %A=undef
  %B=undef
  %C=undef

这是安全的,所有输出位都受 undef 位影响。任何输出位都可有01,具体取决于输入位。

%A=or %X,undef
  %B=and %X,undef
Safe:
  %A=-1
  %B=0
Safe:
  %A=%X  ;; By choosing undef as 0
  %B=%X  ;; By choosing undef as -1
Unsafe:
  %A=undef
  %B=undef

这些逻辑运算的位,不总是受输入影响。例如,如果%X有一个零位,无论 ' undef'的相应位是什么,对于位'and ' 操作的输出,始终为零。优化或假设“ and”的结果 为“ undef”是不安全的。但是,可安全地假设 ' undef' 的所有位都为 0,将 ' and'优化为 0。同样,可安全地假设 ' undef' 算子的所有位设置为'or',从而将'or'设置为-1。

  %A=select undef,%X,%Y
  %B=select undef,42,%Y
  %C=select %X,%Y,undef
Safe:
  %A=%X     (or %Y)
  %B=42     (or %Y)
  %C=%Y
Unsafe:
  %A=undef
  %B=undef

  %C=undef这组示例表明,未定义的“ select”(和条件分支)条件,可采用任何一种方式,必须来自两个算子之一。在%A示例中,如果%X和%Y是两个已知具有明显的低位,%A就必须有一个清除低位。但是,在%C示例中,优化器可假设“ undef”算子,可与  %Y相同,从而select可消除整个“ ”。

  %A=xor undef,undef 
 
  %B=undef 
  %C=xor %B,%B 
 
  %D=undef 
  %E=icmp slt %D,4 
  %F=icmp gte %D,4 
 
Safe: 
  %A=undef 
  %B=undef 
  %C=undef 
  %D=undef 
  %E=undef 
  %F=undef

此示例指出两个“ undef”算子不一定相同。可能会让人们感到惊讶(匹配 C 语义),假设“ X^X”始终为零,即使 X未定义。出于多种原因,情况并非如此,但简短的回答是undef“变量”,可在 “有效范围”内,任意更改值。变量实际上没有有效范围。相反,值是在需要时,从恰好在邻近的任意寄存器逻辑读取的,值不一定随时间保持一致。事实上,%A与 %C需要具有相同的语义,或核心LLVM概念,不会执行“替换所有用途”。

  %A=fdiv undef,%X
  %B=fdiv %X,undef
Safe:
  %A=undef
b: unreachable

这些示例显示了未定义值和未定义行为间的关键区别。undef允许未定义的值(如“ ”)具有任意位模式。%A 操作可常量折叠为“ undef”,“ undef”可能是 SNaN,fdiv(当前)未在 SNaN 上定义。

在第二个示例中,可做出更激进的假设:undef允许是任意值,可假设可能为零。除以零具有未定义的行为,可假设操作根本不执行。删除除法后的所有代码。未定义的操作“不可能发生”,优化器可假设发生在死代码中。

a:  store undef -> %X
b:  store %X -> undef
Safe:
a: <deleted>
b: unreachable

险值

危险值类似于undef 值,代表了这样一个事实,即不能引起辅助的指令,或常量表达式,仍然检测到导致未定义行为的条件。

目前没有办法在 IR 中表示危险值;当通过操作,如产生只存在附加与nsw标记。

危险值与undef值具有相同的行为,附加效果是任何依赖危险值的指令,都具有未定义的行为。

示例:

 

entry:
  %poison=sub nuw i32 0,1           ; Results in a poison value.
  %still_poison=and i32 %poison,0   ; 0,but also poison.
  %poison_yet_again=getelementptr i32,i32* @h,i32 %still_poison
  store i32 0,i32* %poison_yet_again  ; memory at @h[0] is poisoned
 
  store i32 %poison,i32* @g           ; Poison value stored to memory.
  %poison2=load i32,i32* @g         ; Poison value loaded back from memory.
 
  store volatile i32 %poison,i32* @g  ; External observation; undefined behavior.
 
  %narrowaddr=bitcast i32* @g to i16*
  %wideaddr=bitcast i32* @g to i64*
  %poison3=load i16,i16* %narrowaddr ; Returns a poison value.
  %poison4=load i64,i64* %wideaddr  ; Returns a poison value.
 
  %cmp=icmp slt i32 %poison,0       ; Returns a poison value.
  br i1 %cmp,label %true,label %end  ; Branch to either destination.
 
true:
  store volatile i32 0,i32* @g        ; This is control-dependent on %cmp,so
                                       ; it has undefined behavior.
  br label %end
 
end:
  %p=phi i32 [0,%entry],[1,%true]
                                       ; Both edges into this PHI are
                                       ; control-dependent on %cmp,so this
                                       ; always results in a poison value.
 
  store volatile i32 0,i32* @g        ; This would depend on the store in %true
                                       ; if %cmp is true,or the store in %entry
                                       ; otherwise,so this is undefined behavior.
 
  br i1 %cmp,label %second_true,label %second_end
                                       ; The same branch again,but this time the
                                       ; true block doesn't have side effects.
 
second_true:
  ; No side effects!
  ret void
 
second_end:
  store volatile i32 0,i32* @g        ; This time,the instruction always depends
                                       ; on the store in %end. Also,it is
                                       ; control-equivalent to %end,so this is
                                       ; well-defined (ignoring earlier undefined
                                       ; behavior in this example).

 

 

参考链接:

https://releases.llvm.org/6.0.0/docs/LangRef.html#module-structure

 

posted @ 2021-11-03 06:24  吴建明wujianming  阅读(242)  评论(0编辑  收藏  举报