Staged Flow-Sensitive Pointer Analysis

这篇blog中会提到point-to set, point-to graph,point-to information等概念,基本上都是指同一个东西。可表示为1个map,key为top-level varaible或者address-taken variable(后面会介绍概念),value为address-taken variable集合。表示程序中变量的指向集合。

1.Introduction

  • Flow-Insensitive指针分析特点:(1).忽略语句顺序、(2).所有program point共享一个solution。具体规则参考

  • Flow-Sensitive:(1).考虑control flow、(2).每个program point有一个solution。

传统IFDS-based解法是沿着ICFG遍历,对于大型程序,CFG中可能有数10万个node,每个node需要维护2个point-to graph: 对应incoming information和outgoing information。每个point-to graph可能有数10万个指针变量,每个指针可能指向数千个对象,因此效率非常低下。

Solution: staged pointer-to analysis (简称SFS)

  • 1.auxiliary pointer analysis(后面简称AUX): 保守计算def-use信息。

  • 2.primary analysis(简称PA): 利用AUX计算的def-use信息构建sparse value flow graph(SVFG),在SVFG上进行flow-sensitive分析。

Challenges:

  • 1.AUX必须在精度和性能之间取得良好的平衡:如果结果太不精确,那么第1步生产的SVFG的稀疏性就会受到影响;如果AUX性能开销大,则PA将永远无法执行。

  • 2.PA必须能够同时处理AUX的保守的def-use信息,同时保持流敏感指针分析的精度。

  • 3.PA必须有效管理由AUX计算的大量def-use链。

Contributions:

  • 1.提出了SFS方法。

  • 2.引入了access equivalence的概念,它将 def-use 链划分为等价类,从而允许 SFS 有效地处理大量的 def-use 信息。

  • 3.用一个flow-, context-insensitive的inclusion-based (Andersen style)的AUX进行evaluation。benchmark包括16 open-source程序,能够在14min内分析1.9M Loc的程序。

    • 对于小型程序(那些少于100K LOC的程序),SFS依然很高效,但与baseline相比没有提供任何性能优势。

    • 对于中型程序(具有100K 到 400K LOC的程序),SFS比baseline快5.5倍。

    • 对于大型程序(具有超过 800K LOC的程序),SFS可以完成分析,而baseline则不能。

2.Background

2.1.Flow-Sensitive Pointer Analysis

传统Flow-Sensitive Analysis沿着CFG(ICFG)用transfer function进行遍历,直到达到fix point。transfer function为:

I N k = ∪ x ∈ p r e d ( k ) O U T x O U T k = G E N k ∪ ( I N k − K I L L k ) IN_k = \mathop{\cup}\limits_{x \in pred(k)} OUT_x \\ OUT_k = GEN_k \cup (IN_k − KILL_k) INk=xpred(k)OUTxOUTk=GENk(INkKILLk)

k k k 为一个CFG node, x x x 为一个前驱node, I N IN IN 代表incoming pointer information, O U T OUT OUT 代表outgoing pointer information, G E N GEN GEN K I L L KILL KILL 分别表示新生成的和覆盖的pointer information。

  • 对于赋值语句 k : x = y k: x = y k:x=y K I L L k = { x → _ } KILL_k = \{x \rightarrow \_ \} KILLk={x_},这是strong update。

  • 对于 k : ∗ x = y k: *x = y k:x=y,情况就比较复杂。

    • 如果 x x x 一定指向内存区域 z z z,那么 K I L L k = { z → _ } KILL_k = \{z \rightarrow \_ \} KILLk={z_},依旧可以进行strong update。

    • 如果 x x x 可能指向好几块内存区域,那么 K I L L k = { } KILL_k = \{\} KILLk={},此时进行的是weak update。这是种保守策略。

flow-sensitive的分析关键步骤就是kill,也就是strong update,flow-insensitive(Andersen)的做法则是weak update(不断进行增量分析,会引入很多误报)。

任何指针分析的一个重要方面是堆建模,它将概念上无限大小的堆抽象为一组有限的内存位置。作者采用常见的做法,将每个静态内存分配点(memory allocation site)视为不同的抽象内存位置(在程序执行期间可能映射到多个具体内存位置)。

