Add General Apply Benchmark by Pastva & Henzinger
SSoelvsten opened this issue · comments
In [FMCAD], Pastva and Henzinger create a beautiful "average case" BDD experiment which I really would want to generalise as part of this BDD benchmarking collection. All of the BDDs they have generated can be found at [Zenodo].
Benchmark
- Given two (different?) paths to BDDs, deserialize said file (see below). Note, in lib-bdd index 0 is reserved for the false terminal and 1 for the true terminal.
- During deserialization, obtain other statistics of interest about the input.
- Number of internal nodes.
- Number of levels / variables.
- Width of the BDD.
- Number of arcs to each terminal.
- Number of single-parent (internal) nodes.
- Merge the list of variables to get a variable mapping.
- Use the Builders to construct the desired BDD.
- Combine the two BDDs with the desired operator (default: and).
Deserialization
As far as I can tell from [Zenodo] source files, the BDDs are provided in a binary format as it is serialized by the lib-bdd package; this package does not use complement edges which makes it perfect for our general case. A BDD node consists of a BddVariable
(uint16_t
) and two BddPointers
(uint32_t
), i.e. it serializes each BDD node as 2 + 2 • 4 = 10 bytes.
Here, each BddVariable
is serialized and deserialized as follows:
/// Read from little endian byte representation
pub(super) fn from_le_bytes(bytes: [u8; 2]) -> BddVariable {
BddVariable(u16::from_le_bytes(bytes))
}
Here, each BddPointer
is serialized and deserialized as follows:
/// Read from little endian byte representation
pub fn from_le_bytes(bytes: [u8; 4]) -> BddPointer {
BddPointer(u32::from_le_bytes(bytes))
}
These are then combined as follows (code taken from Pastva and Henzinger [Zenodo] as it shows how to handle terminals)
pub fn load(path: &str) -> NaiveBdd {
let file = File::open(path).unwrap();
let mut file = BufReader::with_capacity(1024 * 10, file);
let mut buf = [0u8; 10];
file.read_exact(&mut buf).unwrap(); // Zero node must be there always.
if file.read_exact(&mut buf).is_err() { // One node is also always there unless it's empty BDD.
return Self::mk_false();
}
let mut result = vec![BddNode::zero(), BddNode::one()];
loop {
match file.read_exact(&mut buf) {
Ok(_) => (),
Err(e) => {
if e.kind() == ErrorKind::UnexpectedEof {
return NaiveBdd {
root: result.len() - 1,
nodes: result
}
} else {
panic!("{:?}", e);
}
}
}
result.push(BddNode::new(
u16::from_le_bytes([buf[0], buf[1]]) as usize,
u32::from_le_bytes([buf[2], buf[3], buf[4], buf[5]]) as usize,
u32::from_le_bytes([buf[6], buf[7], buf[8], buf[9]]) as usize,
));
}
}
Note, the first 20 bytes are always used for the false (and then if it exists) the true terminals.
This should be easy to replicate with the adapter's builder functions together with a std::vector<...>
.
References
- [FMCAD] Samuel Pastva and Thomas Henzinger. “Binary Decision Diagrams on Modern Hardware”. In: Proceedings of the 23rd Conference on Formal Methods in Computer-Aided Design (2023)
- [Zenodo] https://zenodo.org/records/7958052