synchronized. Our library, that we called BeeDeeDee, implements this syn- chronization in accessing shared data, while maintaining a speed comparable to other libraries. Before delving into the synchronization details, we start with analyzing how the library is structured.
4.4 Library Structure
Information about BDD nodes are stored in a unique table, implemented with a simple integer array ut. This choice was mandatory to achieve acceptable performance and memory consumption. Indeed, storing node data in a Java object occupies more memory and produces many dead objects during the life of the application, leading to greater pressure on the Java garbage collector. Additionally, it destroys memory locality, since data for a node can be any- where in the heap, instead of in contiguous cells of an array. Good memory locality is necessary to exploit high-speed processor caches, and therefore to achieve good performance. These issues, together with indirection and object creation overhead, would produce poor performance compared to state-of-the- art BDD packages.
Currently in our implementation a node in the table spans over 5 cells, a total of 20 bytes of memory. An auxiliary array H contains pointers (integer indexes) to nodes having a specic hash value. Together, ut and H form an hash table. Collision resolution is achieved through chaining in the unique table itself, via an integer component pointing to the next node in the table. The hash value is constructed using the variable number and the indexes of the low and high branches. This increases the size to 24 bytes per node. The rst few lines of the ut table look like these:
Index variable number low high next hcAux
0: 2147483646 -1 -1 1 0 1: 2147483647 -1 -1 2 1 2: 0 0 1 3 2 3: 1 0 1 4 3 4: 2 0 1 5 4 5: 3 0 1 6 5 . . .
The variable numbers of the terminals need to be larger than any other vari- able number, because some library algorithms rely on this for their correct- ness. Low and high children are identied by their index on the table; terminal nodes have no children, and so the elds are set to −1. The next eld points to the next node in the collision chain. The last eld, hcAux, is a unique iden- tier of the node, that acts as the hash code for a BDD object. It remains the same during the life of the application, even after a garbage collection deletes nodes and compacts the table (see 4.4.1). This is to ensure that operations on hash based structures (e.g. a HashMap or HashSet) are consistent in time.
As already said, uniqueness in the node table is implemented by accessing a node in expected constant time via hashing. Due to the fact that BDDs are canonical forms of Boolean formulas, a node represents a class of equivalent formulas. So a BDD is univocally identied by its index in the table. Clients of the library do not see this internal representation, but rather obtain from the factory a BDD java object pointing to the right starting node of the diagram in the table.
The BDD interface and the Factory abstract class form the main public in- terface of the package. Clients can create a factory of BDD objects by invoking the static method Factory.mk(utSize, cacheSize), that takes as parame- ters the initial size of the unique table, in number of nodes, and the size of the operation caches, in number of entriesrefer to Section 4.4.2. References to basic BDD objects created by the Factory can be obtained by calling the methods
• makeZero() and makeOne()to get a reference to the terminal BDDs 0
and 1
• makeVar(int) and makeNotVar(int)that, given the desired variable in-
dex, return a BDD representing a single variable, respectively in positive and negative form, creating the corresponding single node if necessary. 4.4.1 Internal Garbage Collection
Having all of the BDD nodes stored in a single array prevents the Java Garbage Collector to free memory no more used for a BDD object. So we implemented a garbage collection procedure that compacts the node table, overwriting dead nodes. To nd live nodes, instead of counting references to every node, we implemented a mark and sweep collection. Every user BDD object is consid- ered live until its free() method gets called by the programmer. Every node reachable recursively from these high level objects is then considered live as well. Nodes excluded from this pass (mark) are thus dead, and so their mem- ory can be reclaimed (sweep). Compacting the node table during the garbage collection yields the additional benet of improving memory locality. Garbage collection is started either when the occupation of the table exceeds 98%, or on user's request.
4.4.2 Operation Caches
Operations such as Apply can be very expensive. The traditional approach to get better performance is to maintain all operation results computed so far in a hash table with collision resolution. A small hash-based cache, keeping only the last result for each position in the table, has proven to be the fastest solution, due to the cleaner implementation and the fact that it exploits the temporal locality of operations, as well as the spatial locality of processor caches. Currently our package implements caches for Apply, Quantifica- tion, Replace and Restrict.