2.2.SSA

在静态单赋值(SSA)形式中,每个变量在程序中只被定义一次。SSA形式非常适合执行稀疏分析,因为它明确表示了def-use信息,并允许数据流信息直接从变量定义传递到其相应的使用点。

由于C/C++存在通过指针访问的address-taken variable,将程序转换为SSA形式变得更加复杂,这些间接定义和使用只能通过指针分析来发现。由于由此产生的指针信息是保守的,每个间接定义和使用实际上都是一个可能的定义或使用。为此作者采用了 [2] 中Memory SSA,用 χ \chi χ (CHI) 和 μ \mu μ (MU) 来表示address-taken variable的defs和uses。

  • 对于 store 指令 *x = y。Memory SSA会为其添加标注 v = χ ( v ) v = \chi(v) v=χ(v) v v v 表示可能会被该 store 指令define的变量。

  • 对于 load 指令 x = *y。Memory SSA会为其添加标注 μ ( v ) \mu(v) μ(v) v v v 表示该 load 指令访问的变量。

在转换为Memory SSA形式时,每个 χ \chi χ 函数被视为给定变量的def和use,而每个 μ \mu μ 函数被视为给定变量的use。

为了避免处理 loadstore 的复杂性,一些现代编译器(如GCC和LLVM)使用一种被称为partial SSA的变体。其思路是将变量分为两类。一类包含从不被指针引用的top-level variable,因此它们的def和use可以被轻松确定,而不需要指针信息。对于这些变量,可以使用任何构建SSA形式的算法将它们转换为SSA。第二类包含那些可以被指针引用的变量(address-taken variable),为了避免上述的复杂性,这些变量不会被转换为SSA形式。

相关文档:stackoverflow, handout09

2.3.LLVM IR

在LLVM中,top-level variable保存在一个概念上无限的虚拟寄存器集合中,这些寄存器保持SSA形式。address-taken variable则保存在内存中而不是寄存器中,并且它们不处于SSA形式。top-level variable通过 alloca(内存分配)和 copy 指令进行修改。address-taken variable通过 loadstore 指令访问,这些指令使用top-level variable作为参数。address-taken variable在IR中从不被直接引用,而是通过这些 loadstore 指令间接引用。LLVM指令使用三地址格式,因此每条指令最多只有一级指针解引用(具有多级间接引用的源语句通过引入临时变量简化为这种形式)。

下图为paper中的示例代码,左边是非SSA形式的代码,右边是转换后的partial SSA形式的IR(不是实际编译出来的)。(注意: 这个程序只能又来进行说明,实际上源代码中所有被声明的变量都会存在内存中,包括示例中的 a, b, c, d,因此都属于address-taken variable,top-level variable由于没有实际分配内存只存在于编译后的IR的中间变量中)

左边源代码中存在 abcd 4个address-taken variable,w, x, y, z 4个top-level variable。示例右边的IR中address-taken variable仅通过 loadstore 访问(包括 c = 0 被编译成 store 0 y1,通过 y 进行访问),top-level variable则都是SSA形式(y = &cy = &d 分别成了 y1 = ALLOCcy2 = ALLOCd)。

请添加图片描述

3.Related Work

这里主要说下SFS和semi-sparse analysis [ 3 ] ^{[3]} [3] 区别,semi-sparse analysis对top-level variable执行稀疏分析,同时对address-taken variable使用迭代数据流分析(听起来是IFDS-based方法)。相比之下,SFS是完全稀疏的,并且对所有变量都使用SSA形式。最终的分析比semi-sparse analysis在可扩展性上提高了一个数量级。

4.Staging The Analysis

作者这里选择了一个flow-, context-insensitive inclusion-based (Andersen style)指针分析作为AUX,只要AUX是sound的,SFS也将是sound的,并且它的精度至少与传统的流敏感指针分析相当。AUX的精度主要影响主要分析的稀疏性(从而影响其性能)。

这一部分主要介绍基于AUX构造sparse value flow graph(def-use graph)的过程。

4.1.Sparse Flow-Sensitive Pointer Analysis

