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”。
通常,模块由全局值列表组成(函数和全局变量都是全局值)。全局值由指向内存位置的指针(指向字符数组的指针和指向函数的指针)表示。
命名元数据是元数据的集合。元数据节点(不是元数据字符串)是命名元数据的唯一有效算子。
- 命名元数据表示为带有元数据前缀的字符串。元数据名称的规则与标识符相同,不允许引用名称。"\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>' 是除标签和元数据外的任何类型。 示例
整数将占用的位数由N 值指定。 例示例
指针类型用于指定内存位置。指针通常用于引用内存中的对象。 指针类型可能有一个可选的地址空间属性,用于定义指向对象所在的编号地址空间。默认地址空间是数字零。非零地址空间的语义是特定目标的。 LLVM 不允许指向 void ( void*) 的指针,不允许指向标签 ( label*) 的指针。使用i8* 代替。
<类型> *
向量类型是表示元素向量的简单派生类型。当使用单个指令 (SIMD),并行操作多个原始数据时,将使用向量类型。向量类型需要大小(元素数量)和底层原始数据类型。向量类型被认为是一类的。 < <# elements> x <elementtype> > 元素个数是一个大于0的常量整数值;elementtype 可是任何整数,浮点数或指针类型。不允许大小为零的向量。
数组类型是一种非常简单的派生类型,在内存中按顺序排列元素。数组类型需要大小(元素数)和基础数据类型。
[<# elements> x <elementtype>] 元素的数量是一个常量整数值;elementtype可是具有大小的任何类型。
多维数组的一些示例:
对超出静态类型所隐含的数组末尾的索引,没有限制(在某些情况下对超出已分配对象范围的索引有限制)。在具有零长度数组类型的 LLVM 中,实现一维“可变大小数组”寻址。例如,LLVM 中“pascal 样式数组”的实现,可使用类型“{i32,[0 x float]}”。
结构类型用于表示内存中数据成员的集合。结构的元素可是具有大小的任何类型。 通过使用“ ”指令,获取指向字段的指针,可使用“ load”和“ store”访问内存中的结构getelementptr。使用“ extractvalue”和“ insertvalue”指令,访问寄存器中的结构。 结构可选择是“打包”结构,这表明结构的对齐是一个字节,元素之间没有填充。在非压缩结构中,字段类型之间的填充,按照模块中 DataLayout 字符串的定义插入,这是匹配底层代码生成器所期望的。 结构可是“文字”或“已知”。文字结构是与其它类型(如{i32,i32}*)内联定义的,标识的类型总是在顶层定义一个名称。文字类型因内容而唯一,永远不会是递归或不透明的,无法编写。已知类型可是递归的,可是不透明的,永远不会是唯一的。
% T1 = type { <类型 列表> } ; 已知的 正常 结构体 类型 % T2 = type < { <类型 列表> } > ; 已知的 压缩 结构 类型
不透明结构类型,用于表示没有指定主体的命名结构类型。对应于前向声明结构的 C 概念。
全局变量和函数的地址总是隐式有效(链接时)常量。这些常量在使用全局标识符时显式引用,始终具有指针类型。例如,以下是一个合法的 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