基本块和DAG怎么理解和应用?

文章导读
Previous Quiz Next 在编译器设计中,我们关注几个关键阶段,包括词法分析、语法分析和中间代码生成。另一个关键阶段是优化,它通过提高执行速度和减少内存使用来帮助生成高效的机器代码。
📋 目录
  1. 什么是 Basic Blocks?
  2. 使用 DAGs 优化 Basic Blocks
  3. 为什么使用 DAGs?
  4. 如何为优化构造 DAG
  5. 基本块和 DAG 优化:优点和缺点
  6. 结论
A A

基本块和有向无环图



Previous
Quiz
Next

在编译器设计中,我们关注几个关键阶段,包括词法分析、语法分析和中间代码生成。另一个关键阶段是优化,它通过提高执行速度和减少内存使用来帮助生成高效的机器代码。

一种重要的优化技术涉及 Basic Blocks 和 Directed Acyclic Graphs (DAGs)。基本块将代码分解为独立的单元,而 DAGs 有助于检测公共子表达式并消除冗余计算,从而减少指令数量。

在本章中,我们将探讨基本块,了解 DAGs 如何优化代码,并通过示例以获得更好的清晰度。

什么是 Basic Blocks?

basic block 是一系列指令或操作的序列,具有单一入口点和单一出口点。这意味着执行从开头进入并在结尾离开,中间没有任何跳转或分支。

Basic Blocks 的特性

要正确理解基本块,我们必须了解其特性。下面列出了基本块的重要特性 −

  • 内部无跳转或分支语句 − 基本块内部不包含标签 (LBL)、跳转 (JMP) 或测试 (TST) 语句。
  • 单一入口,单一出口 − 执行从第一条指令开始,并顺序继续直到最后一条指令。
  • 自包含 − 基本块不与其他代码段干扰。这使得它们更容易分析和优化。

示例:识别 Basic Blocks

考虑以下从 Java 表达式生成的原子序列(三地址代码) −

(ADD, b, c, T1)  
(LBL, L1)  
(ADD, b, c, T2)  
(MUL, T1, T2, T3)  
(TST, b, c,, 1, L3)  
(MOV, T3,, a)   

我们可以将其分为 三个基本块

Basic Block Instructions
Block 1 (ADD, b, c, T1)
Block 2 (LBL, L1) (ADD, b, c, T2) (MUL, T1, T2, T3)
Block 3 (TST, b, c, 1, L3) (MOV, T3, , a)

每个块都是独立的,并且在其内部不包含任何控制流变化。

使用 DAGs 优化 Basic Blocks

Directed Acyclic Graph (DAG) 是一种基于图的结构,有助于消除基本块中的冗余计算并检测公共子表达式。在进一步讨论之前,让我们正确理解这一点。

为什么使用 DAGs?

我们使用 DAGs 的原因如下 −

  • 减少冗余计算 − 如果像 b + c 这样的操作出现多次,DAG 有助于仅计算一次。
  • 优化指令数量 − DAG 简化原子序列。生成更短、更高效的版本。
  • 最小化临时存储 − 移除冗余临时变量。这减少了寄存器或内存使用。

示例:表达式优化的 DAG

考虑以下 Java 语句 −

a = (b + c) * (b + c);  

解析器生成的未优化原子序列 −

(ADD, b, c, T1)  
(ADD, b, c, T2)  
(MUL, T1, T2, T3)  
(MOV, T3,, a)  

该序列重新计算了两次 b + c,这浪费了时间和资源。

使用 DAG 表示,我们认识到两次 b + c 是相同的。因此,我们仅存储一次并重复使用 −

(ADD, b, c, T1)  
(MUL, T1, T1, a)  

现在,我们只有一次 ADD 操作,而不是两次。这里 MUL 操作直接将结果存储在 a 中。

如何为优化构造 DAG

我们了解了 DAG 的作用,但我们需要理解如何构建 DAG。为了从原子序列构建 DAG,我们遵循以下步骤:

  • 步骤 1: 读取一个 Atom(指令) − 如果操作和操作数已经在 DAG 中存在,则重用该节点。否则,创建一个新节点。
  • 步骤 2: 连接节点 − 从操作节点到操作数节点绘制有向弧。然后跟踪每个节点关联的变量。
  • 步骤 3: 生成优化的代码 − 现在按逆序遍历 DAG。这将有助于生成最小的原子序列。

示例:为优化构建 DAG

考虑以下 Java 表达式 −

a * b + a * b + a * b 

解析器生成以下 atoms

(MUL, a, b, T1)  
(MUL, a, b, T2)  
(ADD, T1, T2, T3)  
(MUL, a, b, T4)  
(ADD, T3, T4, T5)  

使用 DAG 构造过程

为 MUL、a、b 创建单个节点(而不是三个)。

Using the DAG Construction Process

在两个 ADD 操作中重用它,而不是重新计算。

DAG Construction Process

优化的原子序列(从 DAG 生成)

Generated from DAG
(MUL, a, b, T1)  
(ADD, T1, T1, T3)  
(ADD, T3, T1, T5)  

在这里,我们消除了冗余的乘法运算,并将五个指令减少到仅三个。

基本块和 DAG 优化:优点和缺点

下表突出了基本块和 DAG 优化的优点和缺点 −

优点 缺点
更快的执行 − 更少的指令意味着更少的 CPU 时间,从而加速程序执行。 仅限于基本块 − DAG 仅在单个基本块内工作,无法优化跨越不同块的代码,因为可能存在跳转和分支语句。
减少内存使用 − 更少的临时变量减少了寄存器和内存存储需求。 无法处理带有标签或跳转的代码 − 当存在标签(LBL)或跳转(JMP)指令时,无法进行基于 DAG 的优化,因为控制流会改变执行顺序。
更好的代码优化 − 基于 DAG 的优化帮助编译器生成紧凑、高效的代码。 DAG 构造的开销 − 构建和维护 DAG 会增加编译器的复杂性,大型表达式可能需要更多内存来存储 DAG。

结论

在本章中,我们解释了编译器设计中的基本块和 DAG 概念,以及它们如何帮助代码优化。我们看到了基本块如何将代码分割成独立的执行单元,以及 DAG 如何通过检测公共子表达式来消除冗余计算。

使用基于 DAG 的优化,我们可以减少编译程序中的指令数量、内存使用和执行时间。基本块优化很强大,但它仅限于没有跳转或标签的代码段。