Тестирование параллельного программного обеспечения представляет собой более сложную задачу по сравнению с тестированием последовательной программы. Программист должен знать о подводных камнях при тестировании параллельного кода, имеющихся методологиях и инструментарии.
Говоря о разработке программного обеспечения, выделяют этап тестирования и отладки приложения. Понятия тестирование и отладка слабо разделяют между собой. Это связано с тем, что после нахождения ошибки при тестировании ее устранение часто связано с отладкой программы. А отладку приложения в свою очередь можно называть тестированием методом белого ящика. Иногда под этапом отладки понимают одновременно и поиск и устранение ошибок.
Мы все-таки разделим эти два понятия, и сосредоточимся в этой статье на тестировании параллельного программного обеспечения.
Тестирование программного обеспечения - процесс выявления ошибок в программном обеспечении с использованием различных инструментов и анализ работоспособности программы конечными пользователями [1].
Отладка - процесс обнаружения программистом в коде ошибок, вывяленных в ходе тестирования программного обеспечения с целью их дальнейшего устранения. Отладка обычно подразумевает использование специализированных инструментов для отслеживания состояния программы в ходе ее исполнения.
Отладка параллельных программ - дело неблагодарное, требующее аккуратности и специализированных инструментов. Отладке параллельных программ посвящено много статей и следует посветить еще больше, поскольку данная тематика весьма актуальна в связи с активным развитием многоядерных систем и новых технологий создания параллельных программ. В области инструментария также существуют пробелы. Но прежде чем заняться отладкой - ошибку нужно найти. При этом некоторые методы отладки не только обнаруживают ошибку, но и сразу локализуют место ее нахождения. Поэтому займемся тестированием.
Тестирование приложений с параллельной обработкой - задача непростая. Ошибки распараллеливания сложно выявить из-за недетерминированности поведения параллельных приложений. Даже если ошибка обнаружена, ее часто сложно воспроизвести повторно. Кроме того, после модификации кода, не так просто убедиться, что ошибка действительно устранена, а не замаскирована. Все это можно назвать и по другому, а именно, что ошибки в параллельной программе являются классическими "гейзенбагами".
Гейзенбаг (англ. Heisenbug) - термин, используемый в программировании для описания программной ошибки, которая исчезает или меняет свои свойства при попытке её обнаружения [2]. Данное название является игрой слов и происходит от физического термина "Принцип неопределённости Гейзенберга", который на бытовом уровне понимается как изменение наблюдаемого объекта в результате самого факта наблюдения, происходящее в квантовой механике. В русской терминологии более часто используется термин "плавающая ошибка". Примером могут являться ошибки, которые проявляются в окончательном варианте программы ("релизе"), однако не видны в режиме отладки, или ошибки синхронизации в многопоточном приложении.
Таким образом, задача параллельного тестирования во многом сводится к проблеме создания инструментов диагностики, минимально влияющих на поведение программы или создающих необходимые условия для ее проявления. Поэтому посмотрим на классические методологии тестирования под новым углом.
Методики поиски ошибок в параллельных приложениях, как и в последовательных можно разделить на динамический анализ, статический анализ, проверку на основе моделей и доказательство корректности программы.
Формальное доказательство корректности программы является крайне сложной процедурой, особенно когда речь заходит о параллельном программировании и практически не применяется промышленной разработки программного обеспечения.
Динамический анализ подразумевает под собой необходимость запуска приложения и выполнения различный последовательностей действий, целью которых ставится выявление некоренного поведения программы. Последовательность действий может задаваться как человеком при ручном тестировании, так и с использованием различных инструментов реализующих нагрузочное тестирование или, например, проверку целостности данных. В случае параллельных программ наибольший интерес представляют инструменты подобные Intel Thread Checker, которые оснащают исходный код приложения средствами мониторинга и протоколирования, которые позволяют выявлять взаимоблокировки (как явные, так и потенциальные), зависания, гонки и так далее [3].
Статический анализ работает только с программным кодом приложения, не требуя его запуска. В качестве преимущества можно отметить детальность и полноту охвата анализируемого кода. Следует отметить, что в сфере параллельного программирования методология статического анализа используется достаточно редко и представлены малым количеством инструментов, которые являются скорее исследовательскими, чем коммерческими продуктами.
Проверка на основе модулей представляет собой автоматическую генерацию тестов по заданным правилам. Проверка на основе моделей позволяет формально обосновать отсутствие дефектов в тестируемой части кода, на основе заданной разработчиком правил преобразования данных. В качестве примера можно назвать инструмент KISS, разработанный в Microsoft Research для параллельных программ на C.
Все перечисленные методики имеют свои недостатки, что не позволяет положиться при разработке параллельных программ только на одну из них.
Динамический анализ требует запуска программ, чувствителен к среде исполнения, существенно замедляет скорость выполнения приложения. Достаточно трудно осуществить покрытие тестами всего параллельного кода. Часто зафиксировать состояние гонки (race conditions) удается, только если оно было в данном сеансе работы программы. То есть, если средство динамического анализа сообщает, что ошибок нет, вы все равно не можете быть уверены в этом.
В случае параллельных приложений статический анализ крайне сложен и часто невозможно предсказать поведение программы, так как неизвестен допустимый набор входных значений для различных функций и способ их вызова. Эти значения можно прогнозировать на основе остального кода, но крайне ограниченно, так как возникает огромное пространство возможных состояний и объем проверяемой информации (вариантов) увеличивается до недопустимых значений. Также средства статического анализа часто дают большое количество ложных сообщений о потенциальных ошибках и требуют немалых усилий для их минимизации.
На практике проверка на основе моделей действенна лишь для небольших базовых блоков приложения. В большинстве случаев очень сложно автоматически построить модель на основе кода, а создание моделей вручную - крайне трудоемкое и ресурсоемкое занятие. Фактически необходимо написать те же алгоритмы преобразования данных, но в ином представлении. Как и в случае статического анализа возникает проблема быстрого расширении пространства состояний. Расширение пространства состояний можно в частично контролировать, применяя методы редукции со сложной эвристической логикой, но это приводит к тому, что некоторые дефекты будут пропущены. В качестве примера инструмента проверки для тестирования проектов параллельных приложений на основе моделей можно назвать Zing.
Как видно, использование какого-то одного подхода к тестированию параллельных приложений неэффективно и хорошим решением является использовать несколько методик для тестирования одного и погоже программного продукта.
Поскольку при решении задачи увеличения производительности выбор был сделан в пользу многоядерных процессоров, то это повлекло и новый виток в развитии инструментальных средств разработки. Для многоядерных систем с общей памятью более удобным оказывается использование таких технологий программирования, как OpenMP, вместо более привычных MPI или стандартных средств распараллеливания, предоставляемых операционными системами (fork, beginthread).
Для новых технологий нужны и новые инструменты их поддержки. Сейчас следует активно следить за новыми системами поддержки параллельного программирования, появляющимися на рынке программного обеспечения. Они могут существенно облегчить ваш труд и быстрее адаптировать ваши приложения для эффективного использования параллельной среды.
Как уже было сказано, статический анализ параллельных программ сложен и малоэффективен, так как необходимо хранить крайне много информации о возможных состояниях программы. Это совершенно справедливо при использовании таких технологий параллельного программирования как MPI или распараллеливания средствами операционной системы.
С технологией OpenMP ситуация обстоит лучше и часто можно реализовать эффективный статический анализ, обладающий хорошими показателями. Это связано с тем, что технология OpenMP ориентирована на распараллеливание изолированных участков кода. OpenMP как бы позволяет делать программу параллельной "по кусочкам", с помощью расстановки специальных директив в наиболее критичным по быстродействию частям кода. В результате параллельный код оказывается сгруппирован и не зависит от других частей приложения, что позволяет провести его качественный анализ.
До недавнего времени направление статического анализа OpenMP программ практически было не освоено. В качестве примера можно привести, пожалуй, только достаточно качественную диагностику выполняемую компилятором Sun Studio. Статический анализатор VivaMP заполнил это нишу. Это специализированный инструмент для поиска ошибок в параллельных программах, разработанных с использованием технологии OpenMP на языке Си и Си++ [5].
Данный анализатор как обнаруживает явные ошибки, так и предупреждает о потенциально опасном коде. В качестве диагностики ошибок можно привести пример обнаружения использование одной переменной для записи из параллельных потоков без необходимой синхронизации:
int sum1 = 0;
int sum2 = 0;
#pragma omp parallel for
for (size_t i = 0; i != n; ++i)
{
sum1 += array[i]; // V1205
#pragma atomic
sum2 += array[i]; //Fine
}
А в качестве примера диагностики потенциально опасно опасного кода можно привести пример использования flush для указателя. Несложно ошибиться, забыв, что операция flush будет применена именно к указателю, а не к данным, на которые он ссылается. В результате приведенный код может быть как корректным, так и некорректным:
int *ptr;
...
#pragma omp flush(ptr) // V1202
int value = *ptr;
В следующей версии анализатора VivaMP также будут реализованы некоторые проверки связанные с выявлением неэффективного параллельного кода. Например, будут критические секции там, где было бы достаточно использовать более быструю директиву atomic:
#pragma omp critical
{
a++; //Slow
}
#pragma omp atomic
a++; //Good
Различные формы распараллеливания существуют в мире программного обеспечения уже давно. Однако для создания массовых коммерческих приложений, использующих возможности многоядерных процессоров в полной мере, требуются иные методы разработки, отличные от применяемых при создании последовательных приложений. Хочется надеяться, что данная статья прольет свет на те сложности, с которыми связана разработка параллельных приложений, и программист со всей серьезностью отнесется к выбору наиболее подходящих средств разработки и тестирования таких программ.