编译原理第二次小班课

2024-01-08 01:52
文章标签 编译 原理 第二次 小班

本文主要是介绍编译原理第二次小班课,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

写给入门者的LLVM介绍 - 知乎 (zhihu.com)
代码优化与LLVM IR pass | Kiprey’s Blog
A Tour to LLVM IR(上) - 知乎 (zhihu.com)
第5章 LLVM中间表示 — Getting Started with LLVM Core Libraries 文档 (getting-started-with-llvm-core-libraries-zh-cn.readthedocs.io)

在这里插入图片描述

第一页-什么是LLVM

LLVM是一个编译器(确切的说是一套框架+基于框架的一些编译器实现)
LLVM IR(Intermediate Representation)是LLVM的一种中间表示,也可以视为中间代码(便于代码优化)
LLVM之所以优秀,在于以下几点:

  • 1、LLVM的中间表达(IR)是可以dump出来成为可阅读的文本形式的(语法有点像汇编),看起来微不足道,但是其他很多编译器却只有内存中的数据结构,使得学习调试难度大增。
  • 2、模块化的设计比较好,吸收了很多前人经验,也和设计者的架构功力息息相关。虽然始于学术项目,但LLVM一直受到工业界的支持(Apple),所以不仅好用,而且开源可定制。避免了在Java中类似面临选择HotSpot和Jikes的困境。

在这里插入图片描述

第二页-为什么关心LLVM

有时候一项工作看起来并不完全是个完整的编译行为,但只要涉及到源码到源码的转换,了解LLVM通常会有所帮助。
以下是一些使用 LLVM 完成并非所有编译操作的研究项目的示例:

  • UIUC 的Virtual Ghost展示了您以使用编译器通道来保护进程免受受损操作系统内核的影响。
  • UW 的CoreDet使多线程程序具有确定性。
  • 在近似计算工作中,使用 LLVM pass 将错误注入程序以模拟容易出错的硬件。
    LLVM 不只是用于实现新的编译器及编译优化!

在这里插入图片描述

第三页-LLVM的基本架构

这张图片显示了 LLVM 架构的主要部件:

  • 前端是clang,获取源代码并将其转换为中间表示或 IR。这种翻译简化了编译器其余部分的工作。
  • IR经过N个pass的优化处理。在一般情况下,pass 通常会优化代码:生成一个 IR 程序作为输出,它与作为输入的 IR 执行相同的操作,只是它更快更优。这是需要拓展定制的地方。使用相关工具可以通过在编译过程中查看和更改 IR 来进行。
  • 后端,生成实际的机器码。很多时候不需要接触这部分。
    尽管这种架构描述了当今大多数编译器,但这里值得注意的是 LLVM 的一个新颖之处:整个编译过程中使用相同的 IR 。在其他编译器中,每次传递都可能以独特的形式生成代码。

在这里插入图片描述

第四页-了解LLVM IR

LLVM IR 具有三种表示形式,这三种中间格式是完全等价的:
在内存中的编译中间语言(我们无法通过文件的形式得到)
在硬盘上存储的二进制中间语言(bitcode形式,格式为 .bc)
人类可读的代码语言(LLVM汇编文件形式,格式为 .ll)

在这里插入图片描述

第五页-汇编形式的LLVM IR

汇编形式的IR是可读的。所以这里用一个简单的例子展示一下汇编形式的IR。首先编写一个简单的c语言函数如下:

// add.cpp
int add(int a, int b) {return a + b;
}

使用如下命令可以产生汇编形式的IR:
clang add.cpp -emit-llvm -S -c -o add.ll
具体的汇编IR如下:

; ModuleID = 'add.cpp'
source_filename = "add.cpp"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @_Z3addii(i32, i32) #0 {%3 = alloca i32, align 4%4 = alloca i32, align 4store i32 %0, i32* %3, align 4store i32 %1, i32* %4, align 4%5 = load i32, i32* %3, align 4%6 = load i32, i32* %4, align 4%7 = add nsw i32 %5, %6ret i32 %7
}
  • 注释以 分号(;) 开头,分号(;)后面的注释指明了module的标识,source_filename是表明这个module是从什么文件编译得到的(如果你打开main.ll会发现这里的值是main.cpp),如果该modules是通过链接得到的,这里的值就会是llvm-link
  • LLVM IR 是静态类型的(即在编写时每个值都有明确的类型)
  • 局部变量的作用域是单个函数(比如 @main 中的 %1 是一个 i32* 类型的地址,而 @foo 中的 %1 是一个 i32 类型的值)
  • 临时寄存器(或者说临时变量)拥有升序的名字(比如 @main 函数中的 %1%2%3
  • 全局变量与局部变量由前缀区分,全局变量和函数名以 @ 为前缀,局部变量以 % 为前缀
  • 大多数指令与字面含义相同(alloca 分配内存并返回地址,load 从内存读出值,store 向内存存值,add 用于加法等)

在这里插入图片描述

第六页-内存中的IR

Module包含Functions,其中包含BasicBlocks,其中包含Instructions。除了 Module 之外的所有东西都来自Value。
![[Snipaste_2023-11-28_16-19-11.png|125]]
Module类,Module可以理解为一个完整的编译单元。一般来说,这个编译单元就是一个源码文件,比如一个后缀为cpp的源文件。每个module之间相互独立,module主要包含了声明或者定义的函数、全局变量等。
Function类,就是对应一个函数单元。主要包含了大量 BasicBlock 、参数和返回值类型、可变参数列表、函数的attribute和其他函数的基本信息Function由无数个 BasicBlock组成,使用列表存放,有且仅有一个 EntryBlock ,是列表中的第一个 BasicBlock,代码真正执行的时候,就从 EntryBlock开始执行。
BasicBlock类,这个类表示一个基本代码块,“基本代码块”就是一段没有控制流逻辑的基本流程,相当于程序流程图中的基本过程。控制流只能从基本块的第一个指令进入该块。也就是说,没有跳转到基本块中间的或末尾指令的转移指令。除了基本块的最后一个指令,控制流在离开基本块之前不会跳转或停机。
Instruction类,指令类就是LLVM中定义的基本操作,比如加减乘除这种算数指令、函数调用指令、跳转指令、返回指令等等

在这里插入图片描述

第七页-举例

下面,我们通过一个例子来介绍程序的控制流是如何通过基本块与终结指令描述的:
if.c 导出为 LLVM IR

这个程序的控制流如图所示(点一下PPT)

br i1 %9, label %10, label %11 ; A
br label %12 ; B
br label %12 ; C

br指令一共在代码中出现了三次
在这里,我们先介绍一下br指令的用法, br指令的语法为 br + 标志位 + truelabel + falselabel,或者br + label
形如上面代码中 A 用法的转移指令叫做条件转移,如果标志位为1,程序会跳往truelabel标记的basicblock。如果标志位为0,程序会跳往falseblock标记的basicblock。比如,在代码br i1 %9, label %10, label %11中,如果%9的值为1,就会跳转往基本块%10,如果为0,就会跳转往基本块%11

形如上面代码中B,C的用法的转移指令叫做无条件转移,他会在程序运行到此处时无条件跳转到目标基本块。在上面代码中B,C两处的代码都会无条件跳转到基本块%12

如上图所示,%9icmp eq指令(用来判断两个值是否相等,我们会在_推荐使用的指令_一节详细介绍)的结果,如果%7等于%8,那么%9的值就会为1,否则为0。这条指令对应了源代码中的if(a == b)c=5对应了基本块%10c=10对应了基本块%11,这两个基本块运行结束时都需要跳转到目标基本块%12执行后面的语句putint(c)以及return 0

在这里插入图片描述
在这里插入图片描述

第八页-了解常用的代码优化方法

在这里插入图片描述

删除公共子表达式
如果表达式x op y先前已被计算过,并且从先前的计算到现在,x op y中的变量值没有改变,则x op y的这次出现就称为公共子表达式(common subexpression)

删除无用代码
无用代码(Dead-code):其计算结果永远不会被使用的语句

常量合并
如果在编译时刻推导出一个表达式的值是常量,就可以使用该常量来替代这个表达式。该技术被称为 常量合并

代码移动
这个转换的结果是那些 不管循环多少次都得到相同结果的表达式(即循环不变计算,loop-invariant computation),在进入循环之前就对它们进行求值。

强度削弱用较快的操作代替较慢的操作,如用 代替 。(例:2*x ⇒ x+x)

删除归纳变量
对于一个变量x ,如果存在一个正的或负的常数c使得每次x被赋值时它的值总增加c ,那么x就称为归纳变量(Induction Variable)。在沿着循环运行时,如果有一组归纳变量的值的变化保持步调一致,常常可以将这组变量删除为只剩一个

为了优化给定的代码段,我们可以应用几种常见的编译器优化技术。这些包括但不限于常量传播、代数化简、强度削弱和消除冗余代码。下面是按阶段进行的优化步骤,以及每个阶段的代码变化情况:

初始代码

T1 = j - 2;
T2 = 4 * T1;
temp = A[T2];
T3 = j + 2;
T4 = T3 - 2;
T5 = 8 * T4;
T6 = A[T5];

阶段 1: 常量传播和代数化简

  • T1T4 实际上是相同的操作,都是 j - 2
  • T24 * (j - 2),可以直接计算。
  • T3j + 2T4T3 - 2,所以 T4 实际上就是 j
  • T58 * j(因为 T4 现在是 j)。

优化后的代码:

T1 = j - 2; // 或者可以直接在T2中使用j - 2,从而完全省略这一行
T2 = 4 * (j - 2); // 代数化简
temp = A[T2];
// T3 = j + 2; // 不再需要,因为T4和T1相等
// T4 = j; // 不再需要,因为T4和T1相等
T5 = 8 * j; // 代数化简
T6 = A[T5];

阶段 2: 消除冗余代码

  • 因为 T1T4 相等,我们可以消除 T4
  • 直接在 T2T5 的赋值中使用 j - 2j,进一步消除 T1T4

优化后的代码:

T2 = 4 * (j - 2);
temp = A[T2];
T5 = 8 * j;
T6 = A[T5];

阶段 3: 强度削弱

  • 在许多情况下,乘法操作比加法或位移操作更耗时。但在这个例子中,看起来没有直接的机会应用强度削弱。

最终优化后的代码

T2 = 4 * (j - 2);
temp = A[T2];
T5 = 8 * j;
T6 = A[T5];

这个优化过程消除了冗余的计算,简化了表达式,并减少了变量的使用,从而提高了代码的效率。需要注意的是,这些优化假设没有副作用和别名问题(即数组 A 在此期间不会被修改,且 j 是一个不变的值)。

这篇关于编译原理第二次小班课的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/581954

相关文章

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

MyBatis-Plus 与 Spring Boot 集成原理实战示例

《MyBatis-Plus与SpringBoot集成原理实战示例》MyBatis-Plus通过自动配置与核心组件集成SpringBoot实现零配置,提供分页、逻辑删除等插件化功能,增强MyBa... 目录 一、MyBATis-Plus 简介 二、集成方式(Spring Boot)1. 引入依赖 三、核心机制

redis和redission分布式锁原理及区别说明

《redis和redission分布式锁原理及区别说明》文章对比了synchronized、乐观锁、Redis分布式锁及Redission锁的原理与区别,指出在集群环境下synchronized失效,... 目录Redis和redission分布式锁原理及区别1、有的同伴想到了synchronized关键字

Linux中的HTTPS协议原理分析

《Linux中的HTTPS协议原理分析》文章解释了HTTPS的必要性:HTTP明文传输易被篡改和劫持,HTTPS通过非对称加密协商对称密钥、CA证书认证和混合加密机制,有效防范中间人攻击,保障通信安全... 目录一、什么是加密和解密?二、为什么需要加密?三、常见的加密方式3.1 对称加密3.2非对称加密四、

setsid 命令工作原理和使用案例介绍

《setsid命令工作原理和使用案例介绍》setsid命令在Linux中创建独立会话,使进程脱离终端运行,适用于守护进程和后台任务,通过重定向输出和确保权限,可有效管理长时间运行的进程,本文给大家介... 目录setsid 命令介绍和使用案例基本介绍基本语法主要特点命令参数使用案例1. 在后台运行命令2.

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、