这一步主要用到的数据结构就是def-use graph (DUG),最大的挑战即是为address-taken variable计算def-use graph,下图为一个假想的代码片段对应的CFG(应该不是完整代码片段),右侧是AUX计算出的每个top-level variable对应的point-to set。CFG中只包含 storeload 操作。

请添加图片描述

首先,SFS需要将该CFG转化成Memory SSA形式,即插入 χ \chi χ μ \mu μ 函数。以 *p = ws = *z 为例。p 指向 {a},因此插入 a 1 = χ ( a 0 ) a_1 = \chi(a_0) a1=χ(a0)z 指向 {a, b, c, d},因此插入 μ ( a 2 ) , μ ( b 2 ) , μ ( c 0 ) , μ ( d 2 ) \mu(a_2), \mu(b_2), \mu(c_0), \mu(d_2) μ(a2),μ(b2),μ(c0),μ(d2)。下图则是完整的Memory SSA形式的CFG。

请添加图片描述
由AUX计算的def-use信息相对于SFS将要计算出的计算出精确的flow-sensitive def-use信息来说更加保守,因此对基于AUX计算出的Memory SSA中当遇到带有标注 v m = χ ( v n ) v_m = \chi(v_n) vm=χ(vn)store 命令 *x = y(也就是AUX计算出 x 指向 v),需要考虑以下三种可能性:

  • 1.SFS计算后 x 可能不指向 v。在这种情况下, v m v_m vm 应该是 v n v_n vn 的一个copy,并且不包含 y 的任何信息。也就是 p t s ( v m ) ⊇ p t s ( v n ) pts(v_m) \supseteq pts(v_n) pts(vm)pts(vn)

  • 2.SFS计算后 x 可能只指向 v 1个address-taken variable。在这种情况下,分析算法可以对 v 的points-to information 进行strong update。换句话说, v m v_m vm 应该是 y 的copy,并且不包含 v n v_n vn 的任何信息。也就是 p t s ( v m ) ⊇ p t s ( y ) pts(v_m) \supseteq pts(y) pts(vm)pts(y)

  • 3.SFS计算后 x 除了 v 还会指向别的address-taken variable。在这种情况下,分析算法必须对 v 的pointer-to information进行weak update。换句话说, v m v_m vm 应该包含来自 v n v_n vny 的指向信息。也就是 p t s ( v m ) ⊇ p t s ( v n )    ;    p t s ( v m ) ⊇ p t s ( y ) pts(v_m) \supseteq pts(v_n) \; ; \; pts(v_m) \supseteq pts(y) pts(vm)pts(vn);pts(vm)pts(y)

构造后的def-use graph如下图所示,每条边都标注了address-taken variable,这里简单说明下:

  • p = w 连接到 *r = y 的边表示 a 1 = χ ( a 0 ) a_1 = \chi(a_0) a1=χ(a0) a 2 = χ ( a 1 ) a_2 = \chi(a_1) a2=χ(a1) 的def-use关系。

  • *q = x 连接到 u = *v 的两条边分别表示 e 1 = χ ( e 0 ) e_1 = \chi(e_0) e1=χ(e0) μ ( e 1 ) \mu(e_1) μ(e1) f 1 = χ ( f 0 ) f_1 = \chi(f_0) f1=χ(f0) μ ( f 1 ) \mu(f_1) μ(f1) 的def-use关系。

请添加图片描述

4.2.Access Equivalence

可以看到前面的DUG中有非常多的边,这是由于每个 load/store 可能访问上千个address-taken variable。边太多了显然会影响性能。因此这里作者提出访问等价(access equivalence)的概念。

两个address-taken variable如果每次都被同时 load/store,那么就是访问等价。为了计算访问等价关系,作者用到了一个map A E AE AE,key为address-taken variable,value为指令集合,表示访问了该address-taken variable的所有指令。如果 A E ( x ) = A E ( y ) AE(x) = AE(y) AE(x)=AE(y),那么 x x x y y y 访问等价。

上面示例中的访问等价组包括:{a}, {b, d}, {c}, {e, f }。压缩边集合的DUG如下图所示

请添加图片描述

4.3.Interprocedural Analysis

有两种方式将这一步的def-use analysis扩展为跨函数分析:

