№ 6 (36) 2011
Н. В. Заборовский, аспирант Московского физико-технического института
(государственного университета)
А. Г. Тормасов, докт. физ.-мат. наук, профессор Московского физико-технического института
(государственного университета)
Статическое обнаружение гонок в коде, содержащем ветвления и циклы
В настоящее время весьма актуальными являются задачи, связанные с реализацией многопоточных алгоритмов. Данная работа посвящена анализу корректности неблокирующих алгоритмов в смысле состояний гонки.
Введение
Состояние гонки — ситуация, когда состояние общей для нескольких потоков ячейки памяти зависит от распределения процессорного времени между потоками (фактически нельзя предугадать это состояние на уровне алгоритма — скорее всего, в реализации алгоритма ошибка). Используется статический подход к анализу: анализируется сам исходный код на 0/0++. Далее будет изложена методика, которая позволяет проанализировать код в статическом режиме и его обоснование. Модель, построенная на основе предлагаемого подхода, позволяет отслеживать ситуации гонки для одной из разделяемых переменных и может быть расширена, в частности, до анализа корректности организованных средствами языка критических секций.
Обзор методов
В работе [3] приводятся результаты формального аналитического доказательства корректности неблокирующего алгоритма, в том числе корректности в смысле отсутствия гонок. Аналитический способ доказательства для реальных алгоритмов оказался очень сложным.
В работе [2] была предложена идея построения модели взаимного исполнения ато-
марных инструкций двумя потоками на разделяемой памяти и принцип ее анализа с целью определения состояния гонки. Общая процедура работы с исходным кодом для выявления состояний гонки выглядит следующим образом:
1. Для каждого потока выделяются операции с интересующими нас разделяемыми переменными.
2. Строится граф на основании операций с разделяемыми переменными.
3. На графе выделяются определенные пути, представляющие интерес для анализа.
4. Выделенные пути анализируются, например, с помощью метода неопределенных коэффициентов.
5. По состоянию в финальной вершине делается вывод о наличии гонок.
В основе модели лежит граф G = (V, А) совместного исполнения потоков, в котором путям соответствуют всевозможные варианты исполнения многопоточного алгоритма. Вершинам графа А соответствует множество состояний общей памяти после выполнения очередной инструкции, дугам V — атомарные операции над разделяемыми переменными. Каждому ребру приведена в соответствие операция (чтение/запись) и ячейка памяти, над которой производится операция.
Также для модели определена функция корректности, отражающая субъективное
№ 6 (36) 2011
понятие о корректном или некорректном исполнении программы. Одна и та же многопоточная программа корректна с точки зрения одной функции корректности и некорректна — с другой. Функцию корректности можно переформулировать в терминах частей графа G. К примеру, задача о нахождении неразрешенного состояния гонки может быть сведена к поиску двух различных путей на графе, приводящих к различным значениям одной из разделяемых переменных.
Граф G имеет вид ромба, в котором начальная вершина находится вверху, а конечная внизу, и все ребра имеют направление сверху вниз. Сильное место подхода — небольшая сложность анализа. Количество вершин на каждом из ребер ромба линейно по количеству операций, и каждая вершина проходится при анализе только один раз. Сложность подхода для двух потоков оценивается как О (к • л), где к и п — количество атомарных операций каждого из потоков. Важно заметить, что рассматриваются только операции с разделяемыми переменными.
Далее с построенным графом поступают следующим образом. Рассматривают произвольный путь на графе из начальной вершины в конечную. Такой путь описывает взаимную последовательность исполнения атомарных операций потоками. На графе есть вершины, из которых исходят ребра с операциями над разными ячейками памяти или только операциями чтения одной и той же разделяемой переменной. В этом случае считается, что из вершины можно двигаться по любому из исходящих ребер, т. к. на состояние памяти в финальном состоянии это не повлияет. В остальных случаях результат алгоритма будет зависеть от порядка исполнения инструкций — от того, какой путь из вершины избрать. При этом операции на ребрах называют не коммутирующими, а соответствующую ячейку графа отмечают крестом.
На данном наблюдении основывается построение классов эквивалентности — набо-
ры полных путей на графе G, для которых содержимое ячеек памяти в финальном состоянии одинаково.
После нахождения всех классов путей достаточно взять по одному представителю каждого класса, чтобы охватить все возможные последовательности смены ячеек памяти.
В работе [2] понятие гонки вводится как существование двух разных полных путей, для которых в финальной вершине состояния хотя бы одной из ячеек памяти различны. Формулировка согласуется с функцией корректности, которая может быть выражена как количество различных векторов значений переменных в конечной вершине. Для анализа используется метод неопределенных коэффициентов: на каждом ветвлении добавляется коэффициент к изменению в левой ветви и (1 - а) — в правой. Значение каждой переменной финальной вершины описывается формулой:
X = ^ (х0,{а}),
(1)
где х0 — состояние всех ячеек памяти в начальной вершине; {а} — значения неопределенных коэффициентов. Исходя из определения понятия гонки и введенных принципов построения и анализа графа, условие гонки формулируется как
3/,{а}1,{а}2 : ^(Хо,{а^) ф ^(Хо,{а}2
(2)
где {а}1 и {а}2 — наборы бинарных коэффициентов, в которых хотя бы одна из компонент отличается.
Кроме того, приводятся рассуждения о том, какую сложность привносят в анализ такие конструкции кода, как ветвления и циклы.
Предлагается дополнить определение графа G, чтобы он работал также для ветвлений и циклов, но при этом использовал старые принципы анализа.
о
л
1 £
о
.Э со
со
Эй
39
№ 6 (36) 2011
Проблемы современных систем статического анализа
I. Ложные срабатывания
Для существующих в настоящее время программных средств статического анализа программ проблемой является ошибочное определение состояния гонки (false-positive) и не нахождение гонки при ее наличии (false-negative). Эта проблема неминуемо возникает при анализе программного кода из-за наличия циклов, ветвлений, псевдонимов и других конструкций, усложняющих его. Обычная практика для синтаксических анализаторов — использование набора эвристик для определения состояния гонки. Эвристики хорошо работают на элементарных тестах и модельных задачах, но показывают очень плохие результаты на реальных задачах.
В модели исполнения алгоритма в нескольких потоках сделана попытка учесть все возможные варианты исполнения путем введения не коммутирующих операций и классификации вариантов совместного з исполнения на их основе. Для определен-s£ ных задач анализ модели исполнения по-ss зволяет свести к нулю возможность ошиб-| ки false-negative (что очень важно). Однако | класс задач, для которых метод хорошо ра-
£ ботает, довольно узок. §
<и
I II. Учет значений переменных
&
Ц Возьмем алгоритм взаимного исключе-
¡^ ния Петерсона. Критическая секция явля-
§ ется таковой за счет проверки значений
g переменных, ограничивающих исполнения
g для всех потоков, кроме того, что находит-
§ ся в критической секции. Если статический
^ анализ не включает контроль значений,
Ц критическая секция с точки зрения такого
анализа не является критической. В общем
§ случае задача контроля переменных рав-
g носильна полноценному исполнению про-
£ граммы и вычислению значений всех пере-s
¿3 менных.
Предлагаемый подход
I. Представление ветвлений
Простейшее ветвление, которое можно представить — одиночный оператор if следующего вида: if (condition) { operation;}. Отметим, что одна из идей построения графа совместного исполнения потоков — однозначное отображение один в один инструкций исходного кода на вершины, расположенные по периметру графа. Именно такое построение обеспечивает линейную по набору инструкций сложность в рамках каждого потока. В случае простейшего ветвления имеется набор операций, которые могут исполняться в зависимости от выполнения условия condition. Используем принцип неопределенных коэффициентов. Введем бинарный коэффициент в е {0,1}, чтобы обозначить «попадание» в одну из ветвей — все модификации значений внутри ветви будут иметь множитель р.
Например, пусть в двух потоках исполняется следующий код при начальном значении х = 0:
if (x == 0) { x += 2;}.
С точки зрения графа он будет иметь вид ребра, на котором значение x меняется на величину 2 • р.
Итак, в финальной вершине графа G значение i-й переменной равно f(х0,{а},{р}). Анализ на предмет наличия гонки проводится относительно неопределенных коэффициентов {р} аналогично анализу относительно коэффициентов {а}. Согласно обозначениям, принятым в [1] и [2], в вершине графа ставятся значения разделяемых переменных, а ребра подписываются соответствующей операцией. Для рассмотренного примера получили значение разделяемой переменной 2р1 + 2р2 (рис. 1). Коэффициенты в выражении определяют ветвь, для которой моделируется исполнение. Существуют такие значения неопределенных коэффициентов, при которых разделяемая
№ 6 (36) 2011
{0}
{О + 2Р, + 2Рг}
Рис.1.Граф совместного исполнения двух потоков на разделяемой памяти
переменная принимает различные значения, т. е. присутствует неразрешенное условие гонки.
Обращаем внимание, что коэффициенты {а} отвечают за очередность исполнения операций, а {в} — за путь по ветвлениям.
В общем случае любое ветвление является композицией элементарных ветвлений. Также необходимо заметить, что в случае одиночного if в финальное выражение коэффициент в может войти как множитель, что говорит о вариативности исполнения кода. В случае конструкции if - else в финальное выражение войдут члены с коэффициентами в и (1 - в), отражающие тот факт, что выполняться будет блок кода либо под if, либо под else.
Корректность описанного представления докажем в следующей теореме.
Теорема. Наличие гонки в предлагаемой модели с ветвлениями эквивалентно наличию гонки в исходной задаче.
Доказательство. Фактически формулировка теоремы означает, что предложенное выше представление корректно описывает ветвление в терминах и смысле анализа графа совместного исполнения потоков. Зафиксируем значения коэффициентов {в} в формуле f (1). При этом анализируем линейный участок кода (пути по ветвлениям описаны коэффициентами {в}) способом, изложенным в [1]. Далее, если,
3{р},/,{а}1,{а}2 :f (*0,{аМР}) * f(*о,М2,т | это означает, что есть два пути по ветвлени- ^ ям, которые дают разные значения разде- ¡^ ляемой переменной номер /. Обратно: пусть ^ есть два пути, приводящих к гонке. В соот- Ц*
ветствии с формулой (2) §
О
&
3/,{а}1,{а}2 : f (^МО * f(*о,{аЬ) Ц
со
оба пути описываются с помощью общего оо подхода к ветвлениям коэффициентами {в}. * Таким образом, наличие гонки в модели эквивалентно наличию гонки в задаче.
II. Представление циклов
Исходная задача по анализу циклов несколько сложнее, чем задача о ветвлениях, потому что циклы подразумевают переходы назад, а анализ графа совместного исполнения потоков является нисходящим. Итак, в [2] нет формального описания анализа циклов. Введем формальное правило анализа цикла и докажем его корректность с точки зрения поиска гонок.
Теорема. Для описания цикла в анализируемом графе достаточно одного повторения тела цикла.
Доказательство. Применим метод доказательства от противного. Допустим, что необходимо повторение тела цикла два раза, чтобы обнаружить наличие гонки.
На рисунке 2 жирной линией обозначен произвольный путь на графе. По предположению два подобных этому пути приводят к гонке, тогда как никакие два пути в графе с одним повторением тела цикла к гонке не приводят. Рассматриваем отдельно каждый подграф, соответствующий одному повторению тела цикла и учитываем, что гонка в нем отсутствует. Поскольку в анализе используются только попарные сравнения, а не последовательность использования операций, то из отсутствия гонки в каждой из подзадач следует отсутствие гонки в общей задаче, что противоречит исходному предположению. Таким образом, предположение о том, что надо повторять граф совместного исполнения с повторением более одного раза тела цикла, неверно. Теорема доказана.
№ 6 (36) 2011
Тело цикла Тело цикла
Остальной код
Остальной код
Рис. 2. Представление графа с несколькими повторениями тела цикла в виде подграфов
с одним повторением в каждом
I
I I
5 <и
t ¡5
u
if
an *
0
1
I £
0 <u
1 is is is
и
Ее можно обобщить на случай n > 2 повторений. Отметим, что при замене цикла на графе телом цикла исчезают обратные переходы, что позволяет воспользоваться разработанным в [2] подходом.
Разберем простейший пример применения подхода. Пусть в двух потоках исполняется следующий код:
while (true) { if (i == 0) {i = 1; x++}; else break;}.
Исходное состояние: x = 0; i = 0. Функция корректности имеет следующий вид:
c( x, i) =
true,x = 1 false, x ф 1
(3)
Используем для анализа подходы, изложенные в настоящей работе: оставляем тело цикла и приписываем каждому из ветвлений коэффициент. Строим граф совместного исполнения потоков и отмечаем на нем операции чтения и записи. Берем путь, не идущий по внешним ребрам графа, и выписываем значения переменных с помощью метода неопределенных коэффициентов: i = а1, x=а1 + а2, откуда при а1 = а2 = 1 получим значение x, при котором функция корректности имеет значение false. Значит, состояние гонки присутствует относительно переменной x.
42
Заключение
Итак, реальные практические программы, содержащие циклы и ветвления, могут быть проанализированы в рамках освещаемого подхода. Если раньше в [3] процедура анализа реального кода, содержащего циклы и ветвления, не была полностью описана в рамках существующих подходов, то теперь такое описание появилось. Кроме того, доказана его корректность в рамках применяемого подхода. Расширенный подход создает хорошую базу для дальнейших разработок средств детектирования гонок.
Список литературы
1. Заборовский Н. В., Тормасов А. Г. Моделирование многопоточного исполнения программы и метод статического анализа кода на предмет состояний гонки // Прикладная информатика. 2011. № 4 (34). С. 105 - 110.
2. Кудрин М. Ю., Прокопенко А. С., Тормасов А. Г. Метод нахождения состояний гонки в потоках, работающих на разделяемой памяти // Труды МФТИ. 2009. Т. 1. № 4. С. 182 - 201.
3. Gao H, Groote J. and Hesselink W. Almost Wait-Free Resizable Hashtables // In Proceedings of the 18th International Parallel and Distributed Processing Symposium, April. 2004. Р. 50.
4. Herlihy M, Shavit N. The Art of Multiprocessor Programming. Burlington: Elsevier, 2008.