Холодный Tachyon

Чуть более месяца назад состоялся первый русскоязычный онлайн-семинар от Intel "Intel Parallel Studio workflow". На нем Кирилл Мавродиев продемонстрировал, как можно распараллелить приложение, рассматривая его как черный ящик. Другими словами была рассмотрена типичная ситуация, когда у разработчика имеется незнакомый ему код, которые необходимо модернизировать. Например, распараллелить. В качестве демонстрационного примера была выбрана программа Tachyon, реализующая алгоритм трассировки лучей и рисующая на экране трехмерный фрактал. В качестве инструментария был выбрана технология параллельного программирования OpenMP, компилятор Intel C++, профилировщик многопоточных приложений Parallel Amplifier и инструмент для поиска параллельных ошибок Parallel Inspector. После семинара появилась еще одна запись "По теплым следам онлайн семинара "Intel® Parallel Studio workflow""

Мы подумали и решили, раз был пост "по теплым следам", то этот текст уже следует назвать холодным. Есть и еще одна причина. Динамический анализ на этапе исполнения программы, осуществляемый Parallel Inspector, почему-то ассоциируется со словом "горячий". А альтернативный подход, который здесь будет продемонстрирован, основан на статическом анализе исходного кода и скорее ассоциируется со словом "холодный".

Давно было желание привести новый пример использования анализатора VivaMP, входящего в состав PVS-Studio для выявления ошибок в параллельном коде. Оказалось, что этот альтернативный подход к анализу очень хорошо можно продемонстрировать на примере Tachyon. Напомню, что в вебинаре для поиска параллельных ошибок использовался инструмент Parallel Inspector. Диагностика ошибок происходила в несколько этапов. Мы продемонстрируем на этих этапах работу инструмента VivaMP. Исходные коды проекта Tachyon на разных стадиях распараллеливания и оптимизации лежат здесь.

В начале, мы имеем последовательный код:

TachyonStep1trace.serial.cpp


unsigned int serial = 1;

unsigned int mboxsize = sizeof(unsigned int)*(max_objectid() + 20);

unsigned int * local_mbox = (unsigned int *) alloca(mboxsize);

memset(local_mbox,0,mboxsize);


for (int y = starty; y < stopy; y++) { {

    drawing_area drawing(startx, totaly-y, stopx-startx, 1);

    for (int x = startx; x < stopx; x++) {

       color_t c = render_one_pixel (x, y,

            local_mbox, serial, startx, stopx, starty, stopy);

       drawing.put_pixel(c);

   } }

   if(!video->next_frame()) return;

}


Код совершенно корректен и не вызывает подозрений ни у Parallel Inspector, ни у VivaMP.

Далее код был модифицирован следующим образом:

TachyonStep2trace.par1.cpp


unsigned int serial = 1;

int ison=1;

unsigned int mboxsize = sizeof(unsigned int)*(max_objectid() + 20);

unsigned int * local_mbox = (unsigned int *) alloca(mboxsize);

memset(local_mbox,0,mboxsize);


#pragma omp parallel for

for(int y = starty; y < stopy*ison; y++) { {

    drawing_area drawing(startx, totaly-y, stopx-startx, 1);

    for (int x = startx; x < stopx; x++) {

      color_t c = render_one_pixel (x, y,

            local_mbox, serial, startx, stopx, starty, stopy);

        drawing.put_pixel(c);

    } }

    if(!video->next_frame()) ison=0;

 }


Такое смелое распараллеливание привело к ошибке и построению некорректного изображения на экране. С помощью Parallel Inspector, было выяснено, что в коде возникает несколько ошибок состояния гонки (race condition). Результатом изучения диагностических сообщений Parallel Inspector было решение объявить переменные ison, local_mbox и serial приватными (shared), то есть уникальными для каждого потока. Именно при обращении к этим переменным возникали гонки. Теперь посмотрим, какие предупреждения для этого кода выдает анализатор VivaMP:

1 error V1206: Data race risk. The value of the "scene" variable can be changed concurrently via the "camray" function. r:\src\tachyonstep2\trace.par1.cpp 87

2 error V1206: Data race risk. The value of the "local_mbox" variable can be changed concurrently via the "render_one_pixel" function. r:\src\tachyonstep2\trace.par1.cpp 157

3 error V1206: Data race risk. The value of the "serial" variable can be changed concurrently via the "render_one_pixel" function. r:\src\tachyonstep2\trace.par1.cpp 157

4 error V1205: Data race risk. Unprotected concurrent operation with the "ison" variable. r:\src\tachyonstep2\trace.par1.cpp 160


Обратите внимания, что анализатор VivaMP предупредил о потенциальных ошибках гонки для тех же трех переменных: ison, local_mbox и serial. Есть еще одна потенциальная ошибка использования переменной "camray" в другой функции, но этот момент мы рассмотрим позже.

Основываясь на диагностике Parallel Inspector, код был изменен следующим образом:

TachyonStep2trace.par2.cpp


unsigned int serial = 1;

int ison=1;

unsigned int mboxsize = sizeof(unsigned int)*(max_objectid() + 20);

unsigned int * local_mbox = (unsigned int *) alloca(mboxsize);

memset(local_mbox,0,mboxsize);

#pragma omp parallel for firstprivate(ison,local_mbox,serial)

for(int y = starty; y < stopy*ison; y++) { {

  drawing_area drawing(startx, totaly-y, stopx-startx, 1);

  for (int x = startx; x < stopx; x++) {

    color_t c = render_one_pixel (x, y,

            local_mbox, serial, startx, stopx, starty, stopy);

    drawing.put_pixel(c);

  } }

  if(!video->next_frame()) ison=0

}


Измененная программа по-прежнему продолжает вести себя некорректно, хотя это и выражается иным образом. Повторный анализ с помощью Parallel Inspector выявляет ошибку совместного использования одного массива local_mbox. Директива firstprivate(local_mbox) создает уникальный указатель для каждого из потока. Но сам массив, на который ссылаются эти указатели, по-прежнему общий.

Анализатор VivaMP также смог диагностировать эту проблему (см. второе предупреждение):

1 error V1206: Data race risk. The value of the "scene" variable can be changed concurrently via the "camray" function. r:\src\tachyonstep3.1\trace.par2.cpp 87

2 error V1209: Warning: The "local_mbox" variable of pointer type should not be private. r:\src\tachyonstep3.1\trace.par2.cpp 153


Ниже приведен окончательный вариант функции, в которой устранены все дефекты, и программа корректно строит изображение фрактала:

TachyonStep2trace.par3.cpp


#pragma omp parallel

{

unsigned int serial = 1;

int ison=1;

unsigned int mboxsize = sizeof(unsigned int)*(max_objectid() + 20);

unsigned int * local_mbox = (unsigned int *) alloca(mboxsize);

memset(local_mbox,0,mboxsize);


#pragma omp for

for(int y = starty; y < stopy*ison; y++) { {

  drawing_area drawing(startx, totaly-y, stopx-startx, 1);

  for (int x = startx; x < stopx; x++) {

    color_t c = render_one_pixel (x, y,

            local_mbox, serial, startx, stopx, starty, stopy);

    drawing.put_pixel(c);

  } }

  if(!video->next_frame()) ison=0

 }

}


Это код уже не вызывает у Parallel Inspector подозрений. А вот VivaMP по-прежнему выдает одно диагностическое сообщение, являющееся ложным:

1 error V1206: Data race risk. The value of the "scene" variable can be changed concurrently via the "camray" function. r:\src\tachyonstep3.2\trace.par3.cpp 87


Это сообщение относится к вызываемой параллельно функции render_one_pixel. В ней VivaMP не может разобраться, что объект scene используется в нескольких потоках только для чтения. Можно легко убрать это сообщение, если объявить функцию чуть более изящно. Достаточно сделать параметр функции scenedef константным. То есть заменить

ray  camray(scenedef *, int, int);


на

ray  camray(const scenedef *, int, int);


После этой модификации анализатор VivaMP не выдаст более ни одного предупреждения.

Итак, задача поиска дефектов в параллельном коде Tachyon решена альтернативным методом. Показанный способ, не лучше и не хуже. Статический анализ быстр, позволяет искать многие ошибки еще на этапе кодирования. Динамический анализ диагностирует большее количество ошибок и с большей достоверностью. Эти два метода могут успешно дополнять друг друга на различных этапах разработки программного обеспечения.

Para obtener información más completa sobre las optimizaciones del compilador, consulte nuestro Aviso de optimización.
Etiquetas: