Статья рассказывает о новом направлении в развитии статических анализаторов кода - верификации параллельных программ. В статье рассказывается о нескольких статических анализаторах, которые могут претендовать на звание "Parallel Lint".
Поддержка VivaMP была прекращена в 2014 году. По всем возникшим вопросам вы можете обратиться в нашу поддержку.
Статья ориентирована на разработчиков параллельных Windows приложений, использующих языки Си/Си++, но может быть полезна широкому кругу читателей интересующихся вопросами статического анализа кода.
Статический анализ кода (англ. static code analysis) представляет собой процесс верификации программного обеспечения без реального выполнения исследуемых программ. Обычно анализу подвергается непосредственно исходный код программы, хотя иногда анализу подвергается какой-нибудь вид объектного кода [1].
Статический анализ является разновидностью проверки программного обеспечения с помощью обзора кода (англ. code review). Но если в случае обзора кода, весь проверяемый код должен быть просмотрен непосредственно человеком. То в случае статического анализа, часть работы берет на себя специализированное программное обеспечение, называемое статическим анализатором. Статический анализатор выявляет потенциально опасные места в тексте программы и предлагает эти участки для дальнейшего анализа и исправления человеку, тем самым существенно сокращая объем работы по просмотру кода.
Одним из самых первых и самым известным статическим анализатором является утилита lint, появившаяся в 1979 г. в составе дистрибутива операционной системы Unix 7 в качестве основного инструментального средства контроля качества ПО на языке Си [2, 3]. Этот инструмент настолько известен, что слово "lint" стало почти синонимом понятия "статический анализатор". И очень часто можно прочитать не "инструмент статического анализа", а "lint-подобный инструмент".
Со временем появились новые мощные средства статического анализа, как общего, так и специализированного назначения: Coverity Prevent, PC-Lint, KlocWork K7, PolySpace, Viva64, FXCop, C++Test, JLint, UNO и многие другие.
Эти статические анализаторы помогают обнаружить огромное количество ошибок, разновидности которых даже трудно начать перечислять. Они постоянно совершенствуются и учатся диагностировать все новые классы ошибок. Одним из таких новых направления является верификация параллельных программ.
С появлением многоядерных микропроцессоров параллельное программирование вышло за границы узкоспециализированных решений и быстро распространяется в области разнообразнейших прикладных приложений. Параллельное программирование требует специализированных инструментов для создания, тестирования и отладки программ. И если с технологиями создания параллельных приложений дела обстоят достаточно хорошо, то в области их верификации и тестирования существуют достаточно большие пробелы. Именно это подталкивает разработчиков статических анализаторов осваивать эту достаточно новую для них территорию - территорию верификации параллельных приложений. Пришло время новых решений. Пришло время "Parallel Lint".
Parallel Lint - это не название конкретного инструмента. Но это очень хорошая фраза, просто и коротко описывающая новый класс инструментов для анализа параллельного кода. В этой статье читатель познакомиться с несколькими инструментами, в которых реализована диагностика нового класса параллельных ошибок и которых по праву можно назвать "Parallel Lint".
В девятой версии Gimpel Software PC-lint реализована диагностика ошибок в параллельных программах, построенных на основе технологии POSIX threads или схожих с ней. Другими словами инструмент PC-Lint не ориентирован на конкретную параллельную технологию и может быть адаптирован пользователем для программ, основанных на различных библиотеках, реализующих параллельность. Такая гибкость реализуется за счет расстановки специализированных вспомогательных комментариев для PC-Lint в коде и его настройки. Как всегда за гибкость приходится платить повышенной сложностью в использовании.
Впрочем, сложная настройка потребуется, только если вы используете незнакомую PC-Lint параллельную технологию. PC-Lint поддерживает POSIX threads и может самостоятельно обнаружить ошибки связанные с блокировками, если будут использоваться такие функции, как pthread_mutex_lock() и pthread_mutex_unlock().
Но в любом случае, PC-Lint потребует подсказок от программиста и без них он будет беспомощен. Все дело в том, что статический анализатор не знает, выполняется ли определенный участок кода параллельно и может только строить некоторые предположения. Не так просто бывает понять, будет ли параллельный вызов или нет. Вызов может сложным образом зависеть от логики алгоритма и входных данных, которыми статический анализатор не обладает.
Возьмем пример функции (приведенный в руководстве к PC-Lint версии 9.0):
void f()
{
static int n = 0;
/* ... */
}
Проблема состоит в том, что если функция f() будет вызвана из параллельных потоков, то может возникнуть ошибка инициализации переменной 'n'. Для диагностики данной ситуации, необходимо явно указать анализатору PC-Lint, что функция f() может вызываться параллельно. Для этого необходимо использовать конструкции вида:
//lint -sem(f, thread)
Тогда, при диагностике кода, вы получите предупреждение: Warning 457: "Thread 'f(void)' has an unprotected write access to variable 'n' which is used by thread 'f(void) .
Если механизм параллельности построен не на POSIX threads, то вам потребуется использовать специальные дополнительные директивы, чтобы подсказать PC-Lint какие функции приводят к блокировке и разблокировке:
-sem(function-name, thread_lock)
-sem(function-name, thread_unlock)
В этом случае можно обнаружить ошибки в коде, подобному следующему:
//lint -sem( lock, thread_lock )
//lint -sem( unlock, thread_unlock )
extern int g();
void lock(void), unlock(void);
void f()
{
//-------------
lock();
if( g() )
return; // Warning 454
unlock();
//-------------
if( g() )
{
lock();
unlock();
unlock(); // Warning 455
return;
}
//-------------
if( g() )
lock();
{ // Warning 456
// do something interesting
}
}
Анализатор PC-Lint может эффективно находить ошибки связанные с параллельностью, если предварительно сделать ему все необходимые "подсказки". В противном случае он может протестировать параллельный код, считая его последовательным и тем самым не обнаружив в нем ряд ошибок.
Не смотря на необходимость проделать дополнительную работу по расстановке подсказок, инструмент PC-Lint является крайне удобным и мощным инструментом, способным выявлять многие параллельные ошибки. Особенно его удобно использовать в сочетании с оболочкой Visual Lint, предоставляющей более удобный пользовательский интерфейс к этому анализатору.
Более подробно о диагностических возможностях PC-Lint 9.0 можно узнать в руководстве по использованию PC-lint/FlexeLint 9.0 Manual Excerpts [4].
Из недостатков можно отметить, что PC-Lint не умеет диагностировать параллельный код, построенный на основе технологии OpenMP. Он не обрабатывает директивы OpenMP и настройки и подсказки здесь помочь не смогут. Но для диагностики OpenMP программ существуют другие статические анализаторы, о которых мы расскажем далее [5].
VivaMP это мощный специализированный инструмент, предназначенный для верификации кода приложений, построенных на основе технологии OpenMP. Этот инструмент изначально разрабатывался для проверки параллельного OpenMP кода, поэтому он более всех может претендовать на звание "Parallel Lint".
Анализатор интегрируется в среду Visual Studio 2005/2008 и позволяет сразу приступить к работе, не требуя долгой настройки или размещения каких-то бы ни было комментариев в коде программы. Это является большим преимуществом инструмента, так как позволяет легко попробовать и освоить продукт. Анализатор VivaMP, как и любой другой статический анализатор, в процессе работы потребует дополнительной настройки, чтобы уменьшить количество ложных срабатываний. Но это неизбежная жертва, которые взимают все статические анализаторы.
Возможность избежать утомительной настройки анализатора связана с особенностью технологии OpenMP. Описание директив OpenMP само по себе является поясняющим комментарием для анализатора, рассказывающее о структуре программы: какая часть кода будет выполняться параллельно, какие ресурсы будут локальными, какие общие и так далее. Все это позволяет произвести достаточно подробный анализ, не требуя помощи со стороны программиста.
Продемонстрируем принципы работы VivaMP на нескольких простых примерах.
Пример N1.
#pragma omp parallel for
for (size_t i = 0; i != n; ++i)
{
float *array =
new float[10000]; // V1302
delete [] array;
}
Анализатор VivaMP диагностирует в данном коде ошибку: "V1302. The 'new' operator cannot be used outside of a try..catch block in a parallel section." Ошибка связанна с выбрасыванием исключения из параллельного блока. Согласно спецификации OpenMP, если вы используете исключения внутри параллельного блока, то все эти исключения должны быть обработаны внутри этого блока. Если вы используете внутри параллельного кода оператор new, то вы должны позаботиться о перехвате исключения, которое согласно стандарту языка Си++ будет сгенерировано при ошибке выделения памяти.
Данный пример приведет к некорректному поведению программы и, скорее всего, к ее аварийному завершению, если произойдет ошибка выделения памяти.
Исправление кода состоит в обработке исключений внутри параллельного блока и передаче информации об ошибке через иные механизмы или в отказе от использования оператора 'new'.
Следующий исправленный код с точки зрения анализатора VivaMP будет безопасен:
#pragma omp parallel for
for (size_t i = 0; i != n; ++i)
{
try {
float *array =
new float[10000]; // OK
delete [] array;
}
catch (std::bad_alloc &) {
// process exception
}
}
Пример 2.
int a = 0;
#pragma omp parallel for num_threads(4)
for (int i = 0; i < 100000; i++)
{
a++; // V1205
}
Это пример классической ошибки состояния гонки (англ. race condition). Это ошибка программирования многозадачной системы, при которой работа системы зависит от того, в каком порядке выполняются части кода. Состояние гонки возникает тогда, когда несколько потоков многопоточного приложения пытаются одновременно получить доступ к данным, причем хотя бы один поток выполняет запись. Состояния гонки могут давать непредсказуемые результаты, и зачастую их сложно выявить. Иногда последствия состояния гонки проявляются только через большой промежуток времени и в совсем другой части приложения. Кроме того, ошибки такого рода невероятно сложно воспроизвести повторно. Для предотвращения состояния гонки используются приемы синхронизации, позволяющие правильно упорядочить операции, выполняемые разными потоками.
Анализатор VivaMP диагностирует в данном коде ошибку: V1205. Data race risk. Unprotected concurrent operation with the "a" variable. Поскольку все потоки пишут в одну и ту же область памяти и читают из нее одновременно, значение переменной после этого цикла является непредсказуемым. Чтобы обезопасить эту операцию, ее нужно поместить в критическую секцию, либо (поскольку в данном примере операция является элементарной) использовать директиву "#pragma omp atomic":
int a = 0;
#pragma omp parallel for num_threads(4)
for (int i = 0; i < 100000; i++)
{
#pragma omp atomic
a++; // OK
}
Более подробно познакомиться с типовыми ошибками, возникающими при разработке OpenMP приложений, можно в статье "32 подводных камня OpenMP при программировании на Си++" [6]. Диагностика большинства из описанных в статье ошибок реализована в анализаторе VivaMP или появится в новых версиях.
Диагностика многих параллельных OpenMP ошибок реализована в компиляторе Intel C++ версии 11.0. Угадайте, как называется этот новый механизм в компиляторе. Правильно, новая функциональная возможность компилятора получила название "Parallel Lint".
Статический анализатор, встроенный в компилятор Intel C++ позволяет диагностировать различные ошибки синхронизации, гонки данных и так далее. Для этого компилятору необходимо указать ключ /Qdiag-enable:sc-parallel{1|2|3}, где цифры задают уровень анализа. Дополнительным параметром, важным при анализе параллельных программ, является ключ компилятора /Qdiag-enable:sc-include, который указывает, что необходимо так же осуществлять поиск ошибок в заголовочных файлах.
В режиме проверки параллельной программы компилятор не генерирует исполняемый код. На этапе компиляции будут созданы специальные псевдо объектные файлы (*.obj), содержащие вместо объектного кода структуры данных включающие информацию, необходимую для выполнения анализа. Затем эти файлы поступают на вход статического анализатора, который осуществляет анализ параллельного кода.
Как уже сказано выше, при настройках компилятора для статического анализа параллельных OpenMP программ, генерация исполняемого кода не производится. Поэтому удобным решением будет завести отдельные конфигурации для сборки проекта и для его статического анализа.
В результате, разработчик, например, сможет обнаружить ошибку в следующем коде:
#include <stdio.h>
#include "omp.h"
int main(void)
{
int i;
int factorial[10];
factorial[0]=1;
#pragma omp parallel for
for (i=1; i < 10; i++) {
factorial[i] = i * factorial[i-1]; // warning #12246
}
return 0;
}
Компилятор Intel C++ в режиме статического анализа выдаст следующее предупреждение: omp.c(13): warning #12246: flow data dependence from (file:omp.c line:13) to (file:omp.c line:13), due to "factorial" may lead to incorrect program execution in parallel mode.
Более подробно вы можете узнать о технологии статического анализа Intel C++, посмотрев вебинар Дмитрия Петунина "Static Analysis and Intel® C/C++ Compiler ("Parallel Lint" overview)", который вы сможете найти в архивах на сайте Intel.
Как видите, функциональность статического анализатора в Intel C++ схожа с функциональностью VivaMP. Сложно сказать, какой из анализаторов лучше и больше подойдет для ваших задач. С одной стороны анализатор в Intel C++ является средством, которое вы получаете вместе с компилятором. С другой стороны в компиляторе Intel C++ много разнообразнейших возможностей и "Parallel Lint" всего лишь одна из них, в то время как анализатор VivaMP является узкоспециализированным активно развивающимся продуктом.
Скорее всего, анализатор VivaMP будет удобен разработчикам, использующим Visual C++, так как позволяет выполнять анализ, не требуя каких-либо изменений в конфигурациях проекта и в исходном коде. Чтобы воспользоваться возможностями Intel C++ разработчикам вначале будет необходимо адаптировать свой проект для сборки его данным компилятором. Поэтому воспользоваться "Parallel Lint" в Intel C++ в первую очередь будет удобно тем разработчиком, которые уже используют компилятор Intel C++ для сборки своих приложений.
Конечно, существуют и другие инструменты, которые тоже могут претендовать на звание "Parallel Lint". Но эти инструменты в своем большинстве представляют собой динамические анализаторы или реализуют сочетание статического и динамического анализа. Поэтому рассказать о них в этой статье было бы не совсем корректно. И хочется пожелать всем читателям надежного параллельного кода!
0