>
>
Холодный Tachyon

Андрей Карпов
Статей: 674

Холодный Tachyon

Чуть более месяца назад состоялся первый русскоязычный онлайн-семинар от Intel "Intel Parallel Studio workflow". На нем Кирилл Мавродиев продемонстрировал, как можно распараллелить приложение, рассматривая его как черный ящик. Другими словами, была рассмотрена типичная ситуация, когда у разработчика имеется незнакомый ему код, которые необходимо модернизировать.

Поддержка OpenMP была прекращена в PVS-Studio после версии 5.20. По всем возникшим вопросам вы можете обратиться в нашу поддержку.

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

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

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

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

TachyonStep1\trace.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.

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

TachyonStep2\trace.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, код был изменен следующим образом:

TachyonStep2\trace.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

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

TachyonStep2\trace.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 решена альтернативным методом. Показанный способ, не лучше и не хуже. Статический анализ быстр, позволяет искать многие ошибки еще на этапе кодирования. Динамический анализ диагностирует большее количество ошибок и с большей достоверностью. Эти два метода могут успешно дополнять друг друга на различных этапах разработки программного обеспечения.