【编译器】-LLVMIR

news2025/2/22 9:07:43

概述

LLVM 是一种基于静态单赋值 (SSA) 的表示形式,提供类型安全、低级操作、灵活性以及干净地表示“所有”高级语言的能力。

  • LLVM IR 是一门低级语言,语法类似于汇编
  • 任何高级编程语言(如C++)都可以用LLVM IR表示
  • 基于LLVM IR可以很方便地进行代码优化
    LLVM IR(Intermediate Representation,中间表示)有三种主要的表现形式,这些形式允许编译器在不同的阶段以适合的方式操作和处理代码。这三种表现形式是:
  1. 文本形式(Textual Form)
  2. 二进制形式(Binary Form,又称 Bitcode)
  3. 内存中的数据结构(In-Memory Data Structures)
    文本形式是 LLVM IR 的人类可读版本,通常用作调试和分析工具。它使用类似汇编语言的语法,描述编译器生成的中间代码。可以通过 LLVM 工具(如 llvm-dis)将 LLVM Bitcode 转换为文本形式,或者通过 clang 编译器生成。但体积较大,不适合高效存储和传输。
  • LLVM IR 的文本形式包含全局变量、函数、指令、类型、元数据等内容。
  • 它具备结构化语法,与汇编语言类似,但具有高级语言特性,如面向对象、类型系统和 SSA(Static Single Assignment,静态单赋值)形式。
define i32 @sum(i32 %a, i32 %b) {   %1 = add i32 %a, %b   ret i32 %1 }

二进制形式 是 LLVM IR 的紧凑编码形式,称为 Bitcode。它是 LLVM IR 的序列化格式,设计为编译器内部和外部使用,以便高效存储、传输和重用。

  • Bitcode 是 LLVM 的高效二进制格式,结构紧凑,适合存储在磁盘上或在网络中传输。
  • Bitcode 是目标无关的,因此可以在不同的硬件平台上重新使用。
  • Bitcode 保留了完整的 LLVM IR 信息,可以反序列化为文本形式或直接用于生成机器代码。
    LLVM 编译器可以生成 Bitcode 文件,通常以 .bc 后缀保存。例如,可以通过 clang 命令生成 Bitcode 文件,可以使用 llvm-dis 将 Bitcode 转换回文本形式。
clang -emit-llvm -c input.c -o output.bc

llvm-dis output.bc -o output.ll

内存中的数据结构 是 LLVM IR 的第三种表现形式,通常用于编译器在内存中对中间表示进行操作和优化。

  • 在编译过程中的不同阶段,LLVM IR 会以内存中的数据结构形式存在,表示为 llvm::Module、llvm::Function、llvm::BasicBlock、llvm::Instruction 等对象。
  • 内存数据结构为编译器提供了操作 IR 的 API,允许编译器或工具链进行优化、分析、代码生成等操作。
llvm::LLVMContext Context; 
llvm::Module *Module = new llvm::Module("test", Context); 
llvm::IRBuilder<> Builder(Context);  
llvm::FunctionType *FuncType = llvm::FunctionType::get(Builder.getInt32Ty(), false); 
llvm::Function *Func = llvm::Function::Create(FuncType, llvm::Function::ExternalLinkage, "foo", Module); 

LLVM IR结构

  • 源代码被编译为LLVM IR后,具有以下结构:
    在这里插入图片描述

模块 Module

  • 一个源代码对应LLVM IR中的一个模块。
  • 头部信息包含程序的目标平台,如X86、ARM等,和一些其他信息。
  • 全局符号包含全局变量、函数的定义与声明。
    每个模块由函数、全局变量和符号表条目组成。模块可以与 LLVM 链接器组合在一起,它合并函数(和全局变量)定义、解析前向声明并合并符号表条目。一般来说,模块由全局值列表组成(其中函数和全局变量都是全局值)。全局值由指向内存位置的指针(在本例中为指向 char 数组的指针和指向函数的指针)表示
    target triple 是一个描述目标平台的字符串,通常由三个或四个部分组成,它标识了 LLVM 编译器生成的目标机器的体系结构、供应商、操作系统和可选的环境或 ABI(应用程序二进制接口)。
    ---
    target datalayout 字段描述了目标机器的数据布局规则。它告诉编译器在特定的硬件架构上如何排列数据、对齐内存、以及如何计算数据类型的大小等。
    target datalayout = “e-m:e-i64:64-f80:128-n8:16:32:64-S128”
    函数 Function
  • LLVM IR中的函数表示源代码中的某个函数。
  • 参数,顾名思义为函数的参数。
  • 一个函数由若干基本块组成,其中函数最先执行的基本块为入口块。
    基本块 BasicBlock
  • 一个基本块由若干个指令和标签组成。
  • 正常情况下,基本块的最后一条指令为跳转指令(br或者switch),或返回指令(retn),也叫作终结指令(Terminator Instruction)。
  • PHI指令是一种特殊的指令。
    LLVM IR语法
    LLVM 标识符有两种基本类型:全局标识符和本地标识符。全局标识符(函数、全局变量)以’@’ 字符开头。本地标识符(寄存器名称、类型)以 '%'字符开头。
    标识符有三种不同的格式:
  1. 命名值表示为带有前缀的字符串。例如,%foo,@DivisionByZero, %a.really.long.identifier。实际使用的正则表达式是“ [%@][-a-zA-Z . ] [ − a − z A − Z ._][-a-zA-Z .][azAZ._0-9]*”
  2. 未命名的值表示为带有前缀的无符号数值。例如,%12,@2,%44。
  3. 常量
    LLVM 程序中的字符串由字符分隔"。在字符串中,除\ 开始转义的字符和"结束字符串的第一个字符外,所有字节均按字面意思处理。
  • 要表示一个"字符,请使用\22. ("将以尾随 结束字符串\。)
  • 换行符不会终止字符串常量;字符串可以跨越多行。
  • 字符串常量的解释(例如它们的字符编码)取决于上下文。
    所有全局变量和函数都具有以下链接类型之一:
  • private:具有“ ”链接的全局值private只能由当前模块中的对象直接访问。
  • linkonce:当链接发生时,具有“ linkonce”链接的全局变量将与其他同名全局变量合并。
  • external:如果没有使用上述标识符,则全局是外部可见的,这意味着它参与链接并可用于解析外部符号引用。
    所有全局变量和函数都具有以下可见性样式之一:
    “ default” - 默认样式:在使用 ELF 对象文件格式的目标上,默认可见性意味着声明对其他模块可见,并且在共享库中意味着声明的实体可以被覆盖。
    “ hidden”——隐藏风格:如果具有隐藏可见性的对象位于同一共享对象中,则它们的两个声明将引用同一对象。
    “ protected” - 受保护的样式:在 ELF 上,受保护的可见性表示符号将放置在动态符号表中,但定义模块内的引用将绑定到本地符号。也就是说,该符号不能被另一个模块覆盖。

数据表示

第一类类型
第一类类型是可以被直接传递给函数、保存在内存中、从函数返回的类型。
整数类型:为所需的整数类型指定任意位宽度。可以指定从 1 位到 2 (23) (约 800 万)的任意位宽。
i1
一位整数。
i32
32 位整数。
i1942652
一个超过 100 万位的大整数。
浮点类型:
暂时无法在飞书文档外展示此内容
指针类型:ptr用于指定内存位置。指针通常用于引用内存中的对象。指针类型可以具有可选的地址空间属性,定义所指向的对象所在的编号地址空间。
向量类型:向量类型是表示元素向量的简单派生类型。向量类型需要大小(元素数量)、基础原始数据类型和可扩展属性来表示向量,其中确切的硬件向量长度在编译时未知。
标签类型:标签类型代表代码标签。
令牌类型:当值与指令关联时使用令牌类型。
元数据类型:表示嵌入的元数据。
结构类型:用于表示内存中数据成员的集合。结构的元素可以是具有大小的任何类型。
常量
简单常量:布尔常量、整数常量、浮点常量、空指针常量、令牌常量
复数常量:结构常量、数组常量、向量常量、元素常量
undef:字符串 ’ undef’ 可以用在需要常量的任何地方,并指示该值的用户可能会收到未指定的位模式。

数据区里的数据
在LLVM IR中定义一个存储在数据区中的全局变量,其格式为:

@global_variable = global i32 0

如果是只读的全局变量,也就是常量,我们可以用constant来代替global:

@global_constant = constant i32 0

在LLVM IR中,所有的全局变量的名称都需要用@开头。
链接类型
对于链接类型,常用的主要有什么都不加(默认为external)、private和internal。

用private,则代表这个变量的名字不会出现在符号表中。我们将原来的代码改写成

@global_variable = private global i32 0

用internal则表示这个变量是以局部符号的身份出现(全局变量的局部符号,可以理解成C中的static关键词)。我们将原来的代码改写成

@global_variable = internal global i32 0

寄存器内的数据和栈上的数据

全局变量和栈上变量皆指针
LLVM IR把它们都看作指针。也就是说,对于全局变量:

@global_variable = global i32 0

和栈上变量

%local_variable = alloca i32

寄存器
在LLVM IR中,一个函数的局部变量可以是寄存器或者栈上的变量。对于寄存器而言,我们只需要像普通的赋值语句一样操作,但需要注意名字必须以%开头:

%local_variable = add i32 1, 2


当不需要操作地址并且寄存器数量足够时,我们可以直接使用寄存器。而LLVM IR的策略保证了我们可以使用无数的虚拟寄存器。那么,在需要操作地址以及需要可变变量(之后会提到为什么)时,我们就需要使用栈。

%local_variable = alloca i32

SSA
LLVM IR是一个严格遵守SSA(Static Single Assignment)策略的语言。SSA的要求很简单:每个变量只被赋值一次。
把可变变量放到全局变量或者栈内变量里,虚拟寄存器只存储不可变的变量。比如说,我想实现上面的功能,把两次运算结果储存到同一个变量内:

%stack_variable = alloca i32
%1 = add i32 1, 2
store i32 %1, i32* %stack_variable
%2 = add i32 3, 4
store i32 %2, i32* %stack_variable

基本类型(Primitive Types)
LLVM IR中的基本类型是构建所有复杂类型的基础,它们直接对应硬件支持的原生数据类型。
a. 整数类型
iN: 表示一个N位的整数类型,其中N是整数位宽。LLVM支持任意位宽的整数(不局限于常见的8、16、32、64位),例如i1、i8、i16、i32、i64等。

%1 = add i32 %a, %b     ; 定义一个32位整数加法

i1: 表示布尔类型,i1通常用于条件判断和比较操作。i1的值为0(假)或1(真)。

%cond = icmp eq i32 %a, %b    ; 比较是否相等,结果为i1类型

b. 浮点数类型
half: 16位半精度浮点数。
float: 32位单精度浮点数。
double: 64位双精度浮点数。
fp128: 128位浮点数。

%1 = fadd float %a, %b     ; 定义一个32位浮点数加法 
%2 = fmul double %x, %y    ; 定义一个64位浮点数乘法

c. 其他基本类型
void: 表示无返回值的类型,通常用于函数的返回类型,表示该函数不返回任何值。
label: 表示跳转目标的标签类型,通常用于表示代码块的目标。
metadata: 元数据类型,用于附加到指令、全局变量和其他代码结构中的辅助信息,LLVM不对其执行。
x86_mmx: 表示x86架构中的MMX向量类型。
指针类型(Pointer Types)
指针类型是LLVM IR中用于引用内存地址的类型。所有指针在LLVM IR中都显示为类型化指针,即指向特定类型的指针。指针类型用*表示,表示指向类型的指针。
指针类型可以指向任意类型(例如整数、浮点数、结构体、数组等),并且可以进行算术和比较操作。

%ptr = alloca i32         ; 分配一个i32的指针 
%nullPtr = null i32*      ; 定义一个空指针

getelementptr 指令:用于计算指针的偏移量

%arrayElemPtr = getelementptr [10 x i32], [10 x i32]* %array, i32 0, i32 5 ; 获取数组中第6个元素的指针

数组类型(Array Types)
数组类型用于表示一组相同类型的元素。LLVM中的数组类型用[N x ]表示,其中N是数组的大小,是数组元素的类型。

  • 数组是固定大小的,大小必须在编译时已知。
  • 数组元素通过getelementptr指令访问,通过偏移计算来获得元素地址。
    @myArray = global [10 x i32] [i32 1, i32 2, i32 3, i32 4, i32 5, i32 6, i32 7, i32 8, i32 9, i32 10] ; 定义一个10元素的i32数组
    结构体类型(Structure Types)
    结构体类型是LLVM中的复合类型,表示一组不同类型的数据的组合。结构体类型用{, , …}的形式表示。
  • 结构体可以包含任意类型的元素,包括基本类型、数组类型、指针类型等。
  • 结构体的元素通过getelementptr指令来访问,类似于数组的元素访问。
    %myStruct = type { i32, float } ; 定义一个包含i32和float的结构体类型
    %structPtr = alloca %myStruct ; 在栈上分配结构体
    %fieldPtr = getelementptr %myStruct, %myStruct* %structPtr, i32 0, i32 1 ; 获取结构体的第二个字段
    函数类型(Function Types)
    函数类型描述了一个函数的返回类型和参数类型。函数类型用<return_type>(<arg_type1>, <arg_type2>, …)的形式表示。
  • LLVM IR中的函数是类型化的,每个函数都有明确的返回类型和参数类型。
  • 函数类型支持可变参数(vararg),类似于C语言中的可变参数函数。
  • 函数的返回类型可以是void,表示该函数不返回值。
    define i32 @myFunction(i32 %a, float %b) {
    ; 函数体
    ret i32 %a
    }

Label类型(Label Type)
label类型用于表示跳转目标的标签,主要用于分支跳转和循环控制结构中。label类型没有直接的值,而是作为跳转目标存在。

br label %label1    ; 跳转到label1标签
label1:             ; 定义标签label1

常用指令

LLVM IR指令系统由一组用于表示程序的操作的指令组成,这些指令被设计为抽象的、简洁的并适合多种架构。LLVM IR中的指令既可以表示常见的运算操作,又可以用于表示控制流、内存操作、类型转换、以及并行化和特殊用途的指令等。

  1. 算术指令(Arithmetic Instructions)
    算术指令用于基本的整数和浮点数运算。
    a. 整数算术指令
    add: 整数加法。
    %result = add i32 %a, %b ; 将a和b相加,返回i32类型结果

sub: 整数减法。
%result = sub i32 %a, %b ; 将a减去b,返回i32类型结果

mul: 整数乘法。
%result = mul i32 %a, %b ; 将a和b相乘,返回i32类型结果

udiv / sdiv: 无符号/有符号整数除法。
%result = udiv i32 %a, %b ; 无符号整数除法
%result = sdiv i32 %a, %b ; 有符号整数除法

urem / srem: 无符号/有符号整数取余。
%result = urem i32 %a, %b ; 无符号整数取余
%result = srem i32 %a, %b ; 有符号整数取余
b. 浮点数算术指令
fadd: 浮点数加法。
%result = fadd float %a, %b ; 将a和b相加,返回浮点数结果

fsub: 浮点数减法。
%result = fsub float %a, %b ; 将a减去b,返回浮点数结果

fmul: 浮点数乘法。
%result = fmul float %a, %b ; 将a和b相乘,返回浮点数结果

fdiv: 浮点数除法。
%result = fdiv float %a, %b ; 将a除以b,返回浮点数结果

frem: 浮点数取余。
%result = frem float %a, %b ; 浮点数取余操作
2. 位运算指令(Bitwise Instructions)
位运算指令用于执行位操作,主要用于整数类型。
and: 按位与。
%result = and i32 %a, %b ; 将a和b按位与

or: 按位或。
%result = or i32 %a, %b ; 将a和b按位或

xor: 按位异或。
%result = xor i32 %a, %b ; 将a和b按位异或

shl: 左移操作。
%result = shl i32 %a, %b ; 将a左移b位

lshr: 逻辑右移(无符号)。
%result = lshr i32 %a, %b ; 将a逻辑右移b位

ashr: 算术右移(有符号)。
%result = ashr i32 %a, %b ; 将a算术右移b位
3. 内存访问指令(Memory Access Instructions)
这些指令用于在内存中加载和存储数据。
alloca: 分配栈空间。
%ptr = alloca i32 ; 在栈上分配一个i32类型的空间

load: 从内存中加载数据。
%val = load i32, i32* %ptr ; 从指针%ptr指向的地址加载i32值

store: 将数据存储到内存中。
store i32 %val, i32* %ptr ; 将i32类型的值%val存储到指针%ptr指向的地址中

getelementptr (GEP):计算数组、结构体中的元素地址。
%elemPtr = getelementptr [10 x i32], [10 x i32]* %array, i32 0, i32 2
; 获取数组中第3个元素的指针
4. 控制流指令(Control Flow Instructions)
控制流指令用于程序的跳转、条件分支、函数调用等。
a. 跳转和分支
br: 条件或无条件分支跳转。
br label %target ; 无条件跳转到标签target
br i1 %cond, label %ifTrue, label %ifFalse ; 条件分支,若%cond为真则跳转到ifTrue,否则跳转到ifFalse

switch: 类似C语言中的switch语句,进行多分支选择。
switch i32 %val, label %default [i32 1, label %case1, i32 2, label %case2]

b. 函数调用
call: 调用一个函数。
%result = call i32 @myFunction(i32 %arg1, float %arg2)

ret: 返回函数结果。
ret i32 %result ; 返回i32类型的结果

c. 不正常终止
unreachable: 表示程序不可达位置,编译器假定不会到达该指令。
unreachable
5. 类型转换指令(Conversion Instructions)
类型转换指令用于在不同类型之间进行转换,确保类型安全。
trunc: 截断,将大类型缩减为小类型。
%small = trunc i32 %big to i8 ; 将32位整数截断为8位整数

zext / sext: 零扩展/符号扩展,将小类型扩展为大类型。
%big = zext i8 %small to i32 ; 将8位整数零扩展为32位

fptrunc / fpext: 浮点数截断/扩展。
%smallFloat = fptrunc double %bigFloat to float ; 截断双精度浮点数为单精度

bitcast: 位转换,不改变位模式,只改变解释方式。
%ptr = bitcast i32* %a to float* ; 将i32指针解释为float指针

addrspacecast: 在不同地址空间之间转换指针。
%ptr2 = addrspacecast i32 addrspace(1)* %ptr to i32 addrspace(2)*
6. 比较指令(Comparison Instructions)
这些指令用于对整数、浮点数进行比较,返回布尔类型的i1值。
a. 整数比较指令
icmp: 整数比较指令,支持多种条件(例如相等、大小关系等)。
%isEqual = icmp eq i32 %a, %b ; 判断a是否等于b
%isGreater = icmp sgt i32 %a, %b ; 判断a是否大于b(有符号比较)

b. 浮点数比较指令
fcmp: 浮点数比较指令。
%isEqual = fcmp oeq float %a, %b ; 判断a是否等于b
%isGreater = fcmp ogt float %a, %b ; 判断a是否大于b
7. 原子和同步指令(Atomic and Synchronization Instructions)
这些指令用于并发编程中的原子操作和同步控制。
atomicrmw: 原子读-修改-写操作。
%result = atomicrmw add i32* %ptr, i32 1 seq_cst
; 原子地将1加到%ptr指向的i32值中

cmpxchg: 比较并交换指令。
%old = cmpxchg i32* %ptr, i32 %oldVal, i32 %newVal seq_cst

fence: 用于保证内存访问顺序。
fence acquire
8. 其他指令
a. phi节点
用于在SSA(单静态赋值)形式中选择来自不同控制流路径的值。
%x = phi i32 [ 0, %entry ], [ %y, %loop ]

b. 内联汇编(Inline Assembly)
允许在LLVM IR中嵌入目标架构的汇编代码。
call void asm “mov $0, %eax”, “r”(i32 %val)

c. select
条件选择指令,根据布尔条件选择不同的值。
%result = select i1 %cond, i32 %a, i32 %b ; 如果%cond为true则选择a,否则

函数

在LLVM IR(中间表示)中,函数是程序的基本构建块。它们可以被视为一组指令的集合。LLVM IR中的函数不仅描述了函数的签名,还包括其内部的控制流、局部变量、参数传递和返回值的处理等。

  1. 函数定义(Function Definition)
    在LLVM IR中,函数的定义由其返回类型、函数名、参数类型、以及可选的函数属性组成。
    define <return_type> @function_name(<param_type1> %param1, <param_type2> %param2, …) {
    ; function body
    }
  2. 函数参数(Function Parameters)
    函数的参数在定义时声明,并在调用时传递。参数在函数体内部以局部变量的形式使用。
  • 参数可以是基本类型、结构体类型、指针类型等。
  • 参数的顺序和类型在函数调用时必须匹配。
  1. 返回值(Return Values)
    LLVM IR函数可以有一个返回值,使用ret指令来返回。返回值的类型必须与函数定义中的返回类型一致。
    LLVM IR不直接支持返回多个值,但可以通过结构体或元组等复合类型来实现
    %result = call {i32, i32} @function_returning_struct(i32 %a, i32 %b)
  2. 函数调用(Function Call)
    函数调用通过call指令完成,传递参数并接收返回值。
    %result = call i32 @add(i32 10, i32 20)
    LLVM IR支持可变参数函数,使用…表示参数列表的可变性。
    define i32 @sum(i32 %count, …) {
    ; 函数体
    }
  3. 函数属性(Function Attributes)
    LLVM 函数定义由“ define”关键字、可选的链接类型、可选的运行时抢占说明符、可选的可见性样式、可选的DLL 存储类、可选的调用约定、可选的unnamed_addr属性、返回类型、可选的 参数属性组成。
    define [linkage] [PreemptionSpecifier] [visibility] [DLLStorageClass]
    [cconv] [ret attrs]
    @ ([argument list])
    [(unnamed_addr|local_unnamed_addr)] [AddrSpace] [fn Attrs]
    [section “name”] [partition “name”] [comdat [($name)]] [align N]
    [gc] [prefix Constant] [prologue Constant] [personality Constant]
    (!name !N)* { … }
    LLVM 函数声明由“ declare”关键字、可选链接类型、可选可见性样式、可选DLL 存储类、可选调用约定、可选unnamed_addr orlocal_unnamed_addr属性、可选地址空间、返回类型、可选参数属性组成。

函数属性用于指定优化行为和调用约定,常见的属性包括:

  • nounwind: 指示函数不会抛出异常。
  • readonly: 函数不会修改任何全局状态。
  • writeonly: 函数不会读取任何参数。
  • tail: 尾调用优化指示,允许编译器进行尾调用优化。
    内联函数在调用时会被替换为函数体的内容,以减少函数调用的开销。LLVM IR支持将函数标记为内联。
    define void @inline_function() alwaysinline { ; 函数体 }
    LLVM IR支持控制函数的可见性,使用不同的链接属性来控制符号的暴露。
  • private: 仅在当前模块可见。
  • internal: 在模块内可见,但不能导出。
  • external: 其他模块可见,可以被调用。
    define internal i32 @internal_function() { ; 函数体 }

内部函数

LLVM IR 中的内部函数(Intrinsics)是提供特定功能的内建函数,通常用于对低级硬件操作的封装或为某些特定平台提供优化。内部函数不像普通函数,它们是编译器特有的,用于向LLVM传递某些特定的指令或操作提示。
这些内部函数以llvm.为前缀,通过函数调用语法使用,但实际上它们不会生成常规的函数调用,而是直接转化为底层机器指令或特定的编译优化操作。它们可以在代码优化、平台相关操作、特殊内存管理、向量化等方面发挥重要作用。
内部函数的定义与特性
LLVM 内部函数(intrinsics)与普通函数有以下不同特性:

  • 前缀:内部函数以llvm.为前缀,例如llvm.memcpy.p0i8.p0i8.i64。
  • 不生成普通调用指令:它们直接映射到底层指令,或在某些情况下成为编译时的优化提示。
  • 跨平台支持:很多内部函数具有跨平台能力,例如向量操作和内存管理。
  • 特定目的:用于硬件加速、内存访问控制、并行计算支持等特殊功能。

内存操作相关的内部函数
这些内部函数用于执行各种内存相关的操作,如复制、设置、加载和存储。

llvm.memcpy.*: 复制内存区域。
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %dest, i8* %src, i64 %size, i1 false)

llvm.memmove.*: 内存块重叠时的内存移动。
call void @llvm.memmove.p0i8.p0i8.i64(i8* %dest, i8* %src, i64 %size, i1 false)

llvm.memset.*: 将某个字节值填充到内存区域。
call void @llvm.memset.p0i8.i64(i8* %dest, i8 0, i64 %size, i1 false)

llvm.lifetime.start 和 llvm.lifetime.end: 用于标记内存的生命周期提示。
call void @llvm.lifetime.start.p0i8(i64 64, i8* %ptr)
call void @llvm.lifetime.end.p0i8(i64 64, i8* %ptr)

数学运算相关的内部函数
这些内部函数提供对硬件加速的数学操作支持,通常用于加速计算密集型应用。

llvm.sqrt.*: 计算平方根。
%result = call float @llvm.sqrt.f32(float %val)

llvm.pow.*: 计算幂。
%result = call double @llvm.pow.f64(double %base, double %exp)

llvm.sin.*, llvm.cos.*, llvm.log.*: 常用的三角函数、对数函数等数学操作。

位操作相关的内部函数
这些内部函数用于处理低级的位操作。
llvm.ctlz.*: 计算从高位开始的连续0的个数(Count Leading Zeros)。

%zeros = call i32 @llvm.ctlz.i32(i32 %val, i1 false)

llvm.cttz.*: 计算从低位开始的连续0的个数(Count Trailing Zeros)。
%zeros = call i32 @llvm.cttz.i32(i32 %val, i1 false)

llvm.bswap.*: 交换字节顺序(Byte Swap),常用于大端和小端之间的转换。
%swapped = call i32 @llvm.bswap.i32(i32 %val)

原子操作和同步相关的内部函数
这些内部函数提供对并发编程中原子操作和同步的支持。

llvm.atomicrmw.*: 原子读-修改-写操作。

%result = atomicrmw add i32* %ptr, i32 1 seq_cst

llvm.cmpxchg.*: 比较并交换(Compare-and-Swap)。
%old = cmpxchg i32* %ptr, i32 %oldVal, i32 %newVal seq_cst

llvm.fence.*: 内存屏障,用于保证多线程之间的内存访问顺序。
fence acquire

控制流相关的内部函数
这些函数用于控制LLVM IR中代码生成或优化过程中的控制流逻辑。
llvm.expect.*: 提示编译器某个布尔条件在运行时更可能为true或false,从而进行分支预测优化。
%likely = call i1 @llvm.expect.i1(i1 %condition, i1 true)
栈操作相关的内部函数
这些内部函数用于管理栈内存分配,特别是在内存分配不确定的场景中。

llvm.stacksave 和 llvm.stackrestore: 保存和恢复栈指针。
%stack_ptr = call i8* @llvm.stacksave()
call void @llvm.stackrestore(i8* %stack_ptr)

llvm.alloca: 动态分配栈空间,类似C语言中的alloca函数。
%ptr = alloca i32

向量化和SIMD(单指令多数据)相关的内部函数
LLVM提供了一系列的内部函数来支持向量化操作,特别是在处理并行数据时。

llvm.vector.reduce.add.*: 向量加法的归约操作。
%sum = call float @llvm.vector.reduce.add.v4f32(<4 x float> %vec)

llvm.vector.shuffle: 对向量中的元素进行洗牌操作。
%result = call <4 x i32> @llvm.vector.shuffle.v4i32(<4 x i32> %vec1, <4 x i32> %vec2, <4 x i32> %mask)

llvm.x86.sse2.*: 提供对x86 SSE2指令集的支持,如SIMD操作。
%result = call <2 x double> @llvm.x86.sse2.add.pd(<2 x double> %vec1, <2 x double> %vec2)

内部函数的生命周期

  1. LLVM IR 生成阶段
    在前端编译器(如 Clang)将高级语言代码(如 C/C++)转换为 LLVM IR 时,内部函数被插入到 LLVM IR 中。例如,当代码中涉及到特定的操作,如内存复制、数学计算或原子操作,编译器可能会插入相应的内部函数,如 llvm.memcpy、llvm.sqrt 或 llvm.atomicrmw。
    此时,内部函数仅仅以函数调用的形式存在于 LLVM IR 中,类似于其他普通函数调用。
  2. LLVM 中间优化阶段
    在 LLVM 的中间优化过程中,内部函数和其他 IR 指令一起被处理。LLVM 的优化器可能识别和优化这些内部函数。例如:
  • 常量传播:对于像 llvm.sqrt 这样的数学运算,如果操作数是常量,优化器可以在此阶段直接计算结果。
  • 内联优化:某些内部函数可能会被展开为更简单的 IR 指令,尤其是那些可以直接映射为标准的 LLVM IR 指令的内部函数。
    此阶段,内部函数仍然作为 LLVM IR 的一部分被表示。
  1. LLVM 后端生成阶段
    在代码生成阶段,后端根据目标硬件架构将 LLVM IR 转换为具体的机器代码。在这一阶段,内部函数的处理方式取决于目标平台的硬件特性:
  • 对于某些内部函数(如 llvm.sqrt、llvm.memcpy),后端会将它们映射为目标平台上的硬件指令,如 x86 上的 sqrtss 或 rep movsb 指令。
  • 对于一些复杂的内部函数,后端可能会使用一系列指令来实现其功能,或者展开为更底层的操作。
    在后端生成机器代码的过程中,内部函数会被转换或替换为具体的目标指令。至此,内部函数的生命周期结束。

元数据

元数据(Metadata) 是一种特殊的附加信息,不影响程序的功能执行,但能为编译器优化、调试和分析提供重要的辅助信息。元数据可以包含调试信息、代码优化提示、分析信息等。元数据不会在生成的目标代码中体现,也不会改变代码的行为。
元数据的基本结构使用 ! 符号表示。

  • 元数据分为 独立元数据 和 附加元数据。
    • 独立元数据:与指令或变量无直接关联的元数据。
    • 附加元数据:绑定在指令或全局变量上,为它们提供额外的信息。
      !0 = !{!“some information”} ; 定义一个元数据节点
      1、元数据的语法
      元数据的语法有多种形式,包括元数据节点、字符串、整型、浮点数等。常见的元数据语法如下:
      元数据节点:
      元数据可以表示为一个元数据节点,用花括号括起来,节点中的元素可以是常量、字符串、其他元数据节点等。
!0 = !{!"metadata example", i32 42, !1}
!1 = !{!"nested metadata", i1 true}

附加元数据:
元数据可以与指令关联,这样的元数据叫做附加元数据。它附加在 LLVM IR 的指令后,用元数据节点标记。
%result = add i32 %a, %b, !metadata_tag !0 ; 这个 add 指令带有元数据
在上面例子中,add 指令关联了元数据 !0,其中 !metadata_tag 是元数据的标签名。
元数据的类型
2、元数据的类型
LLVM IR 中的元数据主要有以下几种类型:
调试元数据(Debug Metadata)
调试元数据用于生成调试信息,通常包括源文件名、行号、变量类型等。这些信息不会影响程序的功能,但可以帮助调试工具(如 GDB 或 LLDB)还原源代码中的调试信息。
调试元数据包括以下类型:

  • DILocation:存储指令的源代码位置信息,如行号和文件名。
  • DISubprogram:描述函数的信息。
  • DIType:存储类型信息。
  • DIVariable:存储变量信息。
%1 = call i32 @some_function(), !dbg !12
!12 = !DILocation(line: 42, column: 13, scope: !13)
!13 = !DISubprogram(name: "some_function", ...)

这里的 !dbg 标记表示调用指令 %1 关联了调试信息,!12 描述了源代码中的行号、列号等。
优化提示元数据(Optimization Metadata)
优化提示元数据为编译器提供如何优化代码的提示,例如循环展开、向量化、分支预测等。
常见的优化元数据包括:

  • !llvm.loop: 提供循环优化的信息。
  • !llvm.mem.parallel_loop_access: 指示内存访问的并行性,提示编译器可以安全地对循环进行并行化。
    优化元数据示例:
br label %loop, !llvm.loop !10
!10 = distinct !{!10, !11}
!11 = !{!"llvm.loop.unroll.full"}

这个例子中的 !llvm.loop.unroll.full 表示编译器可以完全展开这个循环。
分析元数据(Analysis Metadata)
分析元数据用于静态分析工具,它提供了额外的信息,以便工具能更好地分析代码。例如,!range 元数据可以为值提供范围信息,帮助分析工具推断变量的取值范围。

%result = load i32, i32* %ptr, !range !20
!20 = !{i32 0, i32 100}  ; 表示 %result 的值在 0 到 100 之间

Type Metadata(类型元数据)
这类元数据描述的是类型信息,特别是在编译器优化过程中,元数据会为类型提供更多的上下文信息,帮助编译器作出优化决策。
TBAA Metadata(Type-Based Alias Analysis 元数据)
TBAA 元数据用于类型别名分析,帮助编译器了解不同内存位置是否可以别名。通过提供别名信息,编译器能够进行更激进的优化。
TBAA 元数据可以防止不必要的内存依赖检查,提升性能。
TBAA 元数据示例:

%0 = load i32, i32* %ptr, !tbaa !5
!5 = !{!"Simple C/C++ TBAA", !6, i64 0}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2303327.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java面试场景问题

还在补充&#xff0c;这几天工作忙&#xff0c;闲了会把答案附上去&#xff0c;也欢迎各位大佬评论区讨论 1.不用分布式锁如何防重复提交 方法 1&#xff1a;基于唯一请求 ID&#xff08;幂等 Token&#xff09; 思路&#xff1a;前端生成 一个唯一的 requestId&#xff08;…

python pandas下载

pandas pandas:就是一个可以处理数据的 python 库 核心功能&#xff1a; 数据的清洗&#xff1a;处理丢失值&#xff0c;重复值数据分析&#xff1a;计算和统计信息&#xff0c;或分组汇总数据可视化&#xff1a;结合 图标库&#xff08;Matplotlib&#xff09;完成数据可视化…

Python+Selenium+Pytest+POM自动化测试框架封装

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、测试框架简介 1&#xff09;测试框架的优点 代码复用率高&#xff0c;如果不使用框架的话&#xff0c;代码会显得很冗余。可以组装日志、报告、邮件等一些高…

猿大师中间件:网页直接内嵌本机EXE、OCX控件、ActiveX控件或桌面应用程序神器

猿大师中间件自从2019年发布以来&#xff0c;迄今为止不断迭代升级&#xff0c;给第三方提供了将自己的桌面程序和OCX控件支持直接内嵌到浏览器网页运行的赋能SDK开发包。 目前针对不同需求发布了三个成熟且商用的产品&#xff1a; 猿大师播放器&#xff1a;浏览器中直接原生…

C++,设计模式,【工厂方法模式】

文章目录 如何用汽车生产线理解工厂方法模式?一、传统生产方式的困境二、工厂方法模式解决方案三、模式应用场景四、模式优势分析五、现实应用启示✅C++,设计模式,【目录篇】 如何用汽车生产线理解工厂方法模式? 某个早晨,某车企CEO看着会议室里堆积如面的新车订单皱起眉…

鸿蒙-canvas-画时钟

文章目录 前言准备分析组成部分数值计算过程 开始第一步 画圆环第二步 画格子第三步 画数字第四、五步 画指针&定时更新最后一步 前言 你在 Android 上能画出来的东西&#xff0c;在鸿蒙上画不出来&#xff1f; 画个时钟嘛&#xff0c;有啥难的&#xff1f; 你行你上&…

【AI实践】阿里百炼文本对话Agent安卓版搭建

环境&#xff1a;安卓手机运行环境&#xff1b;WinsurfAI编程工具&#xff1b;阿里百炼提前创建Agent应用&#xff1b; 耗时&#xff1a;2小时&#xff1b; 1&#xff0c;新建安卓项目 完成文本输入&#xff0c;并将输入的文字显示出来。 2&#xff0c;安装SDK 参考文档 安…

算法很美笔记(Java)——动态规划

解重叠子问题&#xff08;当前解用到了以前求过的解&#xff09; 形式&#xff1a;记忆型递归或递推&#xff08;dp&#xff09; 动态规划本质是递推&#xff0c;核心是找到状态转移的方式&#xff0c;也就是填excel表时的逻辑&#xff08;填的方式&#xff09;&#xff0c;而…

Jest单元测试

由于格式和图片解析问题&#xff0c;可前往 阅读原文 前端自动化测试在提高代码质量、减少错误、提高团队协作和加速交付流程方面发挥着重要作用。它是现代软件开发中不可或缺的一部分&#xff0c;可以帮助开发团队构建可靠、高质量的应用程序 单元测试&#xff08;Unit Testi…

OnlyOffice:前端编辑器与后端API实现高效办公

OnlyOffice&#xff1a;前端编辑器与后端API实现高效办公 一、OnlyOffice概述二、前端编辑器&#xff1a;高效、灵活且易用1. 完善的编辑功能2. 实时协作支持3. 自动保存与版本管理4. 高度自定义的界面 三、后端API&#xff1a;管理文档、用户与权限1. 轻松集成与定制2. 实时协…

骶骨神经

骶骨肿瘤手术后遗症是什么_39健康网_癌症 [健康之路]匠心仁术&#xff08;七&#xff09; 勇闯禁区 骶骨肿瘤切除术

Nacos学习(二)——继承Feign与Config中心

目录 一、集成Feign (一)基础用法 1.添加openfeign依赖 2. 开启openFeign注解扫描 3.创建ProviderService接口 4.修改ConsumerController (二)OpenFeign日志配置 (三)参数传递 1.参数传递的问题 2.参数传递的方式 2.1URL路径传参 2.2URL上拼接参数 2.3body传参 …

计算机网络安全之一:网络安全概述

1.1 网络安全的内涵 随着计算机和网络技术的迅猛发展和广泛普及&#xff0c;越来越多的企业将经营的各种业务建立在Internet/Intranet环境中。于是&#xff0c;支持E-mail、文件共享、即时消息传送的消息和协作服务器成为当今商业社会中的极重要的IT基础设施。然而&#xff0…

uniapp中引入Vant Weapp的保姆级教学(包含错误处理)

废话不多说&#xff0c;直接上方法&#xff0c;网上的教学好多都是错误的 1.安装vant weapp 在Hbuilder的终端&#xff0c;输入以下代码 npm install vant/weapp -S --production 2.新建wxcomponents文件夹 在项目的跟目录新建一个“wxcomponents’文件夹&#xff0c;与app.…

基于Nanopi duo2的WiFi智能摄像头

1.固件包烧录 https://wiki.friendlyelec.com/wiki/index.php/NanoPi_Duo2/zh#.E8.BF.9E.E6.8E.A5WiFi 固件包链接以及烧录工具都在上面链接中 烧录过程 使用读卡器将SD卡插入到电脑,然后打开烧录工具 2.通过串口工具连接板子使其连接WiFi 对应的串口工具,就是这个HyperT…

Java 内存区域详解

1 常见面试题 1.1 基本问题 介绍下Java内存区域&#xff08;运行时数据区&#xff09;Java对象的创建过程&#xff08;五步&#xff0c;建议能够默写出来并且要知道每一步虚拟机做了什么&#xff09;对象的访问定位的两种方式&#xff08;句柄和直接指针两种方式&#xff09;…

MyBatis框架详解与核心配置解读

目录 前言 一、MyBatis框架概述 1.1 什么是MyBatis 1.2 MyBatis的优点 二、MyBatis的使用入门与案例 2.1 MyBatis核心配置文件&#xff08;mybatis-config.xml&#xff09; 2.2 XML映射文件&#xff08;UserMapper.xml&#xff09; 三、MyBatis的常用注解及其用法 3.1…

Windows 快速搭建C++开发环境,安装C++、CMake、QT、Visual Studio、Setup Factory

安装C 简介 Windows 版的 GCC 有三个选择&#xff1a; CygwinMinGWmingw-w64 Cygwin、MinGW 和 mingw-w64 都是在 Windows 操作系统上运行的工具集&#xff0c;用于在 Windows 环境下进行开发和编译。 Cygwin 是一个在 Windows 上运行的开源项目&#xff0c;旨在提供类Uni…

Unity Excel导表工具转Lua文件

思路介绍 借助EPPlus读取Excel文件中的配置数据&#xff0c;根据指定的不同类型的数据配置规则来解析成对应的代码文本&#xff0c;将解析出的字符串内容写入到XXX.lua.txt文件中即可 EPPlus常用API //命名空间 using OfficeOpenXml;//Excel文件路径 var fileExcel new File…

Helix——Figure 02发布通用人形机器人控制的VLA:一组神经网络权重下的快与慢双系统,让两个机器人协作干活

前言 过去一周&#xff0c;我花了很大的心思、力气&#xff0c;把deepseek的GRPO、MLA算法的代码解析通透&#xff0c;比如GRPO与PPO的详细对比&#xff0c;再比如MLA中&#xff0c;图片 公式 代码的一一对应 2.20日晚&#xff0c;无意中刷到figure 02发布Helix的一个演示视频…