Холодный 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 obter mais informações sobre otimizações de compiladores, consulte Aviso sobre otimizações.