From 59d4cd4e2486c1595c265d2404760db28a0986e9 Mon Sep 17 00:00:00 2001 From: DarkSharpness <2040703891@qq.com> Date: Sat, 14 Sep 2024 21:18:02 +0800 Subject: [PATCH 1/2] fix(sccp): optimize phrasing --- 30-optimize.tex | 81 ++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/30-optimize.tex b/30-optimize.tex index fdd6489..8da1567 100644 --- a/30-optimize.tex +++ b/30-optimize.tex @@ -792,50 +792,49 @@ \section{SCCP: Sparse Conditional Constant Propagation 常量传播} \subsection{基础 —— SCP} -首先,我们讨论不含条件分支的稀疏常量传播(Sparse Constant Propagation, SCP)。通过迭代的方式逐步确定程序中的常量。 +首先,我们介绍不含条件分支的稀疏常量传播(Sparse Constant Propagation, SCP),通过迭代逐步确定程序中的常量。 -初始时,我们假设所有值都有可能成为常量,并为每个表达式的结果标记为“未定义”。接着从入口块开始,依次遍历表达式。 -对于纯函数表达式(没有外部状态依赖的表达式,如算术运算、比较以及无副作用的函数), -如果所有操作数都是常量或通过某些规则可确定结果(如 $0$ 乘任何数都为 $0$), -则更新表达式结果为“常量”。如果先前标记为“常量”的结果发生变化,则更新为“非常量”。若无法确定结果,则直接标记为“非常量”。 +初始时,假设所有值都有可能成为常量,并将每条语句的结果标记为“未定义”,而函数入参则被标记为“非常量”。 +然后,将所有基本块内的语句加入队列,反复从中取出语句并尝试更新其标记。更新规则如下: +1. 如果语句的结果已标记为“非常量”,则不进行处理; +2. 否则,将语句中的所有使用(use)替换为当前标记,并尝试对结果进行估值。 -需要特别注意的是,$\phi$ 函数仅当所有入口块传入的值都是相同的常量时,才会被标记为“常量”。 +若无法估值(如 \texttt{load} 指令,其值取决于内存),则标记为“非常量”; +若根据算术规则可以确定结果(如 $0$ 乘任何数为 $0$,或 \texttt{add} 的两个操作数都是常量),则更新结果标记。 +如果结果原标记为“未定义”,更新为“常量”;若已标记为“常量”,则比较新旧值,若不同则更新为“非常量”。 -每次更新表达式的标记后,我们都会更新使用该表达式的所有地方。由于程序是基于 SSA 形式的,因此我们可以沿着 def-use 链传播变化。 -该过程中的每个表达式最多遍历 $3 * (\text{操作数} + 1)$ 次,因为状态转换只可能是“未定义 -> 常量 -> 非常量”。 -因此,SCP 算法的时间复杂度是线性的。在算法结束后,所有被标记为常量的表达式都将被替换为对应的常量。 +特别地,$\phi$ 函数仅在所有入口块传入相同常量时才标记为“常量”。 + +每当语句的结果标记更新后,将使用此结果的语句加入队列。由于程序基于 SSA 形式,变化可沿着 def-use 链传播。 +每条语句最多遍历 $3 * (\text{操作数} + 1)$ 次,因状态只能从“未定义”转变为“常量”,再转变为“非常量”。 +因此,SCP 算法的时间复杂度为线性。在算法结束时,所有标记为常量的值将会被替换为相应的常量。 + +笔者注: 请特别注意区分“未定义”和“非常量”两种状态。“未定义” 是一种最强的状态,其类似未初始化的未定义行为, +因此程序可以假定其为任何使程序合法的值。而“非常量”则是一种最弱的状态,其表示无法分析得到其值,可能取决于 +程序运行时的状态(比如外部输入、内存读取等)。 \subsection{改进 —— SCCP} -注意到以上 SCP 算法对于分支是无能为力的,必须遍历所有可能的分支。而对于 \text{\%phi} 函数,我们要求所有的入口都是相同的常量, -这显然过于苛刻。事实上,在我们给出的例子里面,由于只有一个分支可达,所以这个 \text{\%phi} 函数的结果是可以确定的。 - -一个暴力的解决方案是,在跑完 SCP 后,我们对于所有条件为常量的的分支语句特判,转化为跳转指令。在全部替换结束后, -再把所有由入口块不可到达的块删除,并且删除由这些块引出的 \text{\%phi} 函数。 - -(笔者注:这其实可以是单独的一个优化步骤,把所有不可达的分支删除,并且简化控制流图。 -常用于某些未定义行为的优化。编译器总是假定程序是没有未定义行为的,因此如果一个分支有未定义行为,我们可以假定这个分支是不可达的。 -同时,一个函数必须要有返回语句 (void 类型也有 ret void),否则也是未定义行为。 -因此,我们可以分别从入口块和返回块开始,分别正向和反向遍历,标记所有可达的块。 -只有那些可以从入口块到达的块,以及可以到达返回块的块,才被保留。其他的块都将会被删除。请注意,在删除块的时候,需要检查其后继块, -删除对应的 \text{\%phi} 函数的对应入口。) - -当然,该方法在最坏情况下需要额外执行分支数量的次数。我们可以通过改进 SCP 算法来减少这个数量。我们可以在 SCP 算法中加入对分支的处理, -即为 Sparse Conditional Constant Propagation (SCCP)。 - -首先,我们需要维护两个队列:一个是表达式队列,另一个是控制流边队列。一个小 trick 是加入一个虚拟块作为入口点的前驱块, -初始化控制流队列为从虚拟块到入口块的边,而表达式队列最初为空。 - -在每一轮迭代中,我们尝试从两个dui'lie中取出一个元素。如果是控制流队列的元素(即一条边,从块 $A$ 到 $B$), -我们首先标记 $B$ 从 $A$ 来过。如果之前 $B$ 从 $A$ 来过,那么直接跳过本次操作。否则,我们会遍历 $B$ 块的所有 -\text{\%phi} 函数,以及控制流语句,按照新的规则更新 (见下文)。特别地,如果在此之前块 $B$ 从没有被访问过 (即第一次访问), -那么我们需要把 $B$ 中的其他语句也顺序访问一遍,以确保我们不会遗漏任何一个表达式。所以单个块内的遍历顺序是: -\text{\%phi} 函数 -> (如果是第一次,其他语句) -> 控制流语句。 - -如果是表达式队列的元素,我们基本按照 SCP 的规则更新,但是细节上有所不同。 -对于 \text{\%phi} 函数,我们只需检查其当前所有的来过的入口的值是否都是常量,以此来更新。 -比如块 $C$ 当前仅仅从 $A$ 和 $B$ 来过,那么只需检查每个 \text{\%phi} 函数的来自 $A$ 和 $B$ 的值是否相同即可。 -对于无条件跳转 \text{br} 语句,我们需要把 “当前块” 到 “跳往块” 的边加入控制流队列。对于条件跳转 \text{br} 语句, -如果条件被标记为 “未定义”,则不操作。如果被标记为 “常量”,则根据条件跳转的结果, -把 “当前块” 到 “跳往块” 的那条边加入控制流队列。否则,需要将两条边都加入控制流队列。 -特别地,当一个表达式的标记更新的时候,我们需要把所有使用这个表达式的地方加入表达式队列。 +SCP 对于条件分支无能为力,必须遍历所有可能的分支。此外,SCP 对 \texttt{phi} 函数要求过于严格, +即所有入口值必须是相同常量,而在某些情况下仅部分分支可达时,可能不需要这么严格的要求。 + +一种暴力解决方案是,先运行 SCP,然后将所有条件为常量的分支转化为跳转指令。 +替换完成后,删除不可达块及其相关的 \texttt{phi} 函数,再次运行 SCP,直至没有变化。 + +然而,若函数中有 $b$ 个分支语句,最坏情况下此方法需执行 $b+1$ 次 SCP。 +为减少此次数,可以通过引入条件分支处理来改进 SCP,即稀疏条件常量传播(Sparse Conditional Constant Propagation, SCCP)。 + +SCCP 需要维护两个队列:语句队列和控制流队列。一个小 trick 是引入一个虚拟块作为入口点的前驱, +初始化控制流队列为从虚拟块到入口块的边,语句队列则最初为空。 + +每轮迭代中,尝试从两个队列中取出元素进行处理: + +- 若取出的是控制流队列的元素(即从块 $A$ 到 $B$ 的边),首先标记 $B$ 从 $A$ 来过。 +若 $B$ 已从 $A$ 来过,则跳过此操作。否则,遍历 $B$ 中的 \texttt{phi} 函数和控制流语句,并按语句队列的规则更新。 +若 $B$ 是首次访问,还需遍历块中的其他语句,确保不遗漏任何内容。 +遍历顺序为:\texttt{phi} 函数 -> (仅首次访问时,其他语句) -> 控制流语句。 +- 若取出的是语句队列的元素,则大致按 SCP 的规则更新,细节略有不同: +对于 \texttt{phi} 函数,仅需检查当前所有已访问过的入口的值是否相同; +对于无条件跳转 \texttt{br} 语句,将“当前块”到“目标块”的边加入控制流队列; +对于条件跳转 \texttt{br} 语句,若条件为“未定义”,则不处理,若为“常量”,则根据条件结果加入相应边;若条件为“非常量”,则将两条边都加入控制流队列。 +当语句结果标记更新时,将使用此结果的语句加入语句队列,这一点和 SCP 一致。 From 83e3b9dbcc4a7fa7ec4e5e134bb73de157457dfb Mon Sep 17 00:00:00 2001 From: DarkSharpness <2040703891@qq.com> Date: Sat, 14 Sep 2024 21:27:39 +0800 Subject: [PATCH 2/2] minor(sccp): use itemize to rewrite --- 30-optimize.tex | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/30-optimize.tex b/30-optimize.tex index 8da1567..5c2b4fc 100644 --- a/30-optimize.tex +++ b/30-optimize.tex @@ -350,8 +350,10 @@ \subsubsection{求支配集的其他方法} 以上的方法是通过迭代的方式求解支配集的。在控制流图节点数量较少的情况下,其时间复杂度是可以接受的。 然而,在某些极端的情况下,这种方法的时间复杂度是 $O(n^2)$ 的,可能会导致性能问题。以下给出两个求支配集的更高效的方法: -1. bitset 优化集合操作:注意到在迭代过程中,我们只需要对支配集进行交集、并集等操作,因此可以使用 bitset 位运算来加速这些操作。 -2. Lengauer-Tarjan 算法:该算法可以在接近线性的时间复杂度内求解支配集。 +\begin{itemize} + \item bitset 优化集合操作:注意到在迭代过程中,我们只需要对支配集 w 进行交集、并集等操作,因此可以使用 bitset 位运算来加速这些操作。 + \item Lengauer-Tarjan 算法:该算法可以在接近线性的时间复杂度内求解支配集。 +\end{itemize} 以上两种做法,均在 OI-wiki \url{https://oi-wiki.org/graph/dominator-tree/} 中有详细介绍,感兴趣的读者可以参考。 @@ -796,8 +798,11 @@ \subsection{基础 —— SCP} 初始时,假设所有值都有可能成为常量,并将每条语句的结果标记为“未定义”,而函数入参则被标记为“非常量”。 然后,将所有基本块内的语句加入队列,反复从中取出语句并尝试更新其标记。更新规则如下: -1. 如果语句的结果已标记为“非常量”,则不进行处理; -2. 否则,将语句中的所有使用(use)替换为当前标记,并尝试对结果进行估值。 + +\begin{itemize} + \item 如果语句的结果已标记为“非常量”,则不进行处理; + \item 否则,将语句中的所有使用(use)替换为当前标记,并尝试对结果进行估值。 +\end{itemize} 若无法估值(如 \texttt{load} 指令,其值取决于内存),则标记为“非常量”; 若根据算术规则可以确定结果(如 $0$ 乘任何数为 $0$,或 \texttt{add} 的两个操作数都是常量),则更新结果标记。 @@ -829,12 +834,14 @@ \subsection{改进 —— SCCP} 每轮迭代中,尝试从两个队列中取出元素进行处理: -- 若取出的是控制流队列的元素(即从块 $A$ 到 $B$ 的边),首先标记 $B$ 从 $A$ 来过。 +\begin{itemize} + \item 若取出的是控制流队列的元素(即从块 $A$ 到 $B$ 的边),首先标记 $B$ 从 $A$ 来过。 若 $B$ 已从 $A$ 来过,则跳过此操作。否则,遍历 $B$ 中的 \texttt{phi} 函数和控制流语句,并按语句队列的规则更新。 若 $B$ 是首次访问,还需遍历块中的其他语句,确保不遗漏任何内容。 遍历顺序为:\texttt{phi} 函数 -> (仅首次访问时,其他语句) -> 控制流语句。 -- 若取出的是语句队列的元素,则大致按 SCP 的规则更新,细节略有不同: + \item 若取出的是语句队列的元素,则大致按 SCP 的规则更新,细节略有不同: 对于 \texttt{phi} 函数,仅需检查当前所有已访问过的入口的值是否相同; 对于无条件跳转 \texttt{br} 语句,将“当前块”到“目标块”的边加入控制流队列; 对于条件跳转 \texttt{br} 语句,若条件为“未定义”,则不处理,若为“常量”,则根据条件结果加入相应边;若条件为“非常量”,则将两条边都加入控制流队列。 当语句结果标记更新时,将使用此结果的语句加入语句队列,这一点和 SCP 一致。 +\end{itemize}