方式一:为每个函数单独计def-use graph,保守地将call instruction视为对callee function中所有被定义变量的def点以及所有被使用变量的use点。此方法可能会使得callee中很多 store/load 指令会连接到callsite,因此的缺点是边的数量可能太多影响稀疏性。不过Pinpoint貌似解决了这个问题并采用这个思路。

方式二:也是作者选择的方式,是将整个程序作为一个整体来计算def-use graph,直接跨函数连接变量def和use。其中一个重要考量是间接调用。一些跨越多个函数的def-use chain可能依赖于间接调用分析。给定的分析并没有解决这个问题——它假设def-use chain只依赖于指令所使用的指针的point-to set,而没有考虑到函数指针的额外依赖关系。换句话说,如果AUX计算的调用图对流敏感指针分析计算的调用图进行了过拟合,那么这种技术可能会失去精度。

间接调用问题有两种解决方案。(1).简单地假设AUX计算了一个精确的调用图,即与流敏感指针分析计算的调用图相同,这也是作者采用的方法。(2).每个跨函数的依赖于间接调用的def-use edge会用 <function pointer,target function> 对进行标注。只有当目标函数已被计算为函数指针的指向集的一部分时,指针信息才会跨越这个def-use edge传播。

5.Final Algorithm

5.1.Main Part

完整的算法为:

第一步进行pre-analysis,构造sparse value-flow graph (def-use graph),包括:

  • 1.用AUX进行指针分析,用指针分析的结果求解间接调用并构造ICFG。对于function call,为callsite实参和每个callee形参添加 copy 边。返回值和callsite间添加 copy 边。

  • 2.为top-level variables进行SSA转换,这一步理论上LLVM可以完成。

  • 3.进行访问等价分析,对于每个partition (等价组) P,对 P 中任意变量的 store 标注为 P = χ ( P ) P = \chi(P) P=χ(P)load 标注为 μ ( P ) \mu(P) μ(P),随后构造完整的Memory SSA。

  • 4.为每个pointer-related instruction以及 Φ \Phi Φ 函数构造DUG node并进一步添加edge:

    • 对于 alloc, copy, load 结点 N N N,对于每个使用了 N N N 的top-level variable node M M M,添加边 N → M N \rightarrow M NM

    • 对于 store 结点 N N N,标注了 P n = χ ( P m ) P_n = \chi(P_m) Pn=χ(Pm),那么对于使用 P n P_n Pn 的每个node M M M,添加边 N → P M N \stackrel{P}{\rightarrow} M NPM

    • 对于 Φ \Phi Φ 结点 N N N,其定义了 P n P_n Pn,那么对于使用 P n P_n Pn 的每个node M M M 添加边 N → M N \rightarrow M NM

可以看到存在两种边,没有标记的和标记了address-taken variable的,带标记的边从 store 指令出发,指向其use,这里的use点由于使用了address-taken variable一定是 store 或者 load。因此带标记的边一定是从 storeload/store

def-use graph构造好后就进行flow-sensitive pointer analysis,主算法如下:

  • w o r k l i s t worklist worklist 初始化为所有 alloc node。

  • P G PG PG 为所有top-level variable的point-to set, P t o p ( v ) P_top(v) Ptop(v) 为top-level variable v v v 的point-to set,相当于 P G PG PG 的子集。这里 P G PG PG 相当于一个flow-insensitive Andersen算法的结果。

  • 每一个 load node都包含一个point-to graph I N k IN_k INk 保存该node所有可能访问的address-taken variables。 P k ( v ) P_k(v) Pk(v) 表示 I N k IN_k INk 中的address-taken variable v v v 对应的point-to set。

  • 每一个 store node包含2个point-to graph保存该node所有可能define的address-taken site。 I N k IN_k INk 保存incoming information, O U T k OUT_k OUTk 保存outgoing information。 P k ( v ) P_k(v) Pk(v) I N k IN_k INk 中的address-taken variable v v v 对应的point-to set。 p a r t ( v ) part(v) part(v) v v v 对应的访问等价组。

可以看到相比flow-insensitive,这里既有全局point-to set P G PG PG,又有每个program的point set I N k IN_k INk O U T k OUT_k OUTk (只在 store 中用到)。

请添加图片描述
请添加图片描述
请添加图片描述

5.2.Comparsion with Andersen-style

这里贴上Andersen算法作为对比 (不考虑field-sensitivity):

  • 相同之处在于,SFS在处理 alloc, copy 时方式几乎一样。都会更新 P t o p ( x ) P_{top}(x) Ptop(x)

  • 不同之处:

    • 首先Andersen的worklist算法没有考虑 store, copy 等指令执行的顺序。不过仅仅是这个不同那么算法收敛的时候全局point-to set P G PG PG 起始是一样的。

    • SFS除了 P G PG PG 外还用 I N IN IN O U T OUT OUT 保存流敏感指针分析结果(只保存address-taken variable), I N k IN_k INk O U T k OUT_k OUTk 保存指令 k k k 的incoming和outgoing信息( k k k 只能是 store 或者 load)。

    • 处理 load 指令 x = *ystore 指令 *x = y 时,Andersen算法会添加 copy 边而不是直接更新 P G PG PG。SFS处理 load 指令 k k k 时访问了 I N k IN_k INk 并更新了 P G PG PG,处理 store 指令 k k k 时会应用transfer function更新 O U T k OUT_k OUTk,并更新DUG后继 load/store/Phi node的 I N IN IN 集合,将成功更新的node加入worklist。

请添加图片描述

5.3.指针分析结果及使用

SFS的指针分析结果包括 P G PG PG (针对top-level variable) 和 I N IN IN/ O U T OUT OUT (针对address-taken variable)。从源代码的角度来说,因为所有源代码中声明的变量都是address-taken variable,top-level variable都是LLVM IR中间生成变量,因此最重要的结果就是 I N IN IN/ O U T OUT OUT

6.Evaluation

作者拿SFS和semi-sparse analysis(SSO) [ 3 ] ^{[3]} [3] 进行对比,指标为时间和内存开销,benchmark信息如下表所示

请添加图片描述

运行时间和内存开销如下表所示,SFS的时间开销包括3部分: AUX指针分析、DUG构造、flow-sensitive指针分析。可以看到SFS相比SSO优势主要体现在分析大型和中等大小的程序上。

请添加图片描述
其中分析时间即使对于大小相近的benchmark可能也有很大差异。一些较小的benchmark测试花费的时间比较大的benchmark要长得多。一个benchmark测试的分析时间取决于许多因素,而不仅仅是程序大小原始大小:所涉及的point-to set的大小;def-use graph的特性,这决定了指针信息传播的广度;worklist算法与pointer analysis的交互方式;等等。如果不知道这些信息(只能通过指针分析过程来收集),就很难预测分析时间。

参考文献

[1].Hardekopf B, Lin C. Flow-sensitive pointer analysis for millions of lines of code[C]//International Symposium on Code Generation and Optimization (CGO 2011). IEEE, 2011: 289-298.

[2].Chow F, Chan S, Liu S M, et al. Effective representation of aliases and indirect memory operations in SSA form[C]//Compiler Construction: 6th International Conference, CC’96 Linköping, Sweden, April 24–26, 1996 Proceedings 6. Springer Berlin Heidelberg, 1996: 253-267.

[3].Hardekopf B, Lin C. Semi-sparse flow-sensitive pointer analysis[J]. ACM SIGPLAN Notices, 2009, 44(1): 226-238.

要查看Git中已经添加(staged)的文件的修改差异,可以使用git diff --staged命令。这个命令会比较缓存区(staged)中的文件与最新的提交之间的差异。具体的步骤如下: 1. 确保你已经将需要比较的文件添加到缓存区(staged)中,可以使用git add命令将文件添加到缓存区。 2. 运行git diff --staged命令来比较缓存区中的文件与最新的提交之间的差异。这会显示出被修改的部分以及具体的差异内容。 请注意,--staged参数可以被--cached替代,它们的作用是一样的。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [git常用命令](https://blog.csdn.net/u010700415/article/details/9039735)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [Git add添加暂存区(staged)的文件的修改差异查看方法](https://blog.csdn.net/qq_37037348/article/details/131224616)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值