Санкт-Петербургский Государственный Университет Математико-механический факультет Кафедра системного программирования Реализация библиотеки диаграмм двоичных решений на языке OCaml Дипломная работа студента 544 группы Иванова Дмитрия Аркадьевича Научный руководитель Рецензент “Допустить к защите” зав. кафедры .................. /подпись/ .................. /подпись/ .................. /подпись/ к.ф.-м.н. Д.Ю. Булычев д.ф.-м.н., проф. А.Н. Терехов д.ф.-м.н., проф. А.Н. Терехов Санкт-Петербург 2006 Содержание Введение 1 1 Обзор 1.1 Математические основы BDD . . . . . . . . . . . . . . . . 1.1.1 Основные понятия . . . . . . . . . . . . . . . . . . . 1.1.2 BDD, сокращенные BDD и разделяемые сокращенные BDD . . . . . . . . . . . . . . . . . . . 1.1.3 Дифференциальные BDD . . . . . . . . . . . . . . . 1.1.4 Операции над BDD . . . . . . . . . . . . . . . . . . 1.1.5 Представление множеств и отношений . . . . . . . 1.2 Существующие реализации BDD . . . . . . . . . . . . . . 1.2.1 BuDDy . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.2 CUDD . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 . . . . . . . 5 9 10 15 16 16 16 . . . . . . . . . . . . . . 17 17 17 18 19 19 20 22 23 32 36 36 36 36 38 3 Примеры использования 3.1 Задача о расстановке ферзей . . . . . . . . . . . . . . . . . . 3.2 Японские кроссворды . . . . . . . . . . . . . . . . . . . . . . 3.3 Поиск вхождения образца в граф . . . . . . . . . . . . . . . 38 38 40 41 Заключение 45 Список литературы 46 2 Библиотека 2.1 Обзор библиотеки . . . . . . . . . . . . . . . . . . 2.1.1 Обзор модулей . . . . . . . . . . . . . . . . 2.1.2 Компиляция . . . . . . . . . . . . . . . . . 2.1.3 Абстракции . . . . . . . . . . . . . . . . . . 2.1.4 Начало работы . . . . . . . . . . . . . . . . 2.1.5 Алгоритм для бинарных операций . . . . 2.1.6 Возможности библиотеки . . . . . . . . . . 2.2 Интерфейс к BDD . . . . . . . . . . . . . . . . . . 2.3 Интерфейс к отношениям . . . . . . . . . . . . . . 2.4 Особенности реализации . . . . . . . . . . . . . . 2.4.1 Физическое представление . . . . . . . . . 2.4.2 Кэширование операций . . . . . . . . . . . 2.4.3 Ненадежные переименование и кванторы 2.4.4 Сборка мусора . . . . . . . . . . . . . . . . 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Введение Многие задачи анализа программ требуют большое количество хранимой информации. Например, контекстно-зависимый анализ указателей, как отмечено в [7], требует 1014 байтов для полного хранения всевозможных стеков вызовов в больших программах. Разумеется, невозможно решать такие задачи с использованием классических программых представлений множеств в виде битовых шкал. Для представления больших множеств и отношений можно использовать BDD. Для это множество или отношение надо представить через дизъюнктивную нормальную форму логического выражения (см. раздел 1.1.5). Кроме того, хорошие реализации BDD позволяют эффективно производить операции над множествами. BDD особенно удобны, когда необходимо хранить и обрабатывать большое количество числовых данных. Впервые концепция диаграмм двоичных решений (Binary Decision Diagram, BDD) как средства для представления булевских функций и логических формул была введена Ли (Lee) в 1959 году; обрела известность благодаря Акерсу (Akers) в 1978 году. Эффективные алгоритмы были исследованы и опубликованы Браянтом (Bryant) в Университете Карнеги-Меллона (Carnegie Mellon University) в 1986 году. Ключевыми нововведениями были использование фиксированного порядка логических переменных и разделение подграфов между разными BDD. Современное понимание BDD, а именно — сокращенная упорядоченная диаграмма двоичных решений (Reduced Ordered Binary Decision Diagram, ROBDD), было введено Брэйсом, Руделом и Браянтом (Brace, Rudell, Bryant) в 1990 году. Данная работа ставила своей целью эффективную реализацию библиотеки BDD на языке Objective Caml. Кроме классических ROBDD был реализован экспериментальный вид диаграмм — дифференциальные BDD (Differential Binary Decision Diagram,∆BDD), введенный в [8] в 1995 году. Библиотека должна была быть интергрирована в проект PRANLIB (PRogram ANalisys LIBrary) [5]. В качестве примера использования библиотеки в анализе программ была решена задача по поиску вхождений образца в граф (см. раздел 3.3). 2 1 1.1 Обзор Математические основы BDD В этом разделе мы опишем BDD с математической точки зрения, познакомимся с различными видами BDD, а также увидим, как производить операции над ними и выражать множества и отношения через логические формулы. Основные идеи и определения взяты из [1] и [2]. 1.1.1 Основные понятия Для того, чтобы быть едиными в терминологии, предлагается следующий список ключевых понятий: Доменом будем называть любое конечное множество. Отношением называется подмножество прямого произведения множеств N 1 × N2 × · · · × N k , а число k называется размерностью или арностью этого отношения. Отношение называется бинарным, если его размерность равна двум. Отношения и множества, с которыми мы будем работать, будут иметь конечное число элементов. Функцией f , действующей из A в B (обозначение f : A → B), называется подмножество бинарного отношения A × B, такое, что ∀x ∈ A ∃! y ∈ B : (x, y) ∈ A × B (знак “!” означает единственность). A называется областью определения функции, а B — областью значений. Если A ⊆ N1 × N2 × · · · × Nk , to N1 будем называть первым доменом области определения f , N2 — вторым и т.д. Булевской функцией называется функция f : n → , где = {0, 1} Логической формулой или логическим выражением будем называть синтаксическую конструкцию, образованную по следующему принципу (далее приводится грамматика в форме Бэкуса-Наура): C ::= 1 | 0 логическая константа V ::= x1 | x2 | · · · | xn логическая переменная E ::= | V | ¬E | ( E ) | E ∨ E | E ∧ E |E ⇐ E|E⇒ E|E ⇔ E | ∀V E | ∃V E логическая формула 3 Замечание 1.1. В дальнейшем будем обозначать логические выражения литерой e (например, e1 , e2 , ...), логические переменные — литерой x (например, x1 , x2 , ...), а константы — литерой c. Свободным вхождением переменной x в e называется любое вхождение, которое не ограничено объемлющим квантором с кванторной переменной равной x. Пример 1.2. В выражении x ∧ ∃x(x ∨ y) свободно будет только первое вхождение x. Как видно, мы немного расширяем логику высказываний введением кванторов. Любому логическому выражению можно однозначно сопоставить булевскую функцию очевидным образом. При этом переменной x1 будет соответствовать первый домен области определения булевской функции, x2 — второй и т.д. Функцию, сопоставленную выражению e, будем обозначать f e . Интерпретацией будем называть функцию I : {x1 , ..., xn } → Функцию I часто записывают как [I(x1 )/x1 , ..., I(xn )/xn ]. . Значением выражения e при интерпретации I будем называть f e (I(x1 ), I(x2 ), ..., I(xn )) и будем обозначать как e[I(x1 )/x1 , I(x2 )/x2 , ..., I(xn )/xn ]. Сужением на (x = c) называется логическое выражение, полученное из e подстановкой логической константы c во все свободные вхождения логической переменной x, и обозначается e[c/x]. Композицией e и (e0 ) по переменной x будем называть логическое выражение, полученное из e подстановкой логического выражения e0 во все свободные вхождения логической переменной x, и будем обозначать e[e0 /x]. Логические выражения e1 и e2 называются эквивалентными (e1 ≡ e2 ), если f e1 ≡ f e2 . Логическое выражение e называется общезначимым, если f e ≡ 1. Логическое выражение e называется выполнимым (satisfable), если ¬(f e ≡ 0). Конъюнктом называется логическое выражение вида (V |¬V ) ∧ (V |¬V ) ∧ · · · ∧ (V |¬V ), т.е. формула, записанная как конъюнкция переменных или их отрицаний. 4 Пример 1.3. x1 ∧ ¬x2 ∧ x4 — конъюнкт. 1.1.2 BDD, сокращенные BDD и разделяемые сокращенные BDD Чтобы дать определение BDD, нам потребуются ещё две нестандартные конструкции матлогики: Определение 1.4. if-then-else оператором называется синтаксическое расширение логических выражений. Новое выражение записывается e → e1 , e2 и семантически эквивалентно (e ⇒ e1 ) ∧ (¬e ⇒ e2 ). Определение 1.5. if-then-else нормальной формой булевской функции (сокращенно, INF) называется форма записи логического выражения, которая соответствует грамматике: E ::= C | V → E, E. Пример 1.6. Выражение x ∧ y ∧ ¬z в INF будет выглядеть как x → (y → (z → 0, 1), 0), 0. Теорема 1.7. Закон раскрытия Шэннона (Shannon expansion’s Law). e ≡ x → e[1/x], e[0/x] Следствие 1.8. Любое логическое выражение можно записать в INF. Итак, теперь мы готовы определить диаграммы двоичных решений. BDD есть ни что иное, как INF логического выражения, представленная в виде графа. Определение 1.9. Binary Decision Diagram — это направленный, ацикличный, корневой граф t = (V, E) такой, что: • все дуги делятся на два равных множества One и Zero; • есть две специальных вершины: 0-терминал и 1-терминал, представляющие тождественные истину и ложь. Они называюся терминальными. Это единственные вершины, которые не имеют выходящих рёбер, т.е. ∀(v1 , v2 ) ∈ E v1 6= 1-терминал ∧ v1 6= 0-терминал; • ∀v ∈ V \ {0, 1} ∃!(v1 , v2 ) ∈ One : v1 = v ∧ ∃!(v10 , v20 ) ∈ Zero : v10 = v, т.е. любая вершина, кроме терминальной, имеет ровно две исходящие дуги: одна из множества One, а другая из Zero. Потомки данной вершины называются one(x) и zero(x) соответственно; 5 • Все вершины, кроме терминальных, помечены элементом из множества X = {x1 , ..., xn } (множество переменных). Эта метка называется переменной вершины v и обозначается var(v). Замечание 1.10. Будем обозначать root(t) корневую вершину BDD t. Мы получили довольно широкий класс графов. Для эффективного хранения и манипуляций с данными надо наложить на BDD некоторые ограничения, т.е. сузить этот класс. Будем считать, что на множестве X задан линейный порядок “<”. Определение 1.11. Упорядоченной диаграммой двоичных решений (Ordered Binary Decision Diagram, OBDD) называется такая BDD, что на любом пути (v1 , ..., vk ), помеченном переменными (xv1 , ..., xvk ), соблюдается линейный порядок “<”, т.е. ∀i, j : i < j (xvi < xvj ). Определение 1.12. Разделяемой сокращенной упорядоченной диаграммой двоичных решений (Shared Reduced Ordered Binary Decision Diagram, SROBDD) называется такая OBDD, что для любых нетерминальных вершин v1 , v2 ∈ V \ {0, 1} : • one(v1 ) 6= zero(v1 ); • Если (one(v1 ) = one(v2 ) ∧ zero(v1 ) = zero(v2 ) ∧ var(v1 ) = var(v2 )), то v1 = v2 . Кроме того, все BDD разделяют между собой вершины. Замечание 1.13. Вышеозначенные условия называются свойствами минимальности ROBDD. Замечание 1.14. Слово “разделяемая” обычно опускают. Таким образом, SROBDD превращается в ROBDD. Замечание 1.15. Чаще всего, когда говорят BDD, имеют в виду именно ROBDD. Кроме того, в алгоритмах отождествляют BDD и её корневую вершину. Логическая формула, соответствующая BDD с корневой вершиной v записыватеся ev и определяется рекурсивно следующим образом: • если v = 0, то ev = 0; • если v = 1, то ev = 1; 6 • иначе ev = var(v) → eone(v) , ezero(v) . Пример 1.16. На рисунке 1 изображены два логических выражения, представленные в виде BDD, OBDD и ROBDD соответственно: e1 = (x1 ∧ x2 ∧ x3 ) ∨ (¬x1 ∧ ¬x3 ∧ ¬x2 ) и e2 = (x1 ⇒ (x3 ∧ x2 )) ∧ (¬x1 ⇒ (x2 ∨ ¬x2 )) Рис. 1: Представление выражений в виде BDD, OBDD и ROBDD Замечание 1.17. При изображении BDD приняты нескольно правил: • Все вершины, кроме терминальных, изображаются кругами, а терминальные — квадратами; 7 • Внутри вершины пишется номер переменной, которой помечена эта вершина; • Дуги из множества One рисуются сплошными линиями, а из множества Zero— штрихованными. В изображениях BDD для предыдущего примера корневые вершины были помечены дополнительно метками e1 и e2, чтобы понять, какая вершина какому BDD соответствует. Лемма 1.18. Единственности [1]. Для любой булевской функции f : n → при заданном линейном порядке переменных “<”, таком что x1 < x2 < ... < xn , существует единственная ROBDD с корневой вершиной v, такая что для любой интерпретации I ev [I(x1 )/x1 , ..., I(xn )/xn ] = f (I(x1 ), ..., I(xn )). BDD, соответствующее логическому выражению e, будем записывать как bdd(e), а BDD, соответствующее булевской функции f , как bdd(f ). Можно отметить то, что проверка общезначимости,выполнимости и эквивалентности осуществляется за одну операцию сравнения: • e общезначимо ⇔ root(bdd(e)) — 1-терминал; • e выполнимо ⇔ root(bdd(e)) — любая вершина, кроме 0-терминала; • e1 ≡ e2 ⇔ root(bdd(e1 )) = root(bdd(e2 )). Заметим, что если выражение находится в КНФ, то проверка общезначимости — NP-полная задача. Проверка выполнимости для ДНФ тоже сложная задача (co-NP). В ROBDD проверка действительно осуществляется за одну операцию сравнения, однако построение выражения в худшем случаем может занять эспоненту по времени от количества переменных. Заметим, что размер ROBDD может сильно варьироваться в зависимости от порядка переменных (см. рис. 2). Таким образом возникает еще одна задача — поиск оптимального порядка переменных. Однако, как показывает практика, при решении определенных задач лучше изначально задать некий порядок переменных, а не перестраивать его на ходу, используя эвристики, как делает, например, BuDDy [3]. Обычно вершина ROBDD содержит переменную, как целое число. Линейный порядок на переменных в этом случае задается отношением “<” на целых числах. 8 Рис. 2: ROBDD для (x1 ⇔ x2 ) ∧ (x3 ⇔ x4 ) ∧ (x5 ⇔ x6 ) (x1 ⇔ x4 ) ∧ (x2 ⇔ x5 ) ∧ (x3 ⇔ x6 ) 1.1.3 и Дифференциальные BDD Дифференциальные BDD (∆BDD) — способ сжатия BDD, несколько отличный от ROBDD, появившийся относительно недавно [8] и являющийся экспериментальным. Авторы замечают, что сжимает ∆BDD подчас лучше, а время работы чуть дольше, чем у ROBDD. Однако, дифференциальные BDD гораздо менее чувствительны к порядку переменных. Для перехода от OBDD к ∆BDD мы заменяем метку в вершине var(v) на разницу между var(v) и var(parent(v)). Новую метку вершины будем обозначать L(v). Свойства минимальности ROBDD остаются. Рис. 3 показывает переход от OBDD к ROBDD и к ∆BDD. На рисунке процесс показан в два этапа: сначала меняем все метки-переменные на разности, а потом убираем дублирующиеся вершины. Однако, ∆BDD может и увеличить число вершин относительно ROBDD. Это происходит, когда какая-то вершина в ROBDD является потомком нескольких вершин с разными метками-переменными. Рис. 4 показывает такую ситуацию. Лемма 1.19. [8] 9 Рис. 3: Переход от OBDD к ∆BDD 1. Единственность. При фиксированном порядке переменных любая булевская функция имеет единственное представление ввиде ∆BDD. 2. Выигрыш. При фиксированном порядке переменных уменьшение числа вершин при переходе от OBDD к ∆BDD может быть экспоненциальным. 3. Проигрыш. В худшем случае увеличение числа вершин может быть min(n2 , m), где n — количество вершин, а m — количество переменных. 1.1.4 Операции над BDD В этом подразделе будет показано, как можно строить BDD из логических выражений и эффективно производить операции различного рода: бинарные, кванторные, решать задачу выполнимости (Satisfability, SAT). Представленные алгоритмы — это операции над ROBDD. В конце подраздела будет описано, как легко можно написать схожие алгоритмы для ∆BDD. Для начала мы полагаем, что все вершины лежат в массиве или в таблице T , ключ которой — это целое число. Будем считать, что вершины занумерованы натуральными числами. Тогда в качестве значения T (v) хранит тройку (var(v), one(v), zero(v)). Этого вполне достаточно для представления графа, однако для эффективной работы нам потребуется обратная таблица H : (var, one, zero) 7→ v. 10 Рис. 4: Плохой случай для ∆BDD. Слева направо: OBDD, ROBDD, ∆BDD Мы добавляем очередную вершину только в случае, если она раньше не встречалась. Это проверяется по таблице H. Следующий алгоритм отвечает за добавление вершины и проверяет, что свойства минимальности ROBDD выполнены. Insert (var, low, high) : Node = if low = high then return low else u := H(var, low, high) if u <> null then return u else v := generate_next_node() add (v -> (var, low, high)) into T add ((var, low, high) -> v) into H return v endif endif Теперь будет показано, каким образом из логического выражения 11 построить BDD. Следующий алгоритм предполагает, что у нас на входе есть логическая формула e и определен порядок переменных x1 < x2 < ... < xn . Для построения ROBDD необходимо запустить рекурсивную функцию Build(e,1): Build(e, i) : Node = if i > n then if t = 1 then return terminal-1 else return терминал-0 endif else one := Build(e[1/x(i)], i+1) zero := Build(e[0/x(i)], i+1) var := i return Insert(var, low, high) endif В худшем случае количество вершин может возрасти экспоненциально от числа переменных. Однако обычно, на практических примерах, выбирая правильный порядок переменных, удается избежать эспоненциального взрыва за счет хэширования. Следущий шаг заключается в эффективной реализации бинарных операций. Классическим считается следующий алгоритм: /*вспомогательная функция; сужение e(v) на vr=1*/ opt_one(v, x) : Node = if var(v) = x then one(v) else v /*вспомогательная функция; сужение e(v) на vr=0*/ opt_zero(v, x) : Node = if var(v) = x then zero(v) else v /*основная рекурсивная процедура/ Apply(v1, v2, op):Node = v := D(v1, v2, op); if v <> null then return v else if isTerminal(v1) && isTerminal(v2) then v := op(v1, v2) else x := min(var(v1), var(v2)) v := insert(x, 12 Apply(opt_one(v1,x), opt_one(v2,x), op), Apply(opt_zero(v1,x), opt_zero(v2,x), op)) add ((v1,v2,op) -> v) into D return v endif Здесь таблица D выступает в роли кэша операций. Если реализовывать алгоритм без нее, то произойдет экспоненциальный взрыв, как не трудно заметить. Чтобы этого избежать, применяется динамическое программирование. Алгоритм принимает на вход две BDD и бинарную операцию, после чего рекурсивно “спускается” до терминалов. Работа алгоритма основана на следствии закона раскрытия Шэннона : t1 op t2 ≡ x → (t1 [1/x] op t2 [1/x]), (t1 [1/x] op t2 [0/x]) Кроме того, важной операцией является сужение e[b/x]. Оно описывается следующей рекурсивной функцией: Restrict(v,x,b):Node u := D(v,x,b); if (u <> null) then return u else if x < var(v) then return x else if x = var(v) then if b=true then return one(v) else return zero(v) endif else /* x > var(v)*/ u:= Insert(var(v), Restrict(one(v), x, b), Restrict(zero(v), x, b)); add ((v,x,b) -> u) into D return u endif Как видно из алгоритма, мы снова использовали здесь динамическое программирование. Теперь определим кванторы и суперпозицию. Их можно выразить через описанные выше операции (хотя на практике кванторы лучше реализовывать отдельными функциями): • ∃x e ≡ e[0/x] ∨ e[1/x] • ∀x e ≡ e[0/x] ∧ e[1/x] 13 • e[e0 /x] ≡ e0 → e[1/x], e[0/x] В [1] считается, что, хотя в худшем случае бинарные операции работают за O(n + m), где n и m — количество вершин в ROBDD, в среднем это время составляет O(n + m). Осталось описать алгоритмы для определения интерпретаций, на которых значение логического выражения, соответствующее BDD, истинно. Вот три стандартные операции: • SatAny(v) — выполняет ev ; выдает произвольную итерпретацию, которая • SatAll(v) — выдает все итерпретации, которые выполняют ev ; • SatCount(v) — выдает число выполняющих интерпретаций. Все алгоритмы основаны на рекурсивном обходе дерева. Подробнее об алгоритмах для этих операций можно узнать, например, в [2]. Для ∆BDD все алгоритмы выглядят похожим образом. Единственное отличие заключается в том, что мы должны передавать дополнительный параметр в процедуру, так как в вершинах хранится разность переменных L(v) вместо var(v). Здесь приводится модифицированный алгоритм Restrict, который следует вызывать Restrict(v,x,b,0) Restrict(v,x,b, pred):Node u := D(v,x,b); if (u <> null) then return u else if x < L(v)+pred then return x else if x = L(v)+pred then if b=true then return one(v) else return zero(v) endif else /* x > L(v)+pred*/ u:= Insert(L(v), Restrict(one(v), x, b, L(v)+pred), Restrict(zero(v), x, b, L(v)+pred)); add ((v,x,b) -> u) into D return u endif Как видно, он отличается от алгоритма для ROBDD только тем, что переменная не хранится, а вычисляется. Однако, этот незначительный проигрыш во времени компенсируется размером занимаемой памяти. Остальные алгоритмы строятся по аналогии. 14 1.1.5 Представление множеств и отношений Любое конечное множество можно выразить через логическое выражение. Пусть T — конечное множество {t(1) , ..., t(n) }. Тогда сопоставим каждому t(i) ∈ T кортеж логических констант (i) n b ∈ {0, 1} , i ∈ {1..m}, так что ∀i, j ∈ {1..m}, i 6= j b(i) 6= b(j) , т.е. все кортежи различны. Очевидно, что нам хватит n = log2 dme для такого представления (запись числа в двоичной системе счисления). Пусть фукнция f сопоставляет паре (b, i) ∈ {0, 1} × {1..n} логическое выражение xi , если b = i — иначе. Тогда формула, Wm0 иV¬x (i) n соответствующая T , выглядит так: i=1 j=1 f (bj , j). Будем записывать это выражение eT . Заметим, что полученное выражение находится в ДНФ. Пример 1.20. Пусть T = {t(1) , t(2) , t(3) }, m = 3, n = log2 dme = 2. Определим b : b(1) = (0, 0), b(2) = (0, 1), b(3) = (1, 0). Тогда, T e = (¬x1 ∧ ¬x2 ) ∨ (¬x1 ∧ x2 ) ∨ (x1 ∧ ¬x2 ) Теперь выразим через логическую формулу произвольное конечное отношение R ⊆ {N1 , N2 , ..., Nk }. Каждому элементу (i) (i)(1) (i)(k) отношения t = (t , ..., t ), i ∈ {1..m} сопоставим набор кортежей b(i) = (b(i)(1) , ..., b(i)(k) ), где b(i)(l) ∈ {0, 1}nl , i ∈ {1..m}, l ∈ {1..k}. Заметим, что как и в случае с множествами nl = log2 dNl e. Теперь определим набор функций f (l) , l ∈ 1..k, каждая из которых (l) сопоставляет паре (b, i) ∈ {0, 1} × логическое выражение xi , если b = (l) 0 и ¬xi — иначе. соответствующая R, выглядит так: WmТогда Vk Vnl формула, R (l) (i)(l) , j). Будем записывать это выражение e . i=1 l=1 j=1 f (bj Пример 1.21. Пусть N1 = {u1 , u2 , u3 }, N2 = {u01 , u02 }, R = {t(1) , t(1) }, где t(1) = (u1 , u02 ), t(2) = (u2 , u01 ). Вычислим длины: m1 = 3, m2 = 2 ⇒ n1 = 2, n2 = 1. Теперь определим набор кортежей b следующим образом: b(1) = ((0, 1), 0), b(1) = ((0, 0), 1), основываясь на двоичном разложении номеров элементов в множествах N1 и N2 (нумеруя с нуля). Тогда (1) (1) (2) (1) (1) (2) eR = (¬x1 ∧ x2 ∧ ¬x1 ) ∨ (¬x1 ∧ ¬x2 ∧ x1 ) Как видно из примеров, представление множеств и отношений через логические выражения основано на двоичном разложении числа. 15 1.2 1.2.1 Существующие реализации BDD BuDDy BuDDy[3] является, пожалуй, наиболее известной и используемой реализацией BDD. Авторы статей (например, [7]), в которых используется BDD, чаще всего применяют именно эту библиотеку. Разработчики новых пакетов BDD, как правило, стараются сравнить свои реализации именно с BuDDy. Сама библиотека написана на C, но имеет объектно-ориентированный интерфейс на C++. На сегодняшний день доступна уже версия 2.4 пакета. В BuDDy используются: • общий массив для хранения вершин; • глобальный кэш операций; • сериализация/десериализация; • запись BDD в dot-формат[6]; • динамическое изменение порядка переменных; • сбор статистики; • автоматическая сборка мусора; • поддержка арифметических операций через булевские векторы. Надо заметить, что динамическое изменение порядка переменных, призванное уменьшить объём хранимой информации, на практике сильно тормозит работу программы. Для многих задач оптимальный порядок (или близкий к оптимальному) известен заранее. Есть прецедент, когда BuDDy по скорости проиграл Java-реализации BDD. 1.2.2 CUDD CUDD (CU Decision Diagram Package)[4] — ещё одна известная реализация BDD, разработанная в Университете Колорадо (University of Colorado at Boudler). На сегодняшний день доступна версия 2.4.1. Как и BuDDy, эта библиотека написана на C. Помимо стандартных ROBDD, библиотека поддерживает ещё два вида диаграмм: ZeroSupressed BDD (ZDD), являющаяся ещё одним способом сжатия BDD и Algebraic BDD (ADD), которая описывает функцию {0, 1}n → . ADD к стандартным операциям BDD добавляет арифметические действия и взятие минимума(максимума). В библиотеке реализованы: 16 • общий массив для хранения вершин; • глобальный кэш операций; • поддержка ROBDD, ADD и ZDD; • динамическое переименование переменных; • сборка мусора; • сбор статистики; • запись BDD в blif-, dot- и DaVinci- форматы; • сериализация/десериализация с помощью внешней библиотеки dddmp. 2 Библиотека Разработанная программная компонента является первой библиотекой BDD на языке Objective Caml. Она была создана, прежде всего, как вспомогательный инструмент для анализа программ в рамках проекта PRANLIB(PRogram ANlisys LIBrary). Её применение в задачах анализа программ продемонстрировано в разделе 3.3. Язык OCaml был выбран по нескольким причинам. Во-первых, он обладает очень продуманной системой абстракций, основа которой — параметрический полиморфизм. Это позволяет программировать в терминах предметной области. Кроме того, OCaml статически типизируется, что позволяет отследить большую часть ошибок на этапе компиляции. Во-вторых, программы на OCaml в несколько раз меньше аналогичных на С, что позволяет быть очень лаконичным в описании алгоритмов. В третьих, PRANLIB написан на OCaml и это налагает известные ограничения на выбор языка. В четвёртых, как уже было замечено ранее, на Objective Caml не существует полноценной реализации BDD. В библиотеке реализовано несколько оригинальных алгоритмов, рассмотренных в разделе 2.4. Кроме того, библиотека интегрирована с программой для рисования графов GraphViz[6]. 2.1 2.1.1 Обзор библиотеки Обзор модулей Библиотека состоит из следующих модулей верхнего уровня: 17 • constants.cmo — все константы; конфигурировать библиотеку; изменяя их, можно • bddUtil.cmo — вспомогательные функции, конвертеры; • cache.cmo — структура данных для кэширования операций; • domain.cmo — абстракция “домен”; • bdd.cmo — общий интерфейс для всех BDD; • robdd.cmo — реализация ROBDD; • diffbdd.cmo — реализация ∆BDD; • relation.cmo — реализация абстракции “отношение”; • regression — директория тестов для регрессионного тестирования; • stress — директория тестов для стресс-тестирования. 2.1.2 Компиляция Компиляция библиотеки происходит стандартным образом: ./configure make make check sudo make install Библиотека требует наличия некоторых дополнительных пакетов. Если они не установлены, то configure выдаст сообщение об ошибке с указанием адреса в интернете, откуда недостающие пакеты можно скачать. make check запускает регрессионное и stress-тестирование и пишет отладочную информацию в test.log. После make install библиотека bdd.cma вместе с интерфейсами *.mli и метаинформацией META копируется в директорию для дополнительных библиотек OCaml и может быть использована другими приложениями. 18 2.1.3 Абстракции Интерфейс к абстракции “домен” описан в файле domain.mli. Домен Domain.t представляет из себя массив целых чисел и символизирует упорядоченный набор переменных. Любая логическая переменная в библиотеке задается как целое число. С математической точки зрения мы устанавливаем биекцию из множества переменных на множество натуральных чисел: x1 ↔ 1, x1 ↔ 2 и т.д. При этом линейный порядок на переменных определяется отношением “<” на целых числах. Абстракция “BDD” описана в файле bdd.mli. Мы будем различать BDD и её корневую вершину. Абстракция “вершина” вкупе с операциями над вершинами графа BDD описана в модуле Bdd.Node. Абстракция “Отношение” описана в файле relation.mli. Отношение реализовано через BDD, поэтому требуется функция-конвертер для получения булевского массива из произвольного элемента (кортеж b из 1.1.5). Для того, чтобы получать отдельные элементы отношения требуется обратная функция. Надо заметить, что размеры доменов, из которых состоит отношение, обязательно кратны степени двойки. Два домена можно слить в один. Для отношений это можно трактовать так: R — отношение из N1 × N2 , значит R — это подмножество множества N = N1 × N2 . 2.1.4 Начало работы Если пользователь хочет использовать ROBDD, он пишет в начале своей программы open Robdd а если Diffbdd, то open Diffbdd Интерфейсы для ROBDD и ∆BDD одинаковы и находятся в файле bdd.mli. Поэтому, чтобы перейти с одного типа BDD на другой, пользователю достаточно изменить одну строчку в своей программе. Интерфейс к отношениям лежит в файле relation.mli. Отношение параметризуется типом BDD. Если пользователь хочет использовать отношения, работающие через ROBDD, он пишет в начале своей программы module R = Relation.Make(Robdd) open R 19 а если Diffbdd, то module R = Relation.Make(Diffbdd) open R Здесь тоже нет никаких трудностей для перехода между разными типами BDD. Заметим, что с библиотекой BDD можно работать двумя путями: 1. Как с “черным ящиком”, то есть использовать её как ещё одно средство для представления логических выражений, множеств и отношений, не вдаваясь в детали реализации. 2. Как с “белым ящиком”. Это значит понимать алгоритмы и особенности BDD. Этот способ гораздо эффективнее предыдущего, потому что он позволяет выбрать оптимальный порядок вычислений и наиболее подходящие в конкретном случае алгоритмы. Скорость работы может возрасти в сотни раз. Пример работы с “белым ящиком” дан в разделе 3.3. 2.1.5 Алгоритм для бинарных операций В библиотеке использован отличный от классического алгоритм реализации бинарных операций. Легко заметить, что любую бинарную операцию (и унарное отрицание) можно представить через одну тернарную: if-then-else. Ниже приведена таблица соответствия: ¬e e1 ∧ e 2 e1 ∨ e 2 e1 ⇒ e 2 e1 ⇐ e 2 e1 ⇔ e 2 e → 0, 1 e1 → e 2 , 0 e1 → 1, e2 e1 → e 2 , 1 e2 → e 1 , 1 e1 → e2 , ¬e2 Таким образом, достаточно реализовать одну операцию, а остальные выразить через неё. Кроме того, это полезно для кэширования операций, как мы увидим далее. Алгоритм основан на следующем соотношении, которое вытекает из закона раскрытия Шэннона (f, g, h — логические выражения, x — логическая переменная): f → g, h = x → (f [0/x] → g[0/x], h[0/x]), (f [1/x] → g[1/x], h[1/x]) Нижеследующая процедура соответствует bdd(ev1 → ev2 , ev3 ). 20 /*вспомогательная функция; вызывается, когда v1 - терминал*/ ite(v1, v2, v3):Node = if v1 = terminal-1 then v2 else /* v1=terminal-0*/ v3 /*вспомогательная функция; сужение e(v) на vr=1*/ opt_one(v, x) : Node = if var(v) = x then one(v) else v /*вспомогательная функция; */ opt_zero(v, x) : Node = if var(v) = x then zero(v) else v /*основная рекурсивная процедура; сужение e(v) на vr=0*/ IfThenElse(v1, v2, v3):Node = v := D(v1, v2, v3); if v <> null then return v else if isTerminal(v1) then v := ite(v1, v2, v3) else x := min(var(v1), var(v2), var(v3)) v := insert(x, IfThenElse(opt_one(v1,x), opt_one(v2,x), opt_one(v3,x)), IfThenElse(opt_zero(v1,x), opt_zero(v2,x), opt_zero(v3,x))) add ((v1,v2,v3) -> v) into D return v endif Здесь таблица D, как обычно, выступает в роли кэша операций. Подход, описанный ранее (через apply), является классическим и применяется в BuDDy, CUDD, однако он кажется менее естественным, чем реализация через if-then-else. Во-первых, BDD — это суть INF-представление булевской функции. Во-вторых, таким образом нельзя выразить унарную операцию “¬” и приходится заводить отдельную функцию. В-третьих, для применения нерекурсивной операции приходится ждать пока оба операнда станут терминалами (в отличие от алгоритма, описанного выше). 21 2.1.6 Возможности библиотеки Помимо стандартных операций для BDD, библиотека предлагает целый ряд нововведений: • Абстракция ”отношение“ — позволяет строить произвольные конечные отношения и множества. Можно создавать отношения, выполнять бинарные операции, извлекать элементы. Необходимо лишь указать конвертер из элемента отношения в булевский массив (и обратно). Для числовых отношений в роли такого конвертера часто выступает двоичное разложение числа, для нечисловых — надо использовать таблицу символов. • Интеграция с PRANLIB — позволяет конвертировать BDD в направленные графы (модуль Digraph из библиотеки PRANLIB). • Интеграция с GraphViz — позволяет воспользоваться библиотекой отображения графов GraphViz для изображения BDD. Сериализует BDD в dot-формат (входной формат данных для GraphViz). Все рисунки данной работы получены с помощью пакета GraphViz. • Процедуры обхода графа BDD — могут пригодится для решения задач, которые требуют обход графа BDD. Иногда существующих операций над BDD не хватает (как в случае, например, с Binate Covering [9]). Реализованные гибкие алгоритмы обхода BDD делают попытку предоставить пользователю интерфейс для описания сложных проблем. • Решение задачи SAT — в библиотеке реализованы стандартные операции по поиску выполняющих интерпретаций: SatAny и SatCount. Вместо SatAll был реализован итератор полученных решений. Это удобнее, чем получать весь список сразу (он может быть настолько большим, что не поместится в памяти; в задаче, рассмотренной в [7], число решений может быть больше 1014 ). Кроме того, мы можем управлять порядком вывода интерпретаций. • Сборщик мусора — реализован собственный алгоритм утилизации неиспользуемых более вершин графа BDD на основе слабых хэштаблиц. • Статистика — можно сконфигурировать библиотеку на вывод отладочных сообщений (./configure --enable-log). В этом случае во время работы приложения, основанного на библиотеке, будет выводится отладочная информация. 22 • Процедуры ускорения — библиотека реализована таким образом, чтобы получить максимальный выигрыш в скорости (раздел 2.4). 2.2 Интерфейс к BDD Ниже представлен интерфейс к библиотеке на языке OCaml. Он находится в файле bdd.mli. module type Sig = sig type t Абстрактный тип данных “BDD”. val toString : t -> string Отладочная печать. type elem = bool array Этот тип используется вместе с доменом. Вкупе с Domain.t даёт интерпретацию. val init : unit -> unit Процедура инициализации. Должна быть вызвана один раз для каждого типа BDD, с которым пользователь собирается работать. val empty : t Возвращает 0-терминал. val full : t Возвращает 1-терминал. val isEmpty : t -> bool Проверяет, что вершина — 0-терминал. 23 val isFull : t -> bool Проверяет, что вершина — 1-терминал. val isTerm : t -> bool Проверяет, что вершина — терминал. val equal : t -> t -> bool Проверяет две BDD на равенство. Дальше следует набор бинарных операций. Все они выражаются через if-then-else. val (<&>) : t -> t -> t Конъюнкция. val (<|>) : t -> t -> t Дизъюнкция. val (<=>) : t -> t -> t Эквивалентность. val (=>) : t -> t -> t Импликация. val (<=) : t -> t -> t Обратная импликация. val (<^>) : t -> t -> t 24 Исключающее ИЛИ (xor). val ifthenelse : t -> t -> t -> t Tернарная операция if-then-else. val (!!) : t -> t Унарное отрицание. val exist : t -> Domain.t -> t Квантор существования. exist v [|i1;...;in|] значит ∃x i1 ...∃xin ev . val forall : t -> Domain.t -> t Квантор всеобщности. forall v [|i1;...;in|] значит ∀x i1 ...∀xin ev . val existAllExcept : t -> Domain.t -> t Выполняет функцию exist для всех переменных, которые не входят в данный домен. val rename : t -> Domain.t -> Domain.t -> t Переименовывает переменные, входящие в выражение, соответствующее данному BDD из одного домена в v другой. Например, если e = xi1 ∧ xi2 ∨ xi3 , то после вызова rename v [|i1;i2;i3|] [|j1;j2;j3|] новое выражение будет выглядеть так: xj1 ∧ xj2 ∨ xj3 . val unsafeRename : t -> Domain.t -> Domain.t -> t Операция “белого ящика”. Тоже самое, что и rename, но требует дополнительных условий, наложенных на домен (см. 2.4), зато работает значительно быстрее. val unsafeExist : t -> Domain.t 25 -> t Операция “белого ящика”. Тоже самое, что и exist, но требует дополнительных условий, наложенных на домен (см. 2.4), зато работает значительно быстрее. val unsafeForall : t -> Domain.t -> t Операция “белого ящика”. Тоже самое, что и forall, но требует дополнительных условий, наложенных на домен (см. 2.4), зато работает значительно быстрее. val mkOneNode : int -> bool -> t mkOneNode i b выдаёт такую BDD v, что ev = xi , если b = 1, и ¬xi , если b = 0. val mkOneElem : Domain.t -> elem -> t mkOneElem [|i1;...|in] [|b1;...bn|] mkOneNode i1 b1, ..., mkOneNode in bn выдаёт конъюнкцию val mkInterval : Domain.t -> elem -> elem -> t mkInterval [|i1;...|in] [|b1;...bn|] [|c1;...cn|] выдаёт дизъюнкцию mkOneElem [|i1;...|in] [|d1;...dn|], для всех (d1, ..., dn), таких что b1 ≤ d1 ≤ c1, ..., bn ≤ dn ≤ cn, если считать, что на множестве = {0, 1} действует линейный порядок 0 < 1. val mkSwap : int -> int -> bool -> t mkSwap i j b строит BDD для выражения xi ⇔ xj , если b = 1, и ¬xi ⇔ xj , если b = 0. module Node: sig type bdd = t 26 Операции в этом модуле работают с вершинами в графе BDD. Заметим, что результат функций зависит от типа BDD. type t Абстрактный тип данных “Вершина”. val zero : t -> t Выдаёт потомка данной вершины по дуге из множества Zero. zero v = v 0 : (v, v 0 ) ∈ Zero. val one : t -> t Выдаёт потомка данной вершины по дуге из множества One. one v = v 0 : (v, v 0 ) ∈ One. val root : bdd -> t Выдаёт корневую вершину BDD. Все оставшиеся операции из данного модуля берут в качестве параметров список корневых вершин и флаг inclT erms. Эти операции работают с подграфом общего графа BDD, который получается, если мы пойдём поиском в глубину от всех корневых вершин. Если inclT erm = true, то терминалы включаются в полученный подграф. Кроме того, если список корневых вершин пуст, то считается, что подграф равен всему графу. val count : ?inclTerms:bool -> t list -> int Выдаёт число элементов в подграфе. val fold_left : ?inclTerms:bool -> ?order:Constants.Bdd.Node.FoldOrder.t -> (’a -> t -> ’a) -> ’a -> t list -> ’a 27 Делает обход графа в глубину. Вершины в порядке обхода как бы записываются в воображаемый список, над которым потом производится операция fold_left. Порядок вершин в списке зависит от переменной order. val fold_right : ?inclTerms:bool -> ?order:Constants.Bdd.Node.FoldOrder.t-> (t -> ’a -> ’a) -> t list -> ’a -> ’a Делает обход графа в глубину. Вершины в порядке обхода записываются в воображаемый список, над которым потом производится операция fold_right. Порядок вершин в списке зависит от переменной order. val iter : ?inclTerms:bool -> ?order:Constants.Bdd.Node.FoldOrder.t -> (t -> unit) -> t list -> unit Делает обход графа в глубину. Вершины в порядке обхода записываются в воображаемый список, над которым потом производится операция iter. Порядок вершин в списке зависит от переменной order. val toString : t -> string Отладочная печать end module Sat sig : Операции из этого модуля занимаются поиском и подсчетом числа выполняющих интерпретаций. val count : t -> Domain.t -> Big_int.big_int Выдает количество выполняющих интерпретаций. 28 val any : t -> Domain.t -> elem Выдает произвольную выполняющую интерпретацию. val fold : ?order:Constants.Bdd.Sat.FoldOrder.t -> (elem -> ’a ->’a) -> t -> ’a -> Domain.t -> ’a Получает воображаемый список выполняющих применяет к нему операцию fold_right. интерпретаций и val iter : ?order:Constants.Bdd.Sat.FoldOrder.t -> (elem -> unit) -> t -> Domain.t -> unit Получает воображаемый список интерпретаций и применяет к нему операцию iter. end module DAG sig : Этот модуль содержит операции для взаимодействия с GraphViz и PRANLIB. module G : Digraph.Sig Модуль для операций с ориентированным графом, полученным из BDD. val toDigraph : t list -> G.t Строит PRANLIB-граф из подграфа BDD, задаваемого списком корневых вершин (как в модуле Node). Если список пуст, то подграф считается равным всему графу. val toDOT : ?verbose:bool -> t list -> string 29 Строит строку в dot-формате из подграфа BDD, задаваемого списком корневых вершин (как в модуле Node). Если список пуст, то подграф считается равным всему графу. После записи полученной строки в файл .dot, GraphViz может преобразовать информацию в графический формат, видимый пользователю. end module GC : sig type bdd = t Модуль, отвечающий за сборку мусора. Чтобы включить сборщик мусора, надо в начале программы написать GC.enable() . Каждую BDD, которую предполагается сохранить после сборки мусора, необходимо “обернуть” с помощью GC.wrap. Для того, чтобы производить операции с такими “обернутыми” BDD, надо развернуть их с помощью метода GC.unwrap. Кроме того, модуль содержит процедуры настройки сборщика мусора. В существующей реализации сборщик пытается начать свою работу, когда вызывается метод GC.wrap. Для этого он проверяет, что количество вершин в BDD превысило некий предел (который можно сконфигурировать). type t Абстрактный тип данных “Обернутая BDD”. val isEnabled : unit -> bool Функция проверяет, включен ли сборщик мусора. val enable : unit -> unit Включает сборщик мусора. val disable : unit -> unit 30 Выключает сборщик мусора. Это надо, например, чтобы обернуть все BDD, которые мы намереваемся использовать после сборки мусора, а потом снова включить сборщик. val start : unit -> unit Пытается запустить сборку мусора. Процедура проверяет, что сборщик включен, плюс превышен предел вершин в BDD. val force : unit -> unit Запускает процедуру сборки мусора, даже если сборщик выключен, а предел не превышен. val setThreshold : int -> unit Устанавливает предел вершин в BDD. val getThreshold : unit -> int Возвращает текущий предел. По умолчанию он берется из модуля Constants. val wrap : bdd -> t “Оборачивает” BDD. val unwrap : t -> bdd “Разворачивает” BDD. end end 31 2.3 Интерфейс к отношениям Ниже представлен интерфейс для операций над отношениями. module type Sig = sig type t Абстрактный тип данных “отношение”. val empty : t Пустое отношение. val full : t Полное отношение. Для любого набора доменов полное отношение будет представляться как логическая константа 1. val isEmpty : t -> bool Проверяет, пусто ли отношение. val isFull : t -> bool Проверяет, полно ли отношение. val (!!) : t -> t Дополнение к отношению. val (<&>) : t -> t -> t Пересечение отношений. val (<|>) : t -> t -> t 32 Объединение отношений. val (</>) : t -> t -> t Разность отношений. val rename : t -> Domain.t -> Domain.t -> t Меняет один из доменов в отношении. Если r ⊆ N1 × · · · × Ni−1 × Ni × Ni+1 × · · · × Nk , то rename r Ni Nj выдаст новое отношение r 0 ⊆ N1 × · · · × Ni−1 × Nj × Ni+1 × · · · × Nk , такое, что (a1 , ..., an ) ∈ r ⇔ (a1 , ..., an ) ∈ r 0 val unsafeRename : t -> Domain.t -> Domain.t -> t Операция “белого ящика”. Тоже что и rename, дополнительных ограничений, зато работает быстрее. val exist но требует : t -> Domain.t -> t Убирает зависимость от одного из доменов в отношении. Если r ⊆ N1 × · · · × Ni−1 × Ni × Ni+1 × · · · × Nk , то rename r Ni выдаст новое отношение r 0 ⊆ N1 × · · · × Ni−1 × Ni+1 × · · · × Nk , такое, что (a1 , ..., ai−1 , ai+1 , ..., an ) ∈ r 0 ⇒ (∃ai (a1 , ..., ai−1 , ai , ai+1 , ..., an ) ∈ r) val unsafeExist : t -> Domain.t -> t Операция “белого ящика”. Тоже что и exist, но требует дополнительных ограничений, зато работает быстрее. val join : t -> t -> Domain.t -> t Тоже, что и операция join в базах данных. эквивалентно exist (r1 <&> r2) dom. join r1 r2 dom val project : t -> Domain.t -> t Делает проекцию отношения на домен. Если r ⊆ N1 × · · · × Ni−1 × Ni × Ni+1 × · · · × Nk , то project r Ni выдаст новое отношение r 0 ⊆ Ni , такое что (ai ) ∈ r 0 ⇒ (∃a1 ...∃ai−1 ∃ai+1 ...∃ak (a1 , ..., ai−1 , ai , ai+1 , ..., an ) ∈ r). 33 val restrict : t -> Domain.t -> ’a -> (’a -> bool array) -> t Оставляет только те кортежи, которые на данном домене равны определённому значению. Пусть t ⊆ N1 × · · · × Ni · · · × Nk . Тогда restrict r dom a f выдаст новое отношение r 0 , такое что (a1 , ..., ai−1 , ai , ai+1 , ..., an ) ∈ r 0 ⇒ (a1 , ..., ai−1 , ai , ai+1 , ..., an ) ∈ r ∧ ai = a. Функция-конвертер f переводит произвольное значение в булевский массив.. val mkDiagonal : Domain.t -> Domain.t -> t Создаёт бинарное отношение, где каждый элемент — это пара, у которой совпадают первый и второй элементы. val rmDiagonal : t -> Domain.t -> Domain.t -> t Удаляет диагональ из отношения. val stabilize : (t -> t) -> t -> t Итерирует до тех пор, пока следующее отношение не станет равно предыдущему. stabilize f r выдаёт отношение r 0 = f n (r), где n такое, что f n (r) = f n+1 (r). val trns_clsr : ?supportDomain:Domain.t -> t -> Domain.t -> Domain.t -> t Выдаёт транзитивное замыкание бинарного отношения. supportDomain — это вспомогательный домен (по размерности равный двум другим). Если supportDomain не задан явно, то он создаётся автоматически; однако, это может понизить скорость работы. module Tuple : sig Следующий модуль работает с элементами множеств. val mkOne : Domain.t -> ’a -> (’a -> bool array)-> t 34 Создаёт множество, состоящее из одного кортежа. val mkInterval : Domain.t -> ’a -> ’a -> (’a -> bool array) -> t Создаёт множество, состоящее из кортежей, находящихся между двумя границами (включительно). val count : t -> Domain.t -> Big_int.big_int Подсчитывает число элементов в множестве. val any : t -> Domain.t -> (bool array -> ’a) -> ’a Выдаёт произвольный элемент из множества. val fold : (’a -> ’b -> ’b) -> t -> Domain.t -> (bool array -> ’a) -> ’b -> ’b Записывает элементы множества в виртуальный список и выполняет над ним операцию fold_right. val iter : (’a -> unit) -> t -> Domain.t -> (bool array -> ’a) -> unit Записывает элементы множества в виртуальный список и выполняет над ним операцию iter. end end module Make(Bdd : Bdd.Sig) : Sig Этот функтор генерирует модуль отношений, основанный на определённой реализации BDD, передаваемой ему в качестве параметра. 35 2.4 Особенности реализации В этом разделе кратко будут рассмотрены отличительные особенности библиотеки, ускоряющие её работу. Введем еще одно определение: Определение 2.1. Логическое выражение e не зависит от переменной x , если e ≡ e[0/x]. В противном случае будем говорить, что e зависит от x. Свойство 2.2. Логическое выражение e не зависит от переменной x тогда и только тогда, когда ни одна из вершин ROBDD, соответствующего e, не помечена x. 2.4.1 Физическое представление Все данные хранятся в одном динамическом массиве. BDD отождествляется со своей корневой вершиной, а вершина — с целым числом (индексом массива). В массиве хранятся четвёрки {var:int, one:int, zero:int, mark:int} для ROBDD и {dvar:int, one:int, zero:int, mark: int} для DiffBDD. Введенное дополнительное поле mark позволяет некоторым алгоритмам (например, обходу графа и сборщику мусора) хранить промежуточные данные, не заводя дополнительных структур данных. 2.4.2 Кэширование операций Для динамического программирования в алгоритмах IfThenElse, Restrict и др. используется глобальный кэш операций. Он реализован как специфичная хэштаблица фиксированной длины, которая вместо того, чтобы добавлять элемент с данным хэшем в цепочку, заменяет предыдущий на данный. Это позволяет, во-первых, сделать таблицу единой для всех алгоритмов, а во-вторых, не допустить её разрастания. 2.4.3 Ненадежные переименование и кванторы Самая дорогая операция в BDD — это переименование переменных из одного домена в другой (операция rename). Она реализуется следующим образом (предполагаем, что D = {x1 , ..., xn }, D 0 = {x01 , ..., x0n }): rename(e, D, D 0 ) = ∃x1 ...∃xn (e ∧ (x1 ⇔ x01 ) ∧ · · · ∧ (xn ⇔ x0n )) что требует n − 1 бинарных операций ∧. Заметим, что если логическое выражение ev зависит только от D, то в BDD все переменные, входящие в D, можно просто заменить на 36 соответствующие им переменные из D 0 (т.е. x1 на x01 , x2 на x02 и т.д.). При этом, в новой BDD v 0 будет такое же количество вершин, что и в старой. Этот частный случай можно обобщить. Если выполняются некоторые условия, операцию rename можно реализовать значительно эффективнее. Итак, если для логического выражения e выполнены: • ∀i ∈ {1..n} если xi < x0i , то e не зависит ни от одной переменной x, таких что (x 6∈ D) ∧ xi < x < x0i • ∀i ∈ {1..n} если x0i < xi , то e не зависит ни от одной переменной x, таких что (x 6∈ D) ∧ x0i < x < xi то операцию rename можно заменить на unsafeRename, которая работает следующим образом: UnsafeRename(v, dom1, dom2):Node { u := D(v); if (u <> null) then return u else if isTerminal(v) then return v else if exists i : dom1[i]=v then u := Insert(dom2[i], UnsafeRename(one(v), dom1, dom2), UnsafeRename(zero(v), dom1, dom2)) else u := Insert(v, UnsafeRename(one(v), dom1, dom2), UnsafeRename(zero(v), dom1, dom2)) endif add v->u into D return u endif При этом время работы линейно зависит от числа вершин в исходной BDD. Такая операция переименования называется ненадежным переименованием (unsafeRename), так как вызывающий её пользователь должен представлять, что делает. В данном случае с BDD нельзя работать, как с “черным ящиком”. На самом деле, почти всегда можно так выбрать порядок переменных, что в случае необходимости переименовать переменные вызывать именно unsafeRename (см 3.3). По аналогии с unsafeRename были введены операции unsafeExist и unsafeForall. unsafeExist v D можно вызывать вместо exist, если e v 37 не зависит ни от одной переменной x, такой что (x 6∈ D) ∧ x1 < x, где D = x1 , ..., xn 2.4.4 Сборка мусора Так как все вершины BDD лежат в одном динамическом массиве, они не удаляются при отрабатывании обычного сборщика мусора. Поэтому в библиотеке был сделан собственный сборщик, который реализован следующим образом: • Для того, чтобы пометить BDD, которые переживут сборку мусора (перманентные BDD), пользователь должен использовать метод Bdd.GC.wrap, возвращающий оболочку для данного BDD и заносящий ее в таблицу. Чтобы получить BDD из оболочки, надо использовать метод Bdd.GC.unwrap. • Сборщик использует слабую хэштаблицу, чтобы хранить оболочки. Если исчезают все внешнии ссылки на оболочку, то она удаляется из таблицы. • Сборка мусора запускается в момент вызова Bdd.GC.wrap, если сборщик был активирован ранее методом Bdd.GC.enable и если количество вершин в глобальном массиве превысило некий предел. • Процедура сборки мусора реализована так, что не требует лишней памяти, когда сжимает массив и удаляет ненужные элементы. Надо заметить, что при использовании интерфейса отношений нет необходимости вручную управлять оболочками и сборщиком мусора — всё это спрятано внутрь реализации. 3 Примеры использования В данном разделе мы рассмотрим три задачи, для решения которых может быть использована BDD. Первые две связаны с логическими выражениями, а третья — с отношениями. В качестве стресстестирования библиотека проверялась именно на этих примерах. 3.1 Задача о расстановке ферзей Сначала рассмотрим известную с древних времен задачу — расставить на шахматной доске 8 ферзей так, чтобы никакие два не били друг 38 друга. Нам эта задача интересна с той точки зрения, что её можно представить через логическую формулу. Cопоставим каждой клетке доски логическую переменную : x11 x12 . . . x18 x21 x22 . . . x28 .. .. . . .. . . . . x81 x82 . . . x88 Теперь начнем накладывать ограничения. Нам известно, что если ферзь стоит на определённой клетке, то на этой горизонтали, вертикали и диагонали уже не может быть других ферзей. Запишем это для клетки (1,1): • x11 => ¬x12 ∧ ¬x13 ∧ ... ∧ ¬x18 — горизонталь • x11 => ¬x21 ∧ ¬x31 ∧ ... ∧ ¬x81 — вертикаль • x11 => ¬x22 ∧ ¬x33 ∧ ... ∧ ¬x88 — диагональ Для каждой клетки необходимо повторить данную процедуру. Заметим, что для неугловых клеток нам надо учитывать две диагонали. Осталось еще одно условие: в любой строке должен быть хотя бы один ферзь. Тоже самое можно сказать и про каждый столбец, и про каждую диагональ, но достаточно взять что-нибудь одно — другие ограничения автоматически становится излишним. Итак добавим: xi1 ∧ xi2 ∧ ... ∧ xi8 , i ∈ {1..8}. Искомая логическая формула — это конъюнкция всех полученных условий. Их количество — 64 ∗ 3 + 8 = 200. Теперь, чтобы получить решение, нам надо получить какую-нибудь выполняющую интерпретацию. Если мы хотим получить все решения задачи, то достаточно вызвать метод Bdd.iter. Ниже приведены результаты тестирования библиотеки: N 6 7 8 t(ROBDD) t(∆BDD) 0.23 0.25 1.40 1.52 12.46 13.88 δt s(ROBDD) s(∆BDD) δs 8% 21048 22213 4% 8% 109735 115336 5% 11% 649832 698792 7% В столбцах слева направо содержится следующая информация: 1. количество клеток на вертикали (горизонтали) доски (для тестирования мы берем не только стандартные доски 8 × 8, но и 6 × 6, и 7 × 7); 39 2. время работы ROBDD в секундах; 3. время работы ∆BDD в секундах; 4. выигрыш по времени работы ROBDD относительно ∆BDD в процентах; 5. количество вершин в ROBDD; 6. количество вершин в ∆BDD; 7. выигрыш по количеству вершин ROBDD относительно ∆BDD в процентах. Как видно из теста, в задаче о расстановке ферзей ROBDD по обоим параметрам лучше, чем ∆BDD. 3.2 Японские кроссворды Здесь будет приведен другой очень похожий, но более жизненный пример. Японские кроссворды — любимая игра многих русских домохозяек. Её суть заключается в следуюшем: есть поле n × m. На каждый столбец и строку наложено условие в виде набора целых чисел. Необходимо закрасить клетки поля так, чтобы все условия были выполнены. Если условие для строки i выглядит как (a1 , a2 , ..., ak ), то это значит, что в строке закрашено ровно k сегментов, разделённых не более чем одним пробелом. Сегменты располагаются последовательно слева направо, причем в первом — a1 закрашенных клеток, во втором a2 и т.д. Условие для столбцов выглядит аналогично. Единственная разница — закрашенные сегменты располагаются сверху вниз. В следующем примере рассматривается простейший японский кроссворд 10 × 10: 40 2 2 3 2 1 3 5 3 2 1 5 1 5 1 10 3 2 5 3 1 2 1 3 1 5 4 6 7 1 1 1 Для решения задачи нам потребуется n × m логических переменных. Надо наложить условие на каждую строку (получить логическое выражение от соответствующих переменных) и столбец, а потом взять конъюнкцию всех условий, как и в задаче про ферзей. Для получения решения достаточно получить выполняющую интерпретацию с помощью методов Bdd.Sat.any или Bdd.Sat.iter. Заметим, что мы не применяли никакого алгоритма — просто формализовали условие задачи в терминах математической логики. Ниже приведены результаты тестирования библиотеки: n/m t(ROBDD) t(∆BDD) 10/10 0.05 0.05 20/20 1.90 2.02 25/25 9.31 10.03 δt 0% 6% 8% s(ROBDD) 7231 214129 752103 s(∆BDD) 5991% 173315 568210 δs -17% -19% -25% В первой колонке указаны количество строк и столбцов в исходной задаче. Остальные колонки таблицы такие же, как и в задаче о расстановке ферзей. Стоит отметить, что время работы и количество вершин BDD в задаче о японских кроссвордах более завиcят от условий, наложенных на строки и столбцы, чем от размера игрового поля. 3.3 Поиск вхождения образца в граф Задача ставится следующим образом: есть ориентированный граф G = (V, E), E ⊆ V × V (называемый субъектом), каждая вершина которого помечена какой-нибудь меткой (т.е. существует функция 41 label : V → L, где L — множество меток). Есть образец — тоже помеченный ориентированный граф P = (V 0 , E 0 ), E 0 ⊆ V 0 × V 0 , с функцией label0 : V 0 → L. Необходимо найти все вхождения P в G, т.е все такие отображения f : V 0 → V , такие что label0 (v 0 ) = label(f (v 0 )) ∧ (v10 , v20 ) ∈ E 0 ⇒ (f (v10 ), f (v20 )) ∈ E. Подобная проблема возникает в задаче выбора инструкций. Там субъект — это DAG, представляющий из себя арифметическое выражение. Вершины у субъекта и образцов помечены арифметичискими операциями. Образцы — это представление различных машинных инструкций ввиде графов. Как первый шаг алгоритма, необходимо найти все вхождения образцов в субъект. В случае сложных инструкций (например, несвязных графов, представляющих параллельные инструкции), поиск всех вхождений может стать нетривиальной проблемой. Рис. 5: Субъект и образец Если G и P — это деревья, то данная задача решается динамическим программированием. Приведенное в этом разделе решение не требует никаких ограничений на субъект и образец — они могут не быть деревьями, DAG-ами или хотя бы связными графами. Пример 3.1. На рисунке 5 изображен субъект и образец. Очевидно, что существует всего два вхождения (рисунок 6). Каким же образом решить задачу с помощью BDD? Таким же, как решены предыдущие две — формализовать условие в терминах, удобных для BDD. Будем решать эту проблему через отношения. Мы представляем все возможные покрытия образцом субъекта, как отношение r ∈ {N1 , N2 , ..., Nk }, где k — количество вершин образца. 42 Рис. 6: Вхождения Если кортеж (x1 , ..., xk ) принадлежит отношению, то существует такое вхождение f , что f (x01 ) = x1 , f (x02 ) = x2 и т.д. Субъект представляется как пара отношений g = (n ∈ Nstart × L, e ∈ N start × N end), где N start, N end — домены (такие же, как N 1, N 2, ...), а L — домен меток для вершин. Запишем алгоритм неформально: инициализируем R как все полное отношение, равное N1 x ... x Nk k - количество вершин образца for i=1 to k do r := r сужая на (label(x(i)) = label(x’(i))) done for i=1 to ’кол-во дуг образца’ do пусть i-я дуга образца = (a -> b) тогда и в субъекте должна быть дуга из x(a) в x(b) r := r пересечь c (‘rename (‘rename‘ g.e Nstart Na) Nfinish Nb‘) done В принципе, если образец несвязный, то надо ещё вычесть из полученного отношения диагонали всех пар доменов, чтобы две вершины образца не соответствовали одной и той же вершине субъекта. Заметим, что здесь используется операция rename, которая работает довольно значительное время. Однако, мы можем его существенно снизить, правильно расположив домены и используя операцию unsafeRename. Еще немного ускорить решение мы 43 можем за счет unsafeExist. Расположим домены следующим образом: Nstart , Nf inish , N1 , ..., Nk , L. Ещё один трюк — использовать дополнительное отношение g.w, которое представляет из себя обращенные дуги, т.е. (x1 , x2 ) ∈ g.w ⇒ (x2 , x1 ) ∈ g.e. Это новое отношение нужно для того, чтобы воспользоваться unsafeRename. Теперь запишем алгоритм более формально (предполагаем, что k — количество вершин образца, а l — количество дуг): r := full for i=1 to k do r := r <&> UnsafeExist(UnsafeRename( Restrict(g.n, Nstart, label(x’(i))), Nstart, Ni), L) done for i=1 to l do ]e’(i) = (a,b) if (a < b) then r := r <&> UnsafeRename( UnsafeRename( g.e, Nfinish,Nb), Nstart, Na ) else r := r <&> UnsafeRename( UnsafeRename( g.w, Nfinish,Na), Nstart, Nb ) done Количество операций BDD: • кол-во rename = кол-во дуг образца * 2 + кол-во вершин образца; • кол-во exist = кол-во вершин образца; • кол-во пересечений = кол-во дуг образца + кол-во вершин образца. Ниже приведены результаты тестирования библиотеки на данной задаче: s/p/l t(ROBDD) 100/3/1 0.05 200/5/1 0.13 200/5/3 0.10 t(∆BDD) δt 0.05 0% 0.14 10% 0.11 8% 44 s(ROBDD) 11841 32557 27757 s(∆BDD) 10533% 25042 24650 δs -11% -23% -11% Первый столбец — это количество вершин в субъекте / количество вершин в образце / количество меток. Остальные столбцы такие же как и в предыдущих двух задачах. Заключение В рамках дипломной работы была реализована библиотека диаграмм двоичных решений на языке OCaml. Библиотека включает два типа BDD: сокращенные упорядоченные BDD и дифференциальные BDD. В ходе работы был изобретён ряд алгоритмов, которые существенно ускорили работу библиотеки. Кроме того, в библиотеке реализованы продуманная система абстракций и удобный функциональный интерфейс. В качестве примера задачи анализа программ был придуман, реализован и протестирован алгоритм, находящий все вхождения образца в граф. Алгоритм основан на BDD и использует оригинальные функции библиотеки, позволяющие сильно ускорить его работу. Кроме того, было проведено сравнение ROBDD и ∆BDD. Результат сильно зависел от характера задачи, но можно сказать, что ROBDD гарантировно работает быстрее, чем ∆BDD (разница не более 11%). С другой стороны, на тех задачах, где оптимальный порядок переменных неизвестен заранее, ∆BDD требует меньше памяти, чем ROBDD. В качестве направлений для дальнейших исследований можно указать ещё неизученные сложные задачи анализа программ, которые можно было бы выразить в терминах логических выражений и BDD. Кроме того, существуют другие виды сжатия BDD (такие как ZBDD [4]), которые интересно было бы реализовать и сравнить с ROBDD и ∆BDD. 45 Список литературы [1] Randell E. Bryant. Symbolic Boolean manipulations with ordered binary decision diagrams. ACM Computer Surveys, 24(3):293-318, September 1992. [2] Henric Reif Andersen. An introduction to Binary Decision Diagrams. Lecture notes for 49285 Advanced Algorithms E97, October 1997. [3] Jorn Lind-Nielsen. BuDDy: Binary Decision Diagram package. ITUniversity of Copenhagen (ITU), Novemner 9, 2002. [4] Fabio Somenzi. CUDD: CU Decision Diagram Package. Departament of Electriacal and Computer Engineering. University of Colorado at Boudler. May 17, 2005. [5] PRANLIB, oops.tepkom.ru/projects/pranlib [6] GRAPHVIZ, www.graphviz.org [7] J. Whaley and M. Lam. Cloning-based context-sensitive pointer alias analyses using binary decision diargrams. In: Prog. Lang. Design and Impl. (2004) [8] Anuchit Anuchitanukul, Zohar Manna and Tomas E. Uribe. Computer Science Today . Lecture Notes in Computer Science Vol. 1000, pages 218–233, Springer-Verlag, September, 1995. [9] Seh-Woong Jeong and Fabio Somenzi. A new algorithm for the binate covering problem and its application to the minimization of Boolean relations. ICCAD ’ 92, pp. 417-420. 46