LLVM Cpu0 新后端7 第二部分 窥孔优化

2024-06-10 00:12

本文主要是介绍LLVM Cpu0 新后端7 第二部分 窥孔优化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 想好好熟悉一下llvm开发一个新后端都要干什么,于是参考了老师的系列文章:

LLVM 后端实践笔记

代码在这里(还没来得及准备,先用网盘暂存一下):

链接: https://pan.baidu.com/s/1yLAtXs9XwtyEzYSlDCSlqw?pwd=vd6s 提取码: vd6s 

这一章会介绍与控制流有关的功能实现,比如 if、else、while 和 for 等,还会介绍如何将控制流的 IR 表示转换为机器指令;之后会引入几个后端优化,处理一些跳转需求引入的问题,同时来说明如何编写后端优化的 pass。在条件指令小节中,会介绍 LLVM IR 中的特殊指令 select 和 select_cc,以及如何处理这种指令,从而来支持更细节的控制流支持实现。

目录

一、第三节

1.1 新增的文件

1.1.1 Cpu0DelaySlotFiller.cpp

1.2 修改的文件

1.2.1 Cpu0.h

1.2.2 Cpu0TargetMachine.cpp

1.2.3 Cpu0AsmPrinter.cpp

二、第四节

2.1 修改的文件

2.1.1 Cpu0InstrInfo.td

2.1.2 Cpu0ISelLowering.cpp/.h

2.2 窥孔优化


一、第三节

这是个功能性的 pass。很多 RISC 机器采用多级流水线设计,有些 phase 会产生延迟,为了保证软件运行正确,可能会需要软件(编译器)在需要延迟的指令做处理。Cpu0 就符合这种情况,对于所有的跳转指令,需要有一个 cycle 的延迟,编译器需要负责对这些跳转指令做延迟插入指令。为了让实现简单,我们目前的实现只是将一条 nop 指令填充到跳转指令之后。有关于将其他有用的指令插入到跳转之后,可以参考 Mips 的实现(更加有意义,不单单是一条无用的等待),比如 MipsDelaySlotFiller.cpp 文件。

    jne    $sw, $BB_0nop    // 这里是插入的指令
$BB_1:... other instructions

对于 jne 指令,因为需要为其填充延迟指令,所以实际我们代码运行之后,会在汇编中,jne 的下一条指令,输出一条 nop 指令,这样就可以保证在 jne 执行完毕之后,再进行后续的运行。

与上一节的设计类似,我们依然是设计一个 pass,专门去识别这样一个模式,并创建一个 nop 指令并与跳转指令打到一个 bundle 中。bundle 是 LLVM 在 MI 层支持的一种指令扩展,它会在 bundle emit 之前,将 bundle 看做一条指令,而 bundle 内部却可以包含多条指令。

1.1 新增的文件

1.1.1 Cpu0DelaySlotFiller.cpp

新 pass 的实现代码。和上一小节类似的实现就不赘述了。

定义了一个 hasUnoccupiedSlot() 函数,用来判断某条指令是否满足我们上文指定的模式,首先判断这条指令是否具有延迟槽,调用 hasDelaySlot() 函数,然后判断这条指令是否已经属于一个 bundle 或者是最后一条指令,调用 isBundledWithSucc() 函数。这两个函数都是 LLVM 内置函数,在 MachineInstr.h 中实现。

当满足条件时,先使用 BuildMI 创建 nop 指令,并插入到跳转指令的后边;然后调用 MIBundleBuilder 函数,将跳转指令和 nop 指令打到一个 bundle。

1.2 修改的文件

1.2.1 Cpu0.h

添加创建新 pass 的工厂函数。

1.2.2 Cpu0TargetMachine.cpp

在 addPreEmitPass() 函数中增加我们的 pass,和上一小节同理。

1.2.3 Cpu0AsmPrinter.cpp

void Cpu0AsmPrinter::emitInstruction(const MachineInstr *MI) {......do {if (I->isPseudo())llvm_unreachable("Pseudo opcode found in emitInstruction()");MCInst TmpInst0;MCInstLowering.Lower(&*I, TmpInst0);OutStreamer->emitInstruction(TmpInst0, getSubtargetInfo());} while ((++I != E) && I->isInsideBundle()); // Delay slot check
}

这里是汇编代码发射的地方,需要检查要发射的指令是否是 bundle,如果是,则将 bundle 展开,依次发射其中的每一条指令。这一个 while 代码在之前的章节已经添加。如果不做这个检查,则只有 bundle 中的第一条指令会被发射,这将会导致代码错误。

二、第四节

在这一节我们会增加条件MOV指令。条件 MOV 指令也叫做 Select 指令,和 C 语言中的 select 操作语义一致,由一个条件值、两个指定值和一个定义值(输出)组成。在满足一个条件时,将指定值赋给定义值,否则把另一个指定值赋给定义值。我们在 Cpu0 中将实现两条 MOV 指令,分别是 movz 和 movn,表示当条件成立时(或条件不成立时),赋值第一个值,否则,赋值另一个值。

由于编码位有限,通常的条件 MOV 指令和 Select 指令均设计为其中一个指定值与定义值是同一个操作数(或者也有设计为条件值与定义值是同一个操作数):

movz $1, $2, $3;    @ $3 为条件值,当 $3 满足(为 true)时,将 $2 赋值给 $1,@ 否则,保持 $1 值不变
movn $1, $2, $3;    @ $3 为条件值,当 $3 不满足(为 false)时,将 $2 赋值给 $1,@ 否则,保持 $1 值不变

可以发现,movz 和 movn 是可以相互替代的,即:

movz $1, $2, $3;    @ 等价于
movn $2, $1, $3;    @ 当然,还需要保证上下文数据正确

在 LLVM IR 中,只有一个指令来处理这个情况,叫做 select 指令,我们需要做的就是在后端代码中,将这个 IR 转换为正确的指令表示:

%ret = select i1 %cond, i32 %a, i32 %b

2.1 修改的文件

2.1.1 Cpu0InstrInfo.td

新增和条件 MOV 相关的指令实例和用于窥孔优化的 Pattern 描述。

前者即定义 movz 和 movn 指令。注意到在 class 中使用 let Constraints = "$F = $ra" 的属性来指定两个操作符是同一个值,这种写法通常用于当其中一个 def 操作数同时也需要作为 use 操作数的情况下,比如当前的 select 示例中。

各种Pat模式中是将 IR 过来的 select + cmp 节点组合优化为一条 movz 或 movn 指令。select 指令的 condition 需要一条比较(或其他起相同作用的)指令来得出条件结果,在 Cpu032I 机器中是 cmp 指令,在 Cpu032II 机器中是 slt 指令。因为通常比较两个值是否相等,还可以采用 xor 指令,所以对于低效的 Cpu032I 比较 cmp 指令,可以使用 xor 做替换,但对于大于、小于等条件代码则只能继续使用 cmp 指令,体现在 .td 文件中就是不特别去优化 select 指令组合下的条件指令。

这个优化的路径是:

IR:   icmp + (eq, ne, sgt, sge, slt, sle) + br
DAG:  ((seteq, setne, setgt, setge, setlt, setle) + setcc) + select
Cpu0: movz, movn

2.1.2 Cpu0ISelLowering.cpp/.h

需要做一点配置。首先,LLVM 的后端会默认把 SetCC 和 Select 两个 Node 合并成一条 Select_cc 指令,这是为能够支持 Select_cc 指令的后端而准备的,这种指令是通过 condition code 来作为 select 指令的条件,比如在 X86 机器中。我们的 Cpu0 不支持这种指令,所以需要在 Cpu0ISelLowering.cpp 中,将 Select_cc 设置为 Expand 类型,表示我们希望 LLVM 帮我们替代这个类型的节点。

另一件事是将 ISD::SELECT 这个 Node 的默认下降关掉,也就是设置其为 Custom 类型,在我们自定义的下降中,直接将这个 Node 返回。因为我们不希望 select Node 在 lowering 阶段被选择为 select,这样它会无法选到指令。我们的条件 MOV 指令和这里的 select 指令有一些差异,所以只能通过在指令选择时的优化合并来实现从 select Node 到后端指令的 lowering。

2.2 窥孔优化

上边提到了一个名词窥孔优化,这里简单介绍一下。

窥孔优化(Peephole Optimization)是一种编译器优化技术,通常在生成目标代码的最后阶段应用。窥孔优化通过在目标代码中寻找和替换特定的指令序列,以改进代码的效率和性能。这种优化技术通常针对短小的指令序列,称为“窥孔”(peephole),并尝试通过替换这些指令序列来提高代码的质量。

窥孔优化的目标是通过识别和替换无效、低效或冗余的指令序列,以减少指令的执行次数、减少内存访问或提高代码的局部性,从而提高程序的性能。这种优化技术通常是基于一组预定义的优化规则或模式,编译器会在目标代码中寻找这些模式,并根据规则进行相应的优化操作。

通过窥孔优化,编译器可以在不改变程序逻辑的情况下,对生成的目标代码进行微调和改进,以使程序在运行时更加高效。这种优化技术在编译器中扮演着重要的角色,有助于提高代码的执行速度、减少资源消耗,并改善程序的整体性能。

窥孔优化其实不单单指一种优化,泛指后端多种指令层面的优化,我们后端流水线里默认就带有这个pass:

void TargetPassConfig::addMachineSSAOptimization() {
......addPass(&PeepholeOptimizerID);// Clean-up the dead code that may have been generated by peephole// rewriting.addPass(&DeadMachineInstructionElimID);
}

LLVM的窥孔优化是在PeepholeOptimizer.cpp这个文件里实现的。文件头的注释中例举出来了几个优化的典型场景。

这篇关于LLVM Cpu0 新后端7 第二部分 窥孔优化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

闲置电脑也能活出第二春?鲁大师AiNAS让你动动手指就能轻松部署

对于大多数人而言,在这个“数据爆炸”的时代或多或少都遇到过存储告急的情况,这使得“存储焦虑”不再是个别现象,而将会是随着软件的不断臃肿而越来越普遍的情况。从不少手机厂商都开始将存储上限提升至1TB可以见得,我们似乎正处在互联网信息飞速增长的阶段,对于存储的需求也将会不断扩大。对于苹果用户而言,这一问题愈发严峻,毕竟512GB和1TB版本的iPhone可不是人人都消费得起的,因此成熟的外置存储方案开

HDFS—存储优化(纠删码)

纠删码原理 HDFS 默认情况下,一个文件有3个副本,这样提高了数据的可靠性,但也带来了2倍的冗余开销。 Hadoop3.x 引入了纠删码,采用计算的方式,可以节省约50%左右的存储空间。 此种方式节约了空间,但是会增加 cpu 的计算。 纠删码策略是给具体一个路径设置。所有往此路径下存储的文件,都会执行此策略。 默认只开启对 RS-6-3-1024k

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

MySQL高性能优化规范

前言:      笔者最近上班途中突然想丰富下自己的数据库优化技能。于是在查阅了多篇文章后,总结出了这篇! 数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符 临时库表必须以tmp_为前缀并以日期为后缀,备份

poj 2976 分数规划二分贪心(部分对总体的贡献度) poj 3111

poj 2976: 题意: 在n场考试中,每场考试共有b题,答对的题目有a题。 允许去掉k场考试,求能达到的最高正确率是多少。 解析: 假设已知准确率为x,则每场考试对于准确率的贡献值为: a - b * x,将贡献值大的排序排在前面舍弃掉后k个。 然后二分x就行了。 代码: #include <iostream>#include <cstdio>#incl

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

《数据结构(C语言版)第二版》第八章-排序(8.3-交换排序、8.4-选择排序)

8.3 交换排序 8.3.1 冒泡排序 【算法特点】 (1) 稳定排序。 (2) 可用于链式存储结构。 (3) 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时, 此算法不宜采用。 #include <stdio.h>#include <stdlib.h>#define MAXSIZE 26typedef int KeyType;typedef char In

CSP 2023 提高级第一轮 CSP-S 2023初试题 完善程序第二题解析 未完

一、题目阅读 (最大值之和)给定整数序列 a0,⋯,an−1,求该序列所有非空连续子序列的最大值之和。上述参数满足 1≤n≤105 和 1≤ai≤108。 一个序列的非空连续子序列可以用两个下标 ll 和 rr(其中0≤l≤r<n0≤l≤r<n)表示,对应的序列为 al,al+1,⋯,ar​。两个非空连续子序列不同,当且仅当下标不同。 例如,当原序列为 [1,2,1,2] 时,要计算子序列 [