﻿# Холодный Tachyon

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


> Поддержка OpenMP была прекращена в PVS\\\-Studio после версии 5\\\.20\\\. По всем возникшим вопросам вы можете обратиться в нашу \[поддержку\]\(https://pvs\-studio\.ru/ru/about\-feedback/\)\\\.

Например, распараллелить\. В качестве демонстрационного примера была выбрана программа Tachyon, реализующая алгоритм трассировки лучей и рисующая на экране трехмерный фрактал\. В качестве инструментария был выбрана технология параллельного программирования OpenMP, компилятор [Intel C\+\+](https://software.intel.com/content/www/us/en/develop/tools/parallel-studio-xe.html), профилировщик многопоточных приложений [Parallel Amplifier](https://software.intel.com/content/www/us/en/develop/tools/vtune-profiler.html) и инструмент для поиска параллельных ошибок [Parallel Inspector](https://software.intel.com/content/www/us/en/develop/tools/inspector.html)\. После семинара появилась еще одна запись "[ По теплым следам онлайн семинара "Intel\(R\) Parallel Studio workflow](https://software.intel.com/content/www/us/en/develop/tools/parallel-studio-xe.html)""

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

Давно было желание привести новый пример использования анализатора VivaMP, входящего в состав [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/) для выявления ошибок в параллельном коде\. Оказалось, что этот альтернативный подход к анализу очень хорошо можно продемонстрировать на примере Tachyon\. Напомню, что в вебинаре для поиска параллельных ошибок использовался инструмент Parallel Inspector\. Диагностика ошибок происходила в несколько этапов\. Мы продемонстрируем на этих этапах работу инструмента VivaMP\. 

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

```cpp
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\.

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

```cpp
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, было выяснено, что в коде возникает несколько ошибок [состояния гонки](https://pvs-studio.ru/ru/blog/terms/0042/) \(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, код был изменен следующим образом:

```cpp
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

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

```cpp
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 константным\. То есть заменить

```cpp
ray  camray(scenedef *, int, int);
```

на

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

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

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