Shape Analysis学习

我的研究方向主要是程序验证与程序证明,证明的理论依据是C. A. R. Hoare提出的Hoare Logic,并加上指针和别名的处理,证明的方式则是Prove in code,即在程序点中间插入Assert,每个Assert中有多个公式表示当前程序点的性质(即,无论程序执行多少次,这些性质都必定成立)。新的公式可以通过原有的公式传播和推导两种方式证明。当程序的precondition(一组公式)成立的情况下,程序的postcondition(也是一组公式)也成立,并且所有的公式都能通过precondition以及程序本身得到证明,那么整个程序就证明成功。

对于某些数据结构,如链表,要证明这些数据结构上的操作,必须定义数据结构本身,我们通过递归函数做到这一点。另外,我们还需要全局属性来描述它们本身的性质。数据结构定义和全局属性的正确性由程序员自己保证,可以通过PVS等工具进行验证。我们假设这些信息是正确的。

因为加上了指针和别名,所以程序证明会显得比较复杂,有时我们为了传播一个公式,必须要证明很多与指针相关的细节。我们希望能够通过工具来处理这些指针细节,使得程序员在证明自己的程序的过程中,将精力更多地集中在关键的业务上。

数据流分析和Shape Analysis可以辅助我们做到这一点。首先,数据流分析能够通过程序本身,得出指针之间的关系;其次,Shape Analysis作为一种指针分析,可以预估指针指向的数据结构的形状(如链表、树和有向无环图等);最后,通过我们自定义的模式匹配,将前两者获取到的信息进行匹配,从而得出具体的信息。值得一提的是,这边所谓的具体信息,更侧重于集合层面上的意义,如NodeSet(head)==NodeSet(head)@i-makeSet(q)等类似集合操作的性质。

这篇文章主要是通过仔细阅读《Is it a Tree, a DAG, or a Cyclic Graph? A Shape Analysis for Heap-Directed Pointers in C》这篇论文,整理下自己对Shape Analysis的认识。

 

Shape Analysis的主要作用估计出指针指向的数据结构的形状(链表、树、有向无环图或有环图等),来提高依赖测试和提高编译器对程序进行并行优化的能力,当然,也为更复杂的堆内存分析提供一些指导。

 

Shape Analysis是指针分析的一种类型。一般的指针分析分为两个部分:1)栈内存分析;2)堆内存分析。在C语言中,栈内存的地址通过&操作符获取,而堆内存通过malloc()函数获取,函数返回值即为申请到的地址。

栈内存和堆内存的一个区别在于,栈内存上的指针在编译期间都有一个名字,由于有这个属性,所以可以通过一个类似于(p, x)的形式的points-to对来准确地描述栈上指针的关系,(p, x)表示指针变量p指向数据对象x,或者通过(*p, x)这种形式来表示*p是x的别名。

不幸的是,堆中的对象在它们的生命周期中并没有一个名字,因为动态分配的内存是内在匿名的。因此,一些人通过特定的模式来给这些对象命名,比如通过根据它们在程序中被分配的语句行数,或者进一步加上它们的procedure字符串,来区别不同的调用序列中同一个的语句分配的对象。这种命名本质上是为了通过分析栈对象的方式来分析堆内存对象。但这种方式有可能会给完全不相关的堆内存对象同样的名字,因为不能计算出准确的形状信息。

文章采取将栈内存和堆内存分开进行分析的方式。在他们实现的McCAT编译器中,他们根据待分析程序的复杂度,采取不同的方式分析指向堆内存的指针之间的关系:

1) 对于很少使用到堆内存的程序,采用level-0或者普通的指针分析;

2) 对于使用了一些堆内存分配的数组或者非递归结构的程序,使用level-1和connection analysis,这个分析主要用来辨别两个指向堆内存的指针是够指向同一个结构。通常,用C语言实现的科学计算程序会拥有这个特点,因为它们经常使用一些不相交的动态分配的数组。

3) 也是本文最关注的分析,即shape analysis,其目的是估算从一个指针可以访问到的数据结构的形状,比如是一个树状结构,或是一个DAG形状的结构,或是一个普通的包含环的图。更准确地说,是辨别出程序中构造出的无别名的Tree和DAG形状,否则提供保守的估计。形状分析主要是为那些主要采用递归数据结构或者递归数据结构混合数组的程序而设计的。Shape analysis得出的信息可以用来帮助编译器并行化程序,或者应用优化策略(如循环展开和指令流水线等)。

文章中提到,他们采用的抽象表示比其它多数shape analysis所用到的抽象要简单,并在真实的编译器上进行实现。

 

文章第2部分介绍分析规则。

Shape analysis中使用3种抽象,这3中抽象协同工作,并且在每个程序点中进行计算。对于某个指向堆内存的指针,估计出其形状属性;对于每对指针,估算出它们之间的direction关系和interference关系。这3种抽象定义如下:

定义2.1 给定任意的指向堆内存的指针p,当从p可以访问到的数据结构中任意两个节点(堆内存中的对象)之间只有一条唯一的路径,则p的形状属性p.shape为树;当数据结构中的任意两个节点之间存在不止一条路径,但是没有一条指向自己的路径,则p.shape为有向无环图;当数据结构中包含一个节点,且这个节点有一条指向自己的路径,则p.shape为环。其中,链表为树的一种特例。

定义2.2 给定任意两个指向堆内存的指针p和q,direction矩阵D表示了它们之间如下的关系:

  • D[p, q] = 1:存在一条访问路径,可以从p所指向的对象访问到q指向的对象。这种情况下,可以简单地说p有一条路径指向q。
  • D[p, q] = 0:从p指向的对象没有路径可以到达q指向的对象。

定义2.3 个顶任意两个指向堆内存的指针p和q,interference矩阵I表示了它们之间如下的关系:

  • I[p, q] = 1:从指针p和指针q,可以访问到同一个堆内存对象。这种情况下,称p和q相交(interfere)。
  • I[p, q] = 0:从指针p和指针q,不能访问到共同的堆内存对象。这种情况下,称p和q不相交。

其中direction关系实际用来估计形状信息,而interference关系用来安全的计算direction关系。

下面看个例子:

image

这个堆中的数据结构的direction矩阵和interference矩阵分别为:

image image

                direction矩阵                                      interference矩阵

从这个例子,我们可以有得出一个大体的认识:

1、direction矩阵是非对称的;

2、interference矩阵是对称的;

3、interference矩阵是direction矩阵的子集。

其中第2个属性可以用来节省存储空间,第3个属性表明了这样一个事实:如果存在从p到q的一条路径,则他们都可以访问到q所指向的对象。

下面来演示direction关系如何估计堆内存数据结构的形状。

image

上图中,p.shape和q.shape都是树。另外,D[q,p]是1,因为从q可以通过next字段到达p。语句p->next = q设置了从p通过prev字段到达q。从direction矩阵信息中我们知道,已经存在一条从q到p的路径,现在又加了一条从p到q的路径。因此我们可以推断p和q指向的堆内存对象之间产生了一个环。因此,在这个语句之后,D[p, q] = 1, D[q, p] = 1, p.shape = Cycle且q.shape = Cycle。

应当注意,一个指向堆内存对象的指针p,p.shape只能抽象出从p能够访问到的数据结构的形状。

这种形状信息为区分从p能够访问到的内存提供了重要信息。对于指针p,如果p.shape是树的话,则p->f和p->g将到达完全不相交的两颗子树(假设f和g是不同的字段)。如果p.shape是DAG,则p->f->f和p->g则可能指向相同的堆内存对象。

因此,Shape analysis的目标就是辨别出堆内存中的树和DAG,并将这种信息在分析的过程中保持得越长越好。下面将展示计算direction和interference矩阵的规则,并使用它们估计数据结构的形状。

posted @ 2012-12-13 11:02  tangzhnju  阅读(1069)  评论(0编辑  收藏  举报