CUDD学习总结

CUDD(Colorado University Decision Diagram)是一种操纵决策图的软件开发包,它提供函数来操纵二元决策图(BDDs,代数决策图(ADDs)和零压缩二元决策图(ZDDs)。CUDD包定义了决策图的数据结构和一些列操作决策图的算法:决策图的建立,遍历,排序,互相转换,决策图上通用算法,iteif then else)算法以及节点存储管理等。CUDD官网上为release 2.4.1版,github上为release3.0.0版。

1.CUDD的基本结构

CUDD中最重要的两个数据结构是DdManagerDdNode

1.1.DdManager

所有的BDDs都存储在一个指定的哈希表中,称为unique table。从unique table名字就能知道,unique table中的每一个数据都是唯一的。Unique Table和其他辅助的数据结构构造了DdManager

除了少量的静态计数,CUDD没有全局变量,所以,可以在一个应用里使用多个DdManager

1.1.1.Cudd_Init

使用Cudd_Init去创建DdManagerCudd_Init包含五个参数:

  1. numVarsBDDsADDs数量和的初始值,如果程序所需的变量数是已知的,那么使用这些变量创建一个DdManager是可以增加效率的,但是如果变量数未知,传入0或者其他下界,也是没有问题的;

  2. numVarsZZDDs的数量,目前不使用;

  3. numSolts:确定唯一表的每个子表的初始大小。每个变量都有一个子表。每个子表的大小会动态调整,以反映节点的数量。这个参数通常可以使用默认值,即CUDD_UNIQUE_SLOTS

  4. cacheSize:它是缓存的初始大小(条目数)。默认值为CUDD_CACHE_SLOTS

  5. maxMemory缓存将增长到的最大大小,与命中率或唯一表的大小无关。如果maxMemory设置为0,CUDD会根据可用内存猜测一个好的值.

典型的创建DdManager如下:

manager = Cudd_Init(0,0,CUDD_UNIQUE_SLOTS,CUDD_CACHE_SLOTS,0);

 

注意:要回收内存,需要使用Cudd_Quit,在退出之前(调用DdManager的类的析构函数中)使用。

1.1.2.DdNode

DdNodeBDDs的核心结构体,定义如下

struct DdNode {
    DdHalfWord index;        /**< variable index */
    DdHalfWord ref;        /**< reference count */
    DdNode *next;        /**< next pointer for unique table */
    union {
        CUDD_VALUE_TYPE value;    /**< for constant (terminal) nodes */
        DdChildren kids;    /**< for internal nodes */
    } type;            /**< terminal or internal */
};
  1. Index是节点的唯一索引,类似sat中的变量索引。

  2. Ref存储这个变量的引用计数,每次调用Cudd_Ref时,引用计数就会+1,调用Cudd_Recursive_Deref,引用计数就会-1。当引用计数为0时,DdNode被声明为dead,之后会被回收。

  3. 所有的DdNode节点所代表的变量在Unique table中连接在一起,next代表下一个DdNode的指针。

  4. 每一个DdNode都包含了一个值和子节点的指针,存储在type中。

2.常用函数

2.1. BDD中,逻辑0和常数0是不同的!

常数1(由Cudd_ReadOne返回)对BDDsADDsZDDs是通用的。然而,它对ADDBDDs以及ZDDs的意义是不同的。由常数1节点组成的图仅代表ADDBDDs的常数1功能。对于ZDDs,其含义取决于变量的数量。它是所有变量的补数的结合。反过来说,常数1函数的表示也取决于变量的数量。$n$变量的常数1函数由Cudd_ReadZddOne返回。

常数0ADDZDD是通用的,但对BDD不是。BDD逻辑0与常数0函数没有关系。它是通过常数1的补数(Cudd_Not)获得的。(它也由Cudd_ReadLogicZero返回。)所有其他常数都是专门针对ADD的。

获得常数10的方法如下:

_one = Cudd_ReadOne(manager);
_zero = Cudd_Not(_one);

2.2.创建变量

通过三个函数可以创建BDD变量:

DdNode * Cudd_bddIthVar(

DdManager * dd,

int  i

)

返回带有索引的变量,如果没有传i的值,那么会自动创建一个。

DdNode * Cudd_bddNewVar(

DdManager * dd

)

创建一个新的BDD变量。新变量的索引等于之前最大的索引加1。如果成功,返回一个指向新变量的指针;否则返回NULL

DdNode * Cudd_bddNewVarAtLevel(

DdManager * dd,

int  level

)

创建一个新的BDD变量。新变量的索引等于之前最大的索引加1,并且在顺序中被定位在指定的级别。如果成功,返回一个指向新变量的指针;否则返回NULL

2.3.BDD if-then-else操作

BDD常见的操作可以通过调用Cudd_bddIte来实现。下表为Cudd_bddIte函数。

DdNode *
Cudd_bddIte(
  DdManager * dd /**< manager */,    
  DdNode * f /**< first operand */,
  DdNode * g /**< second operand */,
  DdNode * h /**< third operand */)
{
DdNode *res;

    do {
    dd->reordered = 0;
    res = cuddBddIteRecur(dd,f,g,h);
    } while (dd->reordered == 1);
    if (dd->errorCode == CUDD_TIMEOUT_EXPIRED && dd->timeoutHandler) {
        dd->timeoutHandler(dd, dd->tohArg);
    }
return(res);

} /* end of Cudd_bddIte */

Cudd_bddIte可以实现所有二元函数。但是CUDD包内同样包含了其他布尔函数的封装,诸如Cudd_bddAnd

例如,布尔函数f = x0' * x1' * x2' * x3';

DdManager *manager;
DdNode *f, *var, *tmp;
int i;
...

f = Cudd_ReadOne(manager);
Cudd_Ref(f);
for (i = 3; i >= 0; i--) {
    var = Cudd_bddIthVar(manager,i);
    tmp = Cudd_bddAnd(manager,Cudd_Not(var),f);
    Cudd_Ref(tmp);
    Cudd_RecursiveDeref(manager,f);
    f = tmp;
}

上述代码需要注意以下几点:

  1. 在求解过程中的中间变量是需要通过Cudd_Ref函数和Cudd_RecursiveDeref函数对DdNoderef“+1”“-1”的。然而,新创建的var是一个投影函数,它的引用计数总是大于0,因此,不用调用Cudd_Ref。(todo:这里的逻辑需要看一下cuddUniqueInter

  2. 新的f必须分配给一个临时变量(本例中为tmp)。如果Cudd_bddAnd的结果直接分配给f,即 f = Cudd_bddAnd(manager,Cudd_Not(var),f);旧的f就会丢失,而且没有办法释放其节点。

  3. 下面表格中的两段代码是等价的,但是右边更加高效,因为它的tmp可以被复用。

f = tmp;

f = tmp;

Cudd_Ref(f);          

 Cudd_RecursiveDeref(manager,tmp);

    4.通常情况下,"自下而上 "地构建BDD是更有效的。这就是为什么循环从30的原因。然而,请注意,在变量重新排序后,更高的索引并不一定意味着 "更接近底部"

   5.如果我们想在重新排序后仍以自下而上的方式连接这些变量,我们应该使用Cudd_ReadInvPerm。不过,我们必须小心,在进入循环之前要固定连接的顺序。否则,如果发生重排,就有可能两次使用一个变量而跳过另一个变量。

还有许多CUDD已经封装好的布尔函数,在cuddBddIte.c文件中,这里不再赘述。

2.4.变量的重新排序

  1. 通过调用Cudd_ReduceHeap来手动重新排序,或者当节点数量到达给定的阈值之后会自动触发(该阈值在每次初始化后重新调整)。另一方面,要启用自动动态重排需要调用Cudd_AutodynEnable,自动动态重排也可以通过Cudd_AutodynDisable中断。

  2. 重排方法通过Cudd_ReduceHeapCudd_AutodynEnable传参适配。称这些方法为收敛方法,对变量的相对位置施加约束并重新排序。每个方法都由CUDD .h (CUDD包的外部头文件)中定义的枚举类型Cudd_ReorderingType常量标识,具体的重排方法如下:

  3. CUDD_REORDER_NONE:不重新排序

  4. CUDD_REORDER_SAME:如果传递给Cudd_AutodynEnable,此方法将保持当前用于自动重新排序的方法不变。如果传递给Cudd_ReduceHeap,这个方法会导致使用当前的自动重新排序方法。

  5. CUDD_REORDER_RANDOM:随机选择变量对,并按顺序交换。(交换是通过对相邻变量的一系列交换来完成的。)

  6. CUDD_REORDER_RANDOM_PIVOT:与CUDD_REORDER_RANDOM差不多。不过两个变量的选择是依据:第一个变量在最大节点数变量之上(不清楚是索引还是什么),第二个变量在最大节点数变量之下。如果有多个节点绑定到了最大节点数,则选取最接近根的那个。

  7. CUDD_REORDER_SIFT:该方法基于R. Rudell.Dynamic; variable ordering for ordered binary decision diagrams; In Proceedings of the International Conference on Computer-Aided Design, pages 42-47, Santa Clara, CA, November 1993.筛选的描述如下:变量按顺序上下移动,以便找到所有可能占据的位置。确定最佳位置,并将变量返回到该位置。大体如此,但实际上还有更多可以设置的操作。

  8. CUDD_REORDER_SYSTEM_SIFT:该方法基于S. Panda, F. Somenzi, and B. F. Plessier; Symmetry detection and dynamic variable ordering of decision diagrams; In Proceedings of the International Conference on Computer-Aided Design, pages 628-631, San Jose, CA, November 1994. 该算法和CUDD_REORDER_SIFT类似,增加了如下操作:筛选过程中,相邻变量被测试为对称性,如果是它们对称的,它们就被连接在一个组中。然后继续筛选一组。而不是单个变量。在对称筛选运行之后,可以通过Cudd_SymmProfile来报告。

2.5.定义顺序不变

CUDD允许在重排过程保持一组变量的顺序连续,或这保持其相对顺序。对顺序的约束是通过树来指定的,通过以下两种方式:

  1. 通过调用Cudd_MakeTreeNode

  2. 通过调用MTR库(CUDD的外部包)的函数,并使用Cudd_SetTreemanager注册,可以使用Cudd_ReadTree来读取注册的树。

2.6.输出blif文件

使用Cudd_DumpBlif函数将构建的图写入一个blif格式文件。仅限于BDD

使用Cudd_DumpDot生成绘图程序dot的输入文件。仅限于BDDADD

3.深入理解Reference Counts

一方面Cudd_Ref容易忽略,却掌管何时销毁DdNode的功能。另一方面,自己在使用过程中总觉得这里会出现问题。所以详细了解这部分

官网网址:http://web.mit.edu/sage/export/tmp/y/usr/share/doc/polybori/cudd/node8.html

Garbage collection in the CUDD package is based on reference counts. Each node stores the sum of the external references and internal references. An internal BDD or ADD node is created by a call to cuddUniqueInter, an internal ZDD node is created by a call to cuddUniqueInterZdd, and a terminal node is created by a call to cuddUniqueConst. If the node returned by these functions is new, its reference count is zero. The function that calls cuddUniqueIntercuddUniqueInterZdd, or cuddUniqueConst is responsible for increasing the reference count of the node. This is accomplished by calling Cudd_Ref.

CUDD的回收机制依赖于Reference Count,每个DdNode都存储着内部Reference和外部Reference的总和。内部的BDDNode通过函数cuddUniqueInter,并且一个Terminal Node通过CuddUniqueConst创建。通过这两个函数创建的DdNodereference值初始为0。通过调用Cudd_Ref函数,reference count会增加。

 

When a function is no longer needed by an application, the memory used by its diagram can be recycled by calling Cudd_RecursiveDeref (BDDs and ADDs) or Cudd_RecursiveDerefZdd (ZDDs). These functions decrease the reference count of the node passed to them. If the reference count becomes 0, then two things happen:

  • The node is declared ``dead;" this entails increasing the counters of the dead nodes. (One counter for the subtable to which the node belongs, and one global counter for the unique table to which the node belongs.) The node itself is not affected.

  • The function is recursively called on the two children of the node.

在应用程序不再需要某个函数时,其图所使用的内存可以通过调用Cudd_RecursiveDeref回收。Cudd_RecursiveDeref会减少DdNodereference count,如果reference count减少至0,会发生两件事:

  • DdNode被声明为dead,表示dead node数量的变量增加(一个计数器用于节点所属的子表,一个计数器用于节点所属的唯一表),节点本身不受影响;

  • 函数被node的两个孩子节点递归调用。

 

For instance, if the diagram of a function does not share any nodes with other diagrams, then calling Cudd_RecursiveDeref or Cudd_RecursiveDerefZdd on its root will cause all the nodes of the diagram to become dead.When the number of dead nodes reaches a given level (dynamically determined by the package) garbage collection takes place. During garbage collection dead nodes are returned to the node free list.

When a new node is created, it is important to increase its reference count before one of the two following events occurs:

In practice, it is recommended to increase the reference count as soon as the returned pointer has been tested for not being NULL.

 

例如,如果一个函数的图没有与其他图共享任何节点,那么在其根部调用Cudd_RecursiveDerefCudd_RecursiveDerefZdd将导致该图的所有节点死亡。当死节点的数量达到一个给定的水平(由软件包动态决定),就会进行垃圾收集。在垃圾收集过程中,死节点被返回到节点自由列表中。

 

当一个新的节点被创建时,重要的是在以下两个事件之一发生之前增加它的引用计数。

 

  • cuddUniqueIntercuddUniqueInterZddcuddUniqueConst的调用,或对可能最终导致对它们调用的函数的调用。

  • Cudd_RecursiveDerefCudd_RecursiveDerefZdd的调用,或对可能最终导致调用它们的函数的调用。

 

在实践中,建议在返回的指针经过判断不为NULL后,立即增加引用计数。

 

 

3.1.NULL Return Value

The interface to the memory management functions (e.g., malloc) used by CUDD intercepts NULL return values and calls a handler. The default handler exits with an error message. If the application does not install another handler, therefore, a NULL return value from an exported function of CUDD signals an internal error.

If the aplication, however, installs another handler that lets execution continue, a NULL pointer returned by an exported function typically indicates that the process has run out of memory. Cudd_ReadErrorCode can be used to ascertain the nature of the problem.

An application that tests for the result being NULL can try some remedial action, if it runs out of memory. For instance, it may free some memory that is not strictly necessary, or try a slower algorithm that takes less space. As an example, CUDD overrides the default handler when trying to enlarge the cache or increase the number of slots of the unique table. If the allocation fails, the package prints out a message and continues without resizing the cache.

CUDD使用的内存管理函数(如malloc)的接口拦截NULL返回值并调用一个处理程序。默认的处理程序以错误信息退出。因此,如果应用程序没有安装另一个处理程序,那么CUDD导出的函数的NULL返回值就会发出内部错误信号。

然而,如果应用程序安装了另一个处理程序,让执行继续进行,那么导出的函数返回的NULL指针通常表明进程已经耗尽了内存。Cudd_ReadErrorCode可以用来确定问题的性质。

一个测试结果为NULL的应用程序可以尝试一些补救措施,如果它耗尽了内存。例如,它可以释放一些严格意义上不需要的内存,或者尝试一种较慢的算法,以减少占用空间。作为一个例子,CUDD在试图扩大缓存或增加唯一表的槽数时,会覆盖默认的处理程序。如果分配失败,软件包会打印出一条信息,然后继续进行,而不调整缓存的大小。

 

3.2.Cudd RecursiveDeref vs Cudd Deref

It is often the case that a recursive procedure has to protect the result it is going to return, while it disposes of intermediate results. (See the previous discussion on when to increase reference counts.) Once the intermediate results have been properly disposed of, the final result must be returned to its pristine state, in which the root node may have a reference count of 0. One cannot use Cudd_RecursiveDeref (or Cudd_RecursiveDerefZdd) for this purpose, because it may erroneously make some nodes dead. Therefore, the package provides a different function: Cudd_Deref. This function is not recursive, and does not change the dead node counts. Its use is almost exclusively the one just described: Decreasing the reference count of the root of the final result before returning from a recursive procedure.

通常情况下,递归过程必须保护它要返回的结果,同时处理中间的结果。(参见前面关于何时增加引用计数的讨论。)一旦中间结果被适当处理,最终结果必须返回到原始状态,在这种状态下,根节点的引用计数可能为0。我们不能使用Cudd_RecursiveDeref(或Cudd_RecursiveDerefZdd)来达到这个目的,因为它可能错误地使一些节点死亡。因此,该软件包提供了一个不同的函数。Cudd_Deref。这个函数不是递归的,也不改变死节点的数量。它的用途几乎只有刚才描述的那种。在从递归过程返回之前减少最终结果的根的引用计数。

 

3.3.When Increassing the Reference Count is Unnecessary

When a copy of a predefined constant or of a simple BDD variable is needed for comparison purposes, then calling Cudd_Ref is not necessary, because these simple functions are guaranteed to have reference counts greater than 0 at all times. If no call to Cudd_Ref is made, then no attempt to free the diagram by calling Cudd_RecursiveDeref or Cudd_RecursiveDerefZdd should be made.

当需要一个预定义常数或简单的BDD变量的副本进行比较时,就没有必要调用Cudd_Ref,因为这些简单的函数保证在任何时候都有大于0的引用计数。如果没有调用Cudd_Ref,那么就不应该试图通过调用Cudd_RecursiveDerefCudd_RecursiveDerefZdd来释放图。

 

3.4.Saturating Increments and Decrements

On 32-bit machines, the CUDD package stores the reference counts in unsigned short int's. For large diagrams, it is possible for some reference counts to exceed the capacity of an unsigned short int. Therefore, increments and decrements of reference counts are saturating. This means that once a reference count has reached the maximum possible value, it is no longer changed by calls to Cudd_RefCudd_RecursiveDerefCudd_RecursiveDerefZdd, or Cudd_Deref. As a consequence, some nodes that have no references may not be declared dead. This may result in a small waste of memory, which is normally more than offset by the reduction in size of the node structure.

When using 64-bit pointers, there is normally no memory advantage from using short int's instead of int's in a DdNode. Therefore, increments and decrements are not saturating in that case. What option is in effect depends on two macros, SIZEOF_VOID_P and SIZEOF_INT, defined in the external header file (cudd.h). The increments and decrements of the reference counts are performed using two macros: cuddSatInc and cuddSatDec, whose definitions depend on SIZEOF_VOID_P and SIZEOF_INT.

32位机器上,CUDD包以无符号短int的形式存储参考计数。对于大型图表,一些参考计数有可能超过无符号短int的容量。因此,参考计数的递增和递减是饱和的。这意味着一旦引用计数达到可能的最大值,就不再通过调用Cudd_RefCudd_RecursiveDerefCudd_RecursiveDerefZddCudd_Deref来改变它。因此,一些没有引用的节点可能不会被宣布死亡。这可能会导致少量的内存浪费,但这通常会被节点结构大小的减少所抵消。

当使用64位指针时,在DdNode中使用短int而不是int,通常没有内存优势。因此,在这种情况下,增量和减量是不饱和的。什么选项是有效的取决于两个宏,SIZEOF_VOID_PSIZEOF_INT,定义在外部头文件(cudd.h)中。参考计数的增量和减量是通过两个宏进行的:cuddSatInccuddSatDec,它们的定义取决于SIZEOF_VOID_PSIZEOF_INT。

 

4.深入理解The Unique Table

A recursive procedure typically splits the operands by expanding with respect to the topmost variable. Topmost in this context refers to the variable that is closest to the roots in the current variable order. The nodes, on the other hand, hold the index, which is invariant with reordering. Therefore, when splitting, one must use the permutation array maintained by the package to get the right level. Access to the permutation array is provided by the macro cuddI for BDDs and ADDs, and by the macro cuddIZ for ZDDs.

递归过程通常通过对最上面的变量进行扩展来分割操作数。在这里,最顶层指的是在当前变量顺序中最接近根的变量。另一方面,节点持有索引,它在重新排序时是不变的。因此,在拆分时,必须使用包所维护的包数组来获得正确的层次。BDDADD的宏cuddIZDD的宏cuddIZ提供了对互换数组的访问。

The unique table consists of as many hash tables as there are variables in use. These has tables are called unique subtables. The sizes of the unique subtables are determined by two criteria:

  • The collision lists should be short to keep access time down.

  • There should be enough room for dead nodes, to prevent too frequent garbage collections.

唯一表由同样多的哈希表组成,因为有变量在使用。这些哈希表被称为唯一子表。唯一子表的大小是由两个标准决定的。

  • 碰撞列表应该很短,以减少访问时间。

  • 应该有足够的空间给死节点,以防止过于频繁的垃圾回收。

While the first criterion is fairly straightforward to implement, the second leaves more room to creativity. The CUDD package tries to figure out whether more dead node should be allowed to increase performance. (See also Section 3.4.) There are two reasons for not doing garbage collection too often. The obvious one is that it is expensive. The second is that dead nodes may be reclaimed, if they are the result of a successful cache lookup. Hence dead nodes may provide a substantial speed-up if they are kept around long enough. The usefulness of keeping many dead nodes around varies from application to application, and from problem instance to problem instance. As in the sizing of the cache, the CUDD package adopts a ``reward-based" policy to decide how much room should be used for the unique table. If the number of dead nodes reclaimed is large compared to the number of nodes directly requested from the memory manager, then the CUDD package assumes that it will be beneficial to allow more room for the subtables, thereby reducing the frequency of garbage collection. The package does so by switching between two modes of operation:

  • Fast growth: In this mode, the ratio of dead nodes to total nodes required for garbage collection is higher than in the slow growth mode to favor resizing of the subtables.

  • Slow growth: In this mode keeping many dead nodes around is not as important as keeping memory requirements low.

 

虽然第一个标准是相当直接的实现,但第二个标准给创造性留下了更多的空间。CUDD包试图弄清楚是否应该允许更多的死节点来提高性能。(参见第3.4节。)不经常做垃圾收集有两个原因。显而易见的是,它很昂贵。第二个原因是死节点可以被回收,如果它们是一个成功的缓存查询的结果。因此,如果死节点被保留足够长的时间,它们可以提供一个可观的速度。保留许多死节点的用处因应用而异,也因问题实例而异。如同缓存的大小,CUDD包采用了一个 "基于奖励 "的政策来决定多少空间应该用于唯一表。如果回收的死节点数量与直接向内存管理器请求的节点数量相比很大,那么CUDD包假定为子表留出更多的空间将是有益的,从而减少垃圾收集的频率。该软件包通过在两种操作模式之间切换来实现这一点。

  • 快速增长。在这种模式下,垃圾收集所需的死节点与总节点的比例高于慢速增长模式,以利于调整子表的大小。

  • 慢速增长。在这种模式下,保持许多死节点并不像保持低内存需求那样重要。

 

Switching from one mode to the other is based on the following criteria:

  • If the unique table is already large, only slow growth is possible.

  • If the table is small and many dead nodes are being reclaimed, then fast growth is selected.

This policy is especially effective when the diagrams being manipulated have lots of recombination. Notice the interplay of the cache sizing and unique sizing: Fast growth normally occurs when the cache hit rate is large. The cache and the unique table then grow in concert, preserving a healthy balance between their sizes.

 

从一种模式切换到另一种模式是基于以下标准的。

  • 如果唯一的表已经很大了,就只能缓慢增长。

  • 如果表很小,而且有很多死节点被回收,那么就选择快速增长。

当被操作的图有很多重组的时候,这个策略就特别有效。注意缓存大小和唯一大小的相互作用。快速增长通常发生在缓存命中率很高的时候。缓存和唯一表会协同增长,保持它们之间的健康平衡。

 

 

 

posted on 2021-06-21 13:23  QzZq  阅读(2874)  评论(3)    收藏  举报

导航