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

相关文章

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

一文详解SpringBoot响应压缩功能的配置与优化

《一文详解SpringBoot响应压缩功能的配置与优化》SpringBoot的响应压缩功能基于智能协商机制,需同时满足很多条件,本文主要为大家详细介绍了SpringBoot响应压缩功能的配置与优化,需... 目录一、核心工作机制1.1 自动协商触发条件1.2 压缩处理流程二、配置方案详解2.1 基础YAML

Mysql删除几亿条数据表中的部分数据的方法实现

《Mysql删除几亿条数据表中的部分数据的方法实现》在MySQL中删除一个大表中的数据时,需要特别注意操作的性能和对系统的影响,本文主要介绍了Mysql删除几亿条数据表中的部分数据的方法实现,具有一定... 目录1、需求2、方案1. 使用 DELETE 语句分批删除2. 使用 INPLACE ALTER T

MySQL中慢SQL优化的不同方式介绍

《MySQL中慢SQL优化的不同方式介绍》慢SQL的优化,主要从两个方面考虑,SQL语句本身的优化,以及数据库设计的优化,下面小编就来给大家介绍一下有哪些方式可以优化慢SQL吧... 目录避免不必要的列分页优化索引优化JOIN 的优化排序优化UNION 优化慢 SQL 的优化,主要从两个方面考虑,SQL 语

MySQL中慢SQL优化方法的完整指南

《MySQL中慢SQL优化方法的完整指南》当数据库响应时间超过500ms时,系统将面临三大灾难链式反应,所以本文将为大家介绍一下MySQL中慢SQL优化的常用方法,有需要的小伙伴可以了解下... 目录一、慢SQL的致命影响二、精准定位问题SQL1. 启用慢查询日志2. 诊断黄金三件套三、六大核心优化方案方案

Redis中高并发读写性能的深度解析与优化

《Redis中高并发读写性能的深度解析与优化》Redis作为一款高性能的内存数据库,广泛应用于缓存、消息队列、实时统计等场景,本文将深入探讨Redis的读写并发能力,感兴趣的小伙伴可以了解下... 目录引言一、Redis 并发能力概述1.1 Redis 的读写性能1.2 影响 Redis 并发能力的因素二、

使用国内镜像源优化pip install下载的方法步骤

《使用国内镜像源优化pipinstall下载的方法步骤》在Python开发中,pip是一个不可或缺的工具,用于安装和管理Python包,然而,由于默认的PyPI服务器位于国外,国内用户在安装依赖时可... 目录引言1. 为什么需要国内镜像源?2. 常用的国内镜像源3. 临时使用国内镜像源4. 永久配置国内镜

C#原型模式之如何通过克隆对象来优化创建过程

《C#原型模式之如何通过克隆对象来优化创建过程》原型模式是一种创建型设计模式,通过克隆现有对象来创建新对象,避免重复的创建成本和复杂的初始化过程,它适用于对象创建过程复杂、需要大量相似对象或避免重复初... 目录什么是原型模式?原型模式的工作原理C#中如何实现原型模式?1. 定义原型接口2. 实现原型接口3

Java嵌套for循环优化方案分享

《Java嵌套for循环优化方案分享》介绍了Java中嵌套for循环的优化方法,包括减少循环次数、合并循环、使用更高效的数据结构、并行处理、预处理和缓存、算法优化、尽量减少对象创建以及本地变量优化,通... 目录Java 嵌套 for 循环优化方案1. 减少循环次数2. 合并循环3. 使用更高效的数据结构4

Deepseek使用指南与提问优化策略方式

《Deepseek使用指南与提问优化策略方式》本文介绍了DeepSeek语义搜索引擎的核心功能、集成方法及优化提问策略,通过自然语言处理和机器学习提供精准搜索结果,适用于智能客服、知识库检索等领域... 目录序言1. DeepSeek 概述2. DeepSeek 的集成与使用2.1 DeepSeek API