\subsection{Общие сведения и термины из параллельного программирования}

Когда речь идёт о высокоуровневых технологиях распараллеливания (OpenMP, OpenACC, DVM), говорят о распараллеливании витков циклов. На это есть несколько причин:

\begin{itemize}
    \item обычно, большая часть вычислительной работы в программах находится именно в циклах;

    \item анализировать абстрактные последовательности операторов с точки зрения возможности их параллельного выполнения сложнее, чем анализировать зависимости между витками циклов;

    \item явное указание распараллеливаемых частей кода влечёт за собой изменения в структуре кода, в то время как указание параллельного цикла обычно выглядит как директива перед его заголовком.
\end{itemize}

Более того, принято рассматривать только циклы, находящиеся в \textit{канонической форме}, то есть циклы, у которых есть итерационная переменная, проходящая заданный диапазон значений с заданным шагом:

\begin{lstlisting}[style=FORT,caption={
Пример цикла в канонической форме на языке Фортран с итерационной переменной \texttt{i}, принимающая все значения от 1 до 10.
}]
    DO i = 1, 10
        ...
    ENDDO
\end{lstlisting}

Иногда приходится иметь дело с вложенными циклами, у которых нет операторов между заголовками и концами циклов. Их совокупность называют \textit{гнездом тесновложенных циклов}, или просто гнездом циклов. С точки зрения параллелизма, в общем случае выгоднее распараллеливать не только самый верхний цикл в гнезде, а гнездо целиком, увеличивая тем самым итерационное пространство.

\begin{lstlisting}[style=FORT,caption={
Пример гнезда из трёх тесновложенных циклов.
}]
    DO i = 1, 10
        DO j = 2, 20
            DO k = 3, 30
            ...
            ENDDO
        ENDDO
    ENDDO
\end{lstlisting}

Будем говорить, что витки цикла (или гнезда циклов) имеют \textit{зависимость по данным}, если в нём существуют два витка $i_0$ и $i_1$ такие, что в последовательной программе виток $i_0$ записывает какие-либо данные, а виток $i_1$ их считывает. 

\pagebreak

\begin{lstlisting}[style=FORT,caption={
Пример цикла с зависимостью по данным, где каждый виток с номером $i$ записывает данные, которые читают витки $i+1$ и $i+2$.
}]
    DO i = 3, 10
        A(i) = A(i - 1) + A(i - 2)
    ENDDO
\end{lstlisting}

В общем случае такие циклы нельзя распараллеливать, так как в них важен порядок итераций, но существует несколько разных приёмов, позволяющих в некоторых случаях устранить зависимости по данным.

Важным этапом распараллеливания является определение того, какие данные являются локальными или общими по отношению к виткам циклов. Если скалярная переменная или массив перед использованием переопределяется (перезаписываются значения элементов соответственно) на той же самой итерации, то говорят, что такая переменная является \textit{приватной переменной} или \textit{приватным массивом} соответственно.

\begin{lstlisting}[style=FORT,caption={
Пример цикла по \texttt{i} с приватным массивом \texttt{TMP} и приватными переменными \texttt{i}, \texttt{j}, \texttt{t}.
}]
    DO i = 1, 10
        DO j = 1, 100
            TMP(j) = j
        ENDDO

        t = TMP(100)
    ENDDO
\end{lstlisting}


Часто в программах встречаются циклы, в которых выполняются \textit{редукционные операции} -- в некоторой переменной (\textit{редукционной}) суммируются элементы массива или вычисляется максимальное (минимальное) значение:

\begin{lstlisting}[style=FORT,caption={
Пример цикла с редукционной переменной \texttt{sum} (редукционная операция -- сложение).
}]
    sum = 0
    DO i = 1, 10
        sum = sum + A(i)
    ENDDO

    write(*, *) sum
\end{lstlisting}

Использование редукционных переменных приводит к появлению зависимостей между витками циклов. Но существуют приемы, которые позволяют справиться с такого вида зависимостями при распараллеливании программы. Например, создав для каждого потока (или процесса) копию переменной, можно объединить их значения по завершении выполнения цикла.