转载:【AI系统】LLVM IR 基本概念

news/2024/12/13 19:32:52/文章来源:https://www.cnblogs.com/rizhaojincheng/p/18605699

在上一篇文章中,我们已经简要介绍了 LLVM 的基本概念和架构,我们现在将更深入地研究 LLVM 的 IR(中间表示)的概念。

了解 LLVM IR 的重要性是为了能够更好地理解编译器的运作原理,以及在编译过程中 IR 是如何被使用的。LLVM IR 提供了一种抽象程度适中的表示形式,同时能够涵盖绝大多数源代码所包含的信息,这使得编译器能够更为灵活地操作和优化代码。

本文将进一步探究 LLVM IR 的不同表示形式,将有助于我们更好地理解代码在编译器中是如何被处理和转换的。

LLVM IR 概述

编译器常见的作用是将源高级语言的代码编译到某种中间表示(Intermediate Representation,一般称为 IR),然后再将 IR 翻译为目标体系结构(具体硬件比如 MIPS 或 X86)的汇编语言或者硬件指令。

LLVM IR 提供了一种抽象层,使程序员可以更灵活地控制程序的编译和优化过程,同时保留了与硬件无关的特性。通过使用 LLVM IR,开发人员可以更好地理解程序的行为,提高代码的可移植性和性能优化的可能性。

LLVM 基本架构

目前常见的编译器都分为了三个部分,前端(Frontend),优化层(Optimizeation)以及后端(Backend),每一部分都承担了不同的功能:

  • 前端:负责将高级源语言代码转换为 LLVM 的中间表示(IR),为后续的编译阶段打下基础。

  • 优化层:对生成的中间表示 IR 进行深入分析和优化,提升代码的性能和效率。

  • 后端:将优化后的中间表示 IR 转换成目标机器的特定语言,确保代码能够在特定硬件上高效运行。

这种分层的方法不仅提高了编译过程的模块化,还使得编译器能够更灵活地适应不同的编程语言和目标平台。同理,LLVM 也是按照这一结构设计进行架构设计:

在 LLVM 中不管是前端、优化层、还是后端都有大量的 IR,使得 LLVM 的模块化程度非常高,可以大量的复用一些相同的代码,非常方便的集成到不同的 IDE 和编译器当中。

经过中间表示 IR 这种做法相对于直接将源代码翻译为目标体系结构的好处主要有两个:

  1. 有一些优化技术是目标平台无关的,我们只需要在 IR 上做这些优化,再翻译到不同的汇编,这样就能够在所有支持的体系结构上实现这种优化,这大大的减少了开发的工作量。

  2. 其次,假设我们有 m 种源语言和 n 种目标平台,如果我们直接将源代码翻译为目标平台的代码,那么我们就需要编写 m * n 个不同的编译器。然而,如果我们采用一种 IR 作为中转,先将源语言编译到这种 IR ,再将这种 IR 翻译到不同的目标平台上,那么我们就只需要实现 m + n 个编译器。

值得注意的是,LLVM 并非使用单一的 IR 进行表达,前端传给优化层时传递的是一种抽象语法树(Abstract Syntax Tree,AST)的 IR。因此 IR 是一种抽象表达,没有固定的形态。

抽象语法树的作用在于牢牢抓住程序的脉络,从而方便编译过程的后续环节(如代码生成)对程序进行解读。AST 就是开发者为语言量身定制的一套模型,基本上语言中的每种结构都与一种 AST 对象相对应。

在中端优化完成之后会传一个 DAG 图的 IR 给后端,DAG 图能够非常有效的去表示硬件的指定的顺序。

DAG(Directed Acyclic Graph,有向无环图)是图论中的一种数据结构,它是由顶点和有向边组成的图,其中顶点之间的边是有方向的,并且图中不存在任何环路(即不存在从某个顶点出发经过若干条边之后又回到该顶点的路径)。

在计算机科学中,DAG 图常常用于描述任务之间的依赖关系,例如在编译器和数据流分析中。DAG 图具有拓扑排序的特性,可以方便地对图中的节点进行排序,以确保按照依赖关系正确地执行任务。

编译的不同阶段会产生不同的数据结构和中间表达,如前端的抽象语法树(AST)、优化层的 DAG 图、后端的机器码等。后端优化时 DAG 图可能又转为普通的 IR 进行优化,最后再生产机器码。

LLVM IR 表示形式

LLVM IR 具有三种表示形式,这三种中间格式是完全等价的:

  • 在内存中的编译中间语言(无法通过文件的形式得到的指令类等)

  • 在硬盘上存储的二进制中间语言(格式为.bc)

  • 人类可读的代码语言(格式为.ll)

接下来我们就看一下具体的 .ll 文件格式。

LLVM IR 示例与语法

示例程序

我们编写一个简单的 C 语言程序,并将其编译为 LLVM IR。

test.c 文件内容如下:

#include <stdio.h>void test(int a, int b)
{int c = a + b;
}int main(void)
{int a = 10;int b = 20;test(a, b);return 0;
}

接下来我们使用 Clang 编译器将 C 语言源文件 test.c 编译成 LLVM 格式的中间代码。具体参数的含义如下:

  • clang:Clang 编译器
  • -S:生成汇编代码而非目标文件
  • -emit-llvm:生成 LLVM IR 中间代码
  • .\test.c:要编译的 C 语言源文件
clang -S -emit-llvm .\test.c

在 LLVM IR 中,所生成的 .ll 文件的基本语法为:

  1. 指令以分号 ; 开头表示注释
  2. 全局表示以 @ 开头,局部变量以 % 开头
  3. 使用 define 关键字定义函数,在本例中定义了两个函数:@test@main
  4. alloca 指令用于在堆栈上分配内存,类似于 C 语言中的变量声明
  5. store 指令用于将值存储到指定地址
  6. load 指令用于加载指定地址的值
  7. add 指令用于对两个操作数进行加法运算
  8. i32 32 位 4 个字节的意思
  9. align 字节对齐
  10. ret 指令用于从函数返回

编译完成后,生成的 test.ll 文件内容如下:

; ModuleID = '.\test.c'
source_filename = ".\\test.c"
target datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-w64-windows-gnu"; Function Attrs: noinline nounwind optnone uwtable
define dso_local void @test(i32 noundef %0, i32 noundef %1) #0 { ;定义全局函数@test(a,b)%3 = alloca i32, align 4 ; 局部变量 c%4 = alloca i32, align 4 ; 局部变量 d%5 = alloca i32, align 4 ; 局部变量 estore i32 %0, ptr %3, align 4 ; %0 赋值给%3 c=astore i32 %1, ptr %4, align 4 ; %1 赋值给%4 d=b%6 = load i32, ptr %3, align 4 ; 读取%3,值给%6 就是参数 a%7 = load i32, ptr %4, align 4 ; 读取%4,值给%7 就是参数 b%8 = add nsw i32 %6, %7store i32 %8, ptr %5, align 4 ; 参数 %9 赋值给%5 e 就是转换前函数写的 int c 变量ret void
}; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {%1 = alloca i32, align 4%2 = alloca i32, align 4%3 = alloca i32, align 4store i32 0, ptr %1, align 4store i32 10, ptr %2, align 4store i32 20, ptr %3, align 4%4 = load i32, ptr %2, align 4%5 = load i32, ptr %3, align 4call void @test(i32 noundef %4, i32 noundef %5)ret i32 0
}attributes #0 = { noinline nounwind optnone uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }!llvm.module.flags = !{!0, !1, !2, !3}
!llvm.ident = !{!4}!0 = !{i32 1, !"wchar_size", i32 2}
!1 = !{i32 8, !"PIC Level", i32 2}
!2 = !{i32 7, !"uwtable", i32 2}
!3 = !{i32 1, !"MaxTLSAlign", i32 65536}
!4 = !{!"(built by Brecht Sanders, r4) clang version 17.0.6"}

以上程序中包含了两个函数:@test@main@test 函数接受两个整型参数并计算它们的和,将结果存储在一个局部变量中。@main 函数分配三个整型变量的内存空间,然后分别赋予初始值,并调用 @test 函数进行计算。最后 @main 函数返回整数值 0。

程序的完整执行流程如下:

  1. @main 函数中,首先分配三个整型变量的内存空间 %1,%2,%3,分别存储 0,10,20
  2. 接下来加载 %2 和 %3 的值,将 10 和 20 作为参数调用 @test 函数
  3. @test 函数中,分别将传入的参数 %0 和 %1 存储至本地变量 %3 和 %4 中
  4. 然后加载 %3 和 %4 的值,进行加法操作,并将结果存储至 %5 中
  5. 最后,程序返回整数值 0

LLVM IR 的代码和 C 语言编译生成的代码在功能实现上具有完全相同的特性。.ll 文件作为 LLVM IR 的一种中间语言,可以通过 LLVM 编译器将其转换为机器码,从而实现计算机程序的执行。

基本语法

除了上述示例代码中涉及到的基本语法外,LLVM IR 作为中间语言也同样有着条件语句、循环体和对指针操作的语法规则。

  1. 条件语句

例如以下 C 语言代码:

#include <stdio.h>int main()
{int a = 10;if(a%2 == 0)return 0;else return 1;
}

在经过编译后的 .ll 文件的内容如下所示:

define i32 @main() #0 {
entry:%retval = alloca i32, align 4%a = alloca i32, align 4store i32 0, i32* %retval, align 4store i32 10, i32* %a, align 4%0 = load i32, i32* %a, align 4%rem = srem i32 %0, 2%cmp = icmp eq i32 %rem, 0br i1 %cmp, label %if.then, label %if.elseif.then:                                          ; preds = %entrystore i32 0, i32* %retval, align 4br label %returnif.else:                                          ; preds = %entrystore i32 1, i32* %retval, align 4br label %returnreturn:                                           ; preds = %if.else, %if.then%1 = load i32, i32* %retval, align 4ret i32 %1
}

icmp 指令是根据比较规则,比较两个操作数,将比较的结果以布尔值或者布尔值向量返回,且对于操作数的限定是操作数为整数或整数值向量、指针或指针向量。其中,eq 是比较规则,%rem 和 0 是操作数,i32 是操作数类型,比较 %rem 与 0 的值是否相等,将比较的结果存放到 %cmp 中。

br 指令有两种形式,分别对应于条件分支和无条件分支。该指令的条件分支在形式上接受一个“i1”值和两个“label”值,用于将控制流传输到当前函数中的不同基本块,上面这条指令是条件分支,类似于 c 中的三目条件运算符 < expression ?Statement:statement>;无条件分支的话就是不用判断,直接跳转到指定的分支,类似于 c 中 goto ,比如说这个就是无条件分支 br label %return。br i1 %cmp, label %if.then, label %if.else 指令的意思是,i1 类型的变量 %cmp 的值如果为真,执行 if.then 否则执行 if.else

  1. 循环体

例如以下 C 程序代码:

#include <stdio.h>int main()
{int a = 0, b = 1;while(a < 5){a++;b *= a;}return b;
}

在经过编译后的 .ll 文件的内容如下所示:

define i32 @main() #0 {
entry:%retval = alloca i32, align 4%a = alloca i32, align 4%b = alloca i32, align 4store i32 0, i32* %retval, align 4store i32 0, i32* %a, align 4store i32 1, i32* %b, align 4br label %while.condwhile.cond:                                       ; preds = %while.body, %entry%0 = load i32, i32* %a, align 4%cmp = icmp slt i32 %0, 5br i1 %cmp, label %while.body, label %while.endwhile.body:                                       ; preds = %while.cond%1 = load i32, i32* %a, align 4%inc = add nsw i32 %1, 1store i32 %inc, i32* %a, align 4%2 = load i32, i32* %a, align 4%3 = load i32, i32* %b, align 4%mul = mul nsw i32 %3, %2store i32 %mul, i32* %b, align 4br label %while.condwhile.end:                                        ; preds = %while.cond%4 = load i32, i32* %b, align 4ret i32 %4
}

对比 if 语句可以发现,while 中几乎没有新的指令出现,所以说所谓的 while 循环,也就是“跳转+分支”这一结构。同理,for 循环也可以由“跳转+分支”这一结构构成。

  1. 指针

例如以下 C 程序代码:

int main(){int i = 10;int* pi = &i;printf("i 的值为:%d",i);printf("*pi 的值为:%d",*pi);printf("&i 的地址值为:",%d);printf("pi 的地址值为:",%d);
}

在经过编译后的 .ll 文件的内容如下所示:

@.str = private unnamed_addr constant [16 x i8] c"i\E7\9A\84\E5\80\BC\E4\B8\BA\EF\BC\9A%d\00", align 1
@.str.1 = private unnamed_addr constant [18 x i8] c"*pi\E7\9A\84\E5\80\BC\E4\B8\BA\EF\BC\9A%d\00", align 1
@.str.2 = private unnamed_addr constant [23 x i8] c"&i\E7\9A\84\E5\9C\B0\E5\9D\80\E5\80\BC\E4\B8\BA\EF\BC\9A%p\00", align 1
@.str.3 = private unnamed_addr constant [23 x i8] c"pi\E7\9A\84\E5\9C\B0\E5\9D\80\E5\80\BC\E4\B8\BA\EF\BC\9A%p\00", align 1define i32 @main(){
entry:%i = alloca i32, align 4%pi = alloca i32*, align 8store i32 10, i32* %i, align 4store i32* %i, i32** %pi, align 8%0 = load i32, i32* %i, align 4%call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str, i32 0, i32 0), i32 %0)%1 = load i32, i32* %i, align 4%call1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.1, i32 0, i32 0), i32 %1)%call2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.2, i32 0, i32 0), i32* %i)%2 = load i32*, i32** %pi, align 8%call3 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.3, i32 0, i32 0), i32* %2)ret i32 0
}declare i32 @printf(i8*, ...)

对指针的操作就是指针的指针,开辟一块指针类型的内存,里面放个指针`%pi = alloca i32*, align 8

如果您想了解更多AI知识,与AI专业人士交流,请立即访问昇腾社区官方网站https://www.hiascend.com/或者深入研读《AI系统:原理与架构》一书,这里汇聚了海量的AI学习资源和实践课程,为您的AI技术成长提供强劲动力。不仅如此,您还有机会投身于全国昇腾AI创新大赛和昇腾AI开发者创享日等盛事,发现AI世界的无限奥秘~
转载自:| https://www.cnblogs.com/ZOMI/articles/18558900 | header |
| ---------------------------------------------- | ------ |
| | |

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

转载:【AI系统】AI 框架基础介绍

什么是 AI 算法?什么是神经网络?神经网络有什么用?为什么神经网络需要训练?什么是模型?AI 框架有什么用?AI 框架能解决什么问题? 上面的几个问题其实还挺有挑战的,也是本文需要回答的一个问题。下面来对一些基础概念进程澄清:首先深度学习是机器学习研究领域中的一种范…

转载:【AI系统】谷歌 TPU v1-脉动阵列

本文深入探讨了谷歌 TPU v1 的架构和设计原理。我们将解析 TPU v1 芯片的关键元素,包括 DDR3 DRAM、矩阵乘法单元(MXU)、累加器和控制指令单元。重点介绍脉动阵列(Systolic Array)的工作原理,它是 TPU 的核心,通过数据的流水线式处理实现高效的矩阵乘法计算。此外,我们…

GitLab安装使用

GitLab安装GitLab的安装 1. 环境配置 关闭防火墙和selinux [root@gitlab ~]# systemctl stop firewalld [root@gitlab ~]# systemctl disable firewalld Removed "/etc/systemd/system/multi-user.target.wants/firewalld.service". Removed "/etc/systemd/syst…

2024-2025-1 20241401 《计算机基础与程序设计》 第十二周学习总结

班级链接 2024计算机基础与程序设计作业要求 第十二周作业教材学习内容总结 《C语言程序设计》第11章指针与一维数组间的关系: 指针和一维数组之间的联系,包括如何使用指针访问数组元素等。 指针与二维数组间的关系: 指针与二维数组交互的方式,以及相关的访问方法。 指针数…

转载:【AI系统】编译器基础介绍

随着深度学习的不断发展,AI 模型结构在快速演化,底层计算硬件技术更是层出不穷,对于广大开发者来说不仅要考虑如何在复杂多变的场景下有效的将算力发挥出来,还要应对 AI 框架的持续迭代。AI 编译器就成了应对以上问题广受关注的技术方向,让用户仅需专注于上层模型开发,降…

转载:【AI系统】昇腾 AI 处理器

本文将会介绍华为昇腾 AI 处理器的架构与卷积加速原理。昇腾 AI 处理器是华为基于达芬奇架构专为AI计算加速而设计的处理器,它支持云边端一体化的全栈全场景解决方案,具有高能效比和强大的 3D Cube 矩阵计算单元,支持多种计算模式和混合精度计算。 昇腾 AI 处理器的架构包括…

转载:【AI系统】CPU 计算时延

CPU(中央处理器)是计算机的核心组件,其性能对计算机系统的整体性能有着重要影响。CPU 计算时延是指从指令发出到完成整个指令操作所需的时间。理解 CPU 的计算时延对于优化计算性能和设计高效的计算系统至关重要。在本文中我们将要探讨 CPU 的计算时延组成和影响时延产生的因…

转载:【AI系统】昇腾 AI 架构介绍

昇腾计算的基础软硬件是产业的核心,也是 AI 计算能力的来源。华为,作为昇腾计算产业生态的一员,是基础软硬件系统的核心贡献者。昇腾计算软硬件包括硬件系统、基础软件和应用使能等。 而本文介绍的 AI 系统整体架构(如图所示),则是对应与昇腾 AI 产业的全栈架构较为相似。…

转载:【AI系统】AI 基本理论奠定

AI 基本理论奠定 虽然 AI 在今年取得了举世瞩目的进展与突破,但是其当前基于的核心理论神经网络等,在这波浪潮开始前已经基本奠定,并经历了多次的起起伏伏。神经网络作为 AI 的前身,经历了以下的发展阶段:萌芽兴奋期(约 1950s) 1943 年,神经科学家和控制论专家 Warren …

pyqt5 ms级获取时间小工具

代码:from PyQt5 import QtCore, QtGui, QtWidgets from datetime import datetime //注意添加class Ui_MainWindow(object):def setupUi(self, MainWindow):MainWindow.setObjectName("MainWindow")MainWindow.resize(444, 491)MainWindow.setStyleSheet("ba…