OpenMP Распараллелить цикл C++
Как эффективно распараллелить. Результаты выходят неверные.
#include #include #include using namespace std; int main() < int n=1000; int g = 1; int **A = new int*[1000]; for (int i = 0; i < n; i++) A[i] = new int[n+1]; for (int i = 0; i < n; i++) < for (int j = 0; j < n; j++) < A[i][j] = g; A[j][i] = g+2; // cout // cout cout > > for (int i = n-2; i < n; i++) < for (int j = n-2; j < n; j++) < cout cout for (int i = 0; i < n; i++) delete [] A[i]; delete [] A; >`
Отслеживать
задан 2 мар 2018 в 12:07
31 4 4 бронзовых знака
5. Распараллелить цикл: For(i=2;i
2 мар 2018 в 12:23
Да, опечатку исправила, неверный код отправила. Уже пыталась применять алгоритм чет-нечетной перестановки. Нашла другой алгоритм, все равно неверные данные выводятся. #pragma omp parallel for num_threads(4) for (int k=0; k <2; ++k) < int i1 = k/2; int j1 = k%2; for (int i = 2+i1; i < n; ++i) < for (int j = 2+j1; j < n; j+=2) < A[i][j] = A[i - 2][j] + A[i][j - 2]; >> >2;>
2 мар 2018 в 12:32
Так тоже не выйдет. 1. У Вас только 2 значения для разбивки на потоки, а Вы просите 4 потока. 2. int i1 = k/2; — у Вас всегда будет равно 0, т.к. k
2 мар 2018 в 12:41
@Регина, код с попытками решения стоит добавлять в вопрос (есть кнопочка «править»). Направление верное.
Немного об ускорении программы: распараллеливание (ручное или автоматическое) на базе сверхоптимистичных вычислений
Здравствуйте, уважаемые читатели. В этой публикации речь пойдет о такой (уже ставшей привычной) вещи как ускорение работы программы путем применения параллельных вычислений. Технологии организации таких вычислений известны – это и обычное многопоточное программирование, и применение специальных интерфейсов: OpenMP, OpenAcc, MPI, DVM и многих других (при этом распараллеливаются циклы, используется векторизация или конвейеризация, организуются ленивые вычисления, выделяются независимые блоки программы, которые можно запустить в параллель и т.п.).
При этом обычно исходят из той идеи, что распараллеливание не должно каким-то образом влиять на результаты исполнения программы. Это жесткое, но справедливое для многих случаев требование. Однако если мы пытаемся распараллелить программу, ведущую какие-либо расчеты численными методами (обучаем нейронную сеть, моделируем динамику жидкости или молекулярной системы, решаем обыкновенные дифференциальные уравнения или оптимизационные задачи), то результат и так (в любом случае) будет иметь некоторую погрешность. Поэтому, почему бы не применить «рискованные» технологии распараллеливания, которые могут внести в математическое решение небольшую дополнительную погрешность, но позволят получить еще некоторое дополнительное ускорение? Об одной из таких технологий – о расщеплении тел циклов с предсказанием промежуточных результатов и откатом при неудачном предсказании (собственно, это и есть «сверхоптимистичные» вычисления в частично транзакционной памяти) и пойдет речь.
Идея распараллеливания
Предположим, что мы имеем цикл, тело которого состоит из двух последовательных частей, причем вторая часть зависит от первой. Пусть и отдельные витки цикла зависят друг от друга. Например:
for (int i = 0; i
На первый взгляд, распараллелить такой цикл невозможно. Однако мы попробуем. Попытаемся исполнять параллельно первый и второй операторы тела цикла. Проблема состоит в том, что на момент вычисления g(x) должен быть известен x, но он будет рассчитан только в конце первой части. Что же, введем некоторую схему, которая в начале второй части попытается предсказать новое значение x. Можно это сделать, например, с помощью линейной предикции, которая «обучится» предсказывать новое значение x, опираясь на «историю» его изменения. Тогда вторую часть можно считать параллельно с первой (это и есть «сверхоптимизм»), а когда обе будут подсчитаны, сравнить предсказанное значение x с реальным, полученным в конце первой части. Если они примерно равны, то результат вычислений обеих частей можно принять (и перейти к следующему витку цикла). А если они сильно отличаются, то потребуется пересчитать только вторую часть. При такой схеме в какой-то части случаев получим чистое распараллеливание, в остальных – фактический последовательный счет. Алгоритм выполнения цикла при этом такой:
for (int i = 0; i < N; i++) < Распараллеливаем на два ядра < На ядре 1 – считаем x = f(y). Далее передаем во вторую часть получение значение x; На ядре 2 – предсказываем значение x* и считаем y* = g(x*). Получаем значение x из первой части и сравниваем его с x*. Если разница невелика, то y = y* и завершаем итерацию цикла. Если различие большое, повторяем вычисление с новыми данными: y = g(x). >>
Базовый алгоритм ясен. Теоретическое ускорение – в два раза, но на практике будет, конечно, меньше, поскольку: а) часть времени тратится на предсказания и согласования; б) не все итерации выполнятся параллельно; в) первая и вторая части тела цикла могут иметь различную трудоемкость (в идеале требуется равная). Перейдем к реализации.
Реализация распараллеливания — сверхоптимистичные вычисления
Поскольку в алгоритме распараллеливания идет речь об отмене части расчетов (при неудаче) и их повторном выполнении, здесь явно есть что-то от идеи работы в транзакционной памяти. Лучше – в частично транзакционной, где определенные переменные работают по схеме транзакционной памяти, а остальные переменные – как обычно. Передачу данных из первой части во вторую можно организовать с помощью некоторого специального канала. Пусть этот канал будет предсказывающим: а) если на момент приема данные в канал уже переданы, то они из него и читаются, б) если на момент приема данные в канал еще не поступили, то он пытается предсказать эти данные и возвращает результат предсказания. Этот канал и будет работать по немного не свойственной обычной транзакционной памяти схеме: если в конце транзакции второй части цикла обнаружится расхождение между поступившими в канал данными и предсказанными им данными, то транзакция второй части цикла отменяется и исполняется повторно, при этом из канала будут читаться уже не «предсказания», а реально пришедшие данные. Цикл приобретет вид:
for (int i = 0; i < N; i++) < Распараллеливаем на два ядра, включаем частично транзакционную память < Ядро 1 (транзакция 1): x = f(y); Предсказывающий_Канал.put(x); Ядро 2 (транзакция 2): Предсказывающий_Канал.get(x); y = g(x); >>
Готово. Заботу о предсказании данных взял на себя канал, заботу об отмене расчетов при излишне оптимистичном предсказании взяла на себя частично транзакционная память.
Некоторые полезные применения: нейронные сети, метод частиц в ячейках
Такую схему распараллеливания тела цикла можно применить в ряде математических алгоритмов, например, при моделировании электростатической линзы методом частиц в ячейках, а также при обучении нейронной сети прямого распространения методом обратного распространения ошибки. Первая задача очень специальная, поэтому обсуждать ее здесь я не буду, скажу только, что изложенный подход к распараллеливанию дал ускорение на 10-15%. А вот вторая задача уже более популярная, поэтому о ней несколько слов сказать просто необходимо.
Цикл обучения нейронной сети включает последовательный проход по обучающим парам, причем для каждой пары выполняется прямой ход (расчет выхода сети) и обратный ход (коррекция весов и смещений). Это и есть две части тела цикла по обучающим парам и для их распараллеливания можно применить вышеизложенный подход (кстати, его можно применить и при параллельном проходе по обучающим парам, с незначительными изменениями). В результате на типичной задаче обучения нейронной сети я получил 50% выигрыша по скорости работы.
Автоматизация распараллеливания C-программ
Идея сверхоптимистичных вычислений не очень сложна, поэтому была написана специальная программа-транслятор, которая занимается автоматическим распараллеливанием – находит в исходной C-программе циклы, для которых такое распараллеливание может дать положительный результат и расщепляет их тела на две части, вставляя необходимые директивы OpenMP, находя потенциальные переменные для каналов, подключая библиотеку работы с частично транзакционной памятью и предицирующими каналами и, в конечном итоге, порождая выходную распараллеленную программу.
В частности, такой транслятор был применен к программе моделирования электростатической линзы. Приведу обе программы – исходную (в которую включена директива-указание на распараллеливание циклов) и полученную после трансляции.
Исходная программа:
#include #include #include #pragma auto parallelize #pragma auto pure(malloc,fabs,free,sizeof,omp_get_wtime) #define theta 1.83 #define NX 40 #define NY 40 #define h 0.1 #define NP 15000 // Собирающая электростатическая линза #define U1 200 #define U2 5000 #define e -1.5E-13 #define m 1E-11 #define e0 8.85E-12 #define V (h*h) #define tau 0.000015 #define T 0.09 #define POISSON_EPS 0.01 #define TOL_EPS 0.25 int main() < double * U = (double *)malloc(NY*NX*sizeof(double)); double * UU = (double *)malloc(NY*NX*sizeof(double)); double * EX = (double *)malloc(NY*NX*sizeof(double)); double * EY = (double *)malloc(NY*NX*sizeof(double)); double * PX = (double *)malloc(NP*sizeof(double)); double * PY = (double *)malloc(NP*sizeof(double)); int * X = (int *)malloc(NP*sizeof(int)); int * Y = (int *)malloc(NP*sizeof(int)); double ro[NY][NX]; split_private double t; split_private double tm; split_private int i, j; for (i = 0; i < NY; i++) for (j = 0; j < NX; j++) < UU[i*NX+j] = j == NX-1 ? U2 : j == NX/2 && (i < NY/4 || i >3*NY/4) ? U1 : 0.0; EX[i*NX+j] = 0.0; EY[i*NX+j] = 0.0; > for (i = 0; i < NP; i++) < int x, y; PX[i] = 0.5*NX*h*rand()/RAND_MAX; PY[i] = NY*h*rand()/RAND_MAX; x = PX[i]/h; y = PY[i]/h; if (x < 0) x = 0; else if (x >NX-1) x = NX-1; if (y < 0) y = 0; else if (y >NY-1) y = NY-1; X[i] = x; Y[i] = y; > tm = omp_get_wtime(); for (t = 0.0; t < T; t += tau) < unsigned int n[NY][NX] = < 0 >; double err; int ptr = 0; for (i = 0; i < NY; i++) for (j = 0; j < NX; j++, ptr++) U[ptr] = UU[ptr]; for (i = 1; i < NY - 1; i++) for (j = 1; j < NX - 1; j++) < EX[i*NX+j] = -(U[i*NX+j+1]-U[i*NX+j-1])/2.0/h; EY[i*NX+j] = -(U[(i+1)*NX+j]-U[(i-1)*NX+j])/2.0/h; >for (i = 0; i < NP; i++) < PX[i] += tau*e*EX[Y[i]*NX+X[i]]/m; PY[i] += tau*e*EY[Y[i]*NX+X[i]]/m; >for (i = 0; i < NP; i++) < int x = PX[i]/h; int y = PY[i]/h; if (x < 0) x = 0; else if (x >NX-1) x = NX-1; if (y < 0) y = 0; else if (y >NY-1) y = NY-1; Y[i] = y; X[i] = x; n[y][x]++; > for (i = 0; i < NY; i++) for (j = 0; j < NX; j++) ro[i][j] = n[i][j]*e/V; do < err = 0.0; for (i = 1; i < NY - 1; i++) for (j = 1+(i-1)%2; j < NX - 1; j+=2) < int ptr = i*NX + j; if (!(j == NX/2 && (i < NY/4 || i >3*NY/4))) < double _new = (1-theta)*UU[ptr] + theta/4.0*(UU[ptr-1]+UU[ptr+1]+UU[ptr+NX]+UU[ptr-NX]-h*h*ro[i][j]/e0); double loc_err = fabs(UU[ptr] - _new); if (loc_err >err) err = loc_err; UU[ptr] = _new; > > for (i = 1; i < NY - 1; i++) for (j = 1+i%2; j < NX - 1; j+=2) < int ptr = i*NX + j; if (!(j == NX/2 && (i < NY/4 || i >3*NY/4))) < double _new = (1-theta)*UU[ptr] + theta/4.0*(UU[ptr-1]+UU[ptr+1]+UU[ptr+NX]+UU[ptr-NX]-h*h*ro[i][j]/e0); double loc_err = fabs(UU[ptr] - _new); if (loc_err >err) err = loc_err; UU[ptr] = _new; > > for (j = 0; j < NX; j++) < UU[j] = UU[NX + j]; UU[(NY-1)*NX + j] = UU[(NY-2)*NX + j]; >> while (err > POISSON_EPS); > for (i = 0; i < NY; i++) < for (j = 0; j < NX; j++) printf("%lf\t", UU[i*NX+j]); printf("\n"); >return 0; >
Автоматически распараллеленная программа
#include "transact.h" #define split_private /* split-private */ #include #include #include #define theta 1.83 #define NX 40 #define NY 40 #define h 0.1 #define NP 15000 #define U1 200 #define U2 5000 #define e -1.5E-13 #define m 1E-11 #define e0 8.85E-12 #define V (h*h) #define tau 0.000015 #define T 0.09 #define POISSON_EPS 0.01 #define TOL_EPS 0.25 int main( ) < double * U = (double *)malloc(NY*NX*sizeof(double)); double * UU = (double *)malloc(NY*NX*sizeof(double)); double * EX = (double *)malloc(NY*NX*sizeof(double)); double * EY = (double *)malloc(NY*NX*sizeof(double)); double * PX = (double *)malloc(NP*sizeof(double)); double * PY = (double *)malloc(NP*sizeof(double)); int * X = (int *)malloc(NP*sizeof(int)); int * Y = (int *)malloc(NP*sizeof(int)); double ro[NY][NX]; split_private double t; split_private double tm; split_private int i, j; for ( i = 0; i < NY; i++ ) for ( j = 0; j < NX; j++ ) < UU[i*NX+j] = j == NX-1 ? U2 : j == NX/2 && (i < NY/4 || i >3*NY/4) ? U1 : 0.0; EX[i*NX+j] = 0.0; EY[i*NX+j] = 0.0; > for ( i = 0; i < NP; i++ ) < int x, y; PX[i] = 0.5*NX*h*rand()/RAND_MAX; PY[i] = NY*h*rand()/RAND_MAX; x = PX[i]/h; y = PY[i]/h; if ( x < 0 ) x = 0; else if ( x >NX-1 ) x = NX-1; if ( y < 0 ) y = 0; else if ( y >NY-1 ) y = NY-1; X[i] = x; Y[i] = y; > tm = omp_get_wtime(); #pragma omp parallel num_threads(2) private(t,tm,i,j) < int __id__ = omp_get_thread_num(); TOut* out_ro = __id__ == 0 ? new TOut("ro63", (NY)*(NX), 2, 0.01, -1, "63") : NULL; TIn * in_ro = __id__ == 1 ? new TIn("ro63", (NY)*(NX), 2, 0.01, -1, "63") : NULL; for ( t = 0.0; t < T; t += tau ) < unsigned int n[NY][NX] = < 0 >; double err; int ptr = 0; if ( __id__ == 0 ) < for ( i = 0; i < NY; i++ ) for ( j = 0; j < NX; j++, ptr++ ) U[ptr] = UU[ptr]; >transaction_atomic("63") < if ( __id__ == 0 ) < for ( i = 1; i < NY - 1; i++ ) for ( j = 1; j < NX - 1; j++ ) < EX[i*NX+j] = -(U[i*NX+j+1]-U[i*NX+j-1])/2.0/h; EY[i*NX+j] = -(U[(i+1)*NX+j]-U[(i-1)*NX+j])/2.0/h; >for ( i = 0; i < NP; i++ ) < PX[i] += tau*e*EX[Y[i]*NX+X[i]]/m; PY[i] += tau*e*EY[Y[i]*NX+X[i]]/m; >for ( i = 0; i < NP; i++ ) < int x = PX[i]/h; int y = PY[i]/h; if ( x < 0 ) x = 0; else if ( x >NX-1 ) x = NX-1; if ( y < 0 ) y = 0; else if ( y >NY-1 ) y = NY-1; Y[i] = y; X[i] = x; n[y][x]++; > for ( i = 0; i < NY; i++ ) for ( j = 0; j < NX; j++ ) ro[i][j] = n[i][j]*e/V; out_ro->put((double *)ro); > else < double ro[NY][NX]; in_ro->get((double *)ro, 0); do < err = 0.0; for ( i = 1; i < NY - 1; i++ ) for ( j = 1+(i-1)%2; j < NX - 1; j+=2 ) < int ptr = i*NX + j; if ( !(j == NX/2 && (i < NY/4 || i >3*NY/4)) ) < double _new = (1-theta)*UU[ptr] + theta/4.0*(UU[ptr-1]+UU[ptr+1]+UU[ptr+NX]+UU[ptr-NX]-h*h*ro[i][j]/e0); double loc_err = fabs(UU[ptr] - _new); if ( loc_err >err ) err = loc_err; UU[ptr] = _new; > > for ( i = 1; i < NY - 1; i++ ) for ( j = 1+i%2; j < NX - 1; j+=2 ) < int ptr = i*NX + j; if ( !(j == NX/2 && (i < NY/4 || i >3*NY/4)) ) < double _new = (1-theta)*UU[ptr] + theta/4.0*(UU[ptr-1]+UU[ptr+1]+UU[ptr+NX]+UU[ptr-NX]-h*h*ro[i][j]/e0); double loc_err = fabs(UU[ptr] - _new); if ( loc_err >err ) err = loc_err; UU[ptr] = _new; > > for ( j = 0; j < NX; j++ ) < UU[j] = UU[NX + j]; UU[(NY-1)*NX + j] = UU[(NY-2)*NX + j]; >> while ( err > POISSON_EPS ) ; > > > delete in_ro; delete out_ro; > for ( i = 0; i < NY; i++ ) < for ( j = 0; j < NX; j++ ) printf("%lf\t", UU[i*NX+j]); printf("\n"); >return 0; >
Итоги
Итак, иногда можно пытаться распараллелить программу даже в случаях, когда она состоит из строго последовательных фрагментов, и даже получать положительные результаты по ускорению (в моих экспериментах – прирост ускорения от 15 до 50%). Надеюсь, что эта небольшая статья окажется кому-нибудь полезной.
- распараллеливание
- транзакционная память
- предсказание
- язык программирования
- сверхоптимистичные вычисления
- Программирование
- C++
- Параллельное программирование
Распараллеливание программ
Потоки и библиотеки для распараллеливания
Процесс — это экземпляр программы (.exe) во время выполнения, которому выделены системные ресурсы (процессорное время и память). Каждый процесс выполняется в отдельном адресном пространстве: один процесс не может получить доступ к данных другого. Для обмена информацией процессы могут использовать конвейеры (pipe), файлы, [сетевые каналы](47027.html) и другие способы межпроцессорного взаимодействия.
Потоки существуют внутри процесса и используют его ресурсы.
Поток (thread) — независимый поток исполнения команд в рамках процесса.
Все потоки имеют доступ к общей глобальной памяти, а также могут иметь собственную память (thread_local).
*Для доступа к общим данным требуется синхронизация*.
Современные процессоры содержат от 4 (в ПК и мобильных телефонах) до 128 ядер (в серверах для облачных вычислений).
Кроме того, каждое ядро содержит несколько универсальных и специализированных АЛУ, что позволяет ядру выполнять несколько машинных команд за 1 такт или векторные команды с несколькими значениями одновременно (SSE/AVX), эмулировать дополнительное виртуальное ядро (hyper threading, увеличение производительности до 30%). В языках программирования нет способа явно использовать первые две возможности, но компиляторы могут сгенерировать команды при включении высокого уровня оптимизации.
Явным образом можно задать только распределение вычислений по нескольким ядрам процессора. Для этого можно использовать библиотеки POSIX для управления потоками (1995), OpenMP (1998) или библиотеки, входящие в стандарт языка программирования (2011).
При одновременной работе нескольких потоков необходимо учитывать следующие проблемы:
* изменение одной переменной несколькими потоками — для каждого ядра есть 2 уровня кэш-памяти и новое значение переменной должно стать доступным для других ядер;
* обеспечение соответствия между значением переменной и другими переменными и/или предыдущим значением этой переменной — между чтением данных для вычислений (в регистры) и записью результата присваивания может произойти изменение этих данных в другом потоке;
* доступ к ресурсам (файлам, сети, консоли и другим устройствам) — чтение и запись информации производится связанными группами, пока группа действий не будет выполнена, другой поток не должен прерывать это взаимодействие;
* повторное вхождение в функцию в нескольких потоках (потокобезопасность) — для функций, использующих глобальные переменные и ресурсы для вычислений.
Первая проблема решается самим процессором, когда из какого-либо кэша данные переписываются в оперативную память, контроллеры остальных ядер получают сигнал об этом изменении и, если необходимо, изменяют соответствующие данные в своих кэшах. Но компилятор должен знать о возможности обращения к переменной в других потоках и не использовать регистры для ускорения вычислений с этой переменной (спецификатор volatile).
Для решения трех других проблем в коде выделяются критические участки, а с важными переменными и ресурсами связывается семафор или мьютекс (вариант семафора, который может разблокировать только поток, выполнивший его блокировку). Перед выполнением критического участка мьютекс блокирует использование данных или ресурсов, а по окончании — разблокирует.
При попытке блокировки уже заблокированного мьютекса другим потоком, поток приостанавливается и ставится в очередь, пока мьютекс не будет разблокирован первым потоком.
В простых случаях используются атомарные переменные, при изменении которых применяется специальная машинная команда, гарантирующая, что выполнению действия не помешают другие ядра процессора.
В С++ используется специальный класс
> «#include «
> «std::atomic v2;«
Для упрощения переноса кода из C в С++ и обратно в заголовочных файлах «« языка С и «« языка C++ для всех базовых типов определены названия соответствующих атомарных типов: «atomic_int«, «atomic_llong«, «atomic_char« и т.д. Также в «« определены вспомогательные функции для работы с атомарными переменными, аналоги атомарных машинных команд.
Семафоры реализуются с помощью атомарных переменных.
Методы распараллеливания программ в оптимизирующем компиляторе

Рассматриваются методы распараллеливания программ в оптимизирующем компиляторе, использующие параллелизм операций, коротких векторов и параллельных потоков управления. Предложенные методы являются достаточно универсальными, т.к. они практически применяются для двух архитектурных платформ: «Эльбрус» с явным параллелизмом операций и «МЦСТ-R» с суперскалярным (в исходном порядке) выполнением операций, при этом обе платформы содержат короткие (несовпадающие) векторные операции и поддерживают многопроцессорность на общей памяти. Приводятся результаты практического использования данных методов распараллеливания.
Большинство современных микропроцессорных архитектур использует различные методы повышения производительности исполняемых программ за счет их распараллеливания, а именно:
- конвейеризация исполнения операций – разбиение процесса исполнения на стадии (такты) и одновременное исполнение операций, находящихся на разных стадиях конвейера;
- параллельное (одновременное) исполнение нескольких операций, находящихся на одной стадии конвейерного исполнения;
- применение одной операции к нескольким данным одновременно;
- поддержка многопоточного исполнения внутри одного процессорного ядра, на нескольких процессорных ядрах или в многопроцессорной системе, работающей на общей памяти;
- поддержка неоднородной многоядерной или многопроцессорной системы, в которой ядра могут работать одновременно, используя при этом локальную или общую память.
Эти методы распараллеливания требуют поддержки в оптимизирующих компиляторах, т.к. обычные языки программирования, такие как C, C++, Fortran (начиная с Fortran-90, есть операции над векторами), являются языками последовательного программирования. В работе рассматриваются методы распараллеливания программ в компиляторах, которые ориентируются на аппаратную поддержку указанных видов параллелизма. Для исследования используется оптимизирующий компилятор с языков C, C++, Fortran, реализованный для микропроцессорных архитектур «Эльбрус» и «МЦСТ-R» (совместима с архитектурой SPARC) [1]. Обе архитектуры поддерживают конвейерное параллельное исполнение операций, набор целочисленных и вещественных операций над короткими векторами, а также многоядерность и многопроцессорность на общей памяти с когерентным доступом.
Результаты представлены для архитектуры «Эльбрус» (т.к. она обеспечивает наиболее полную аппаратную поддержку данных видов параллелизма и поддержку со стороны оптимизирующего компилятора) и вычислительного комплекса (ВК) «Эльбрус-3М1» [2]. Наиболее универсальный вид распараллеливания (на уровне операций) представлен на задачах пакета SPECcpu2000 [3], а результаты векторизации и распараллеливания на потоки управления – на отдельных задачах пакетов SPEC и на высокопроизводительных библиотеках, в которых доминируют вычисления в циклах.

Подробнее. Загрузить файл
Содержание:
1. Распараллеливание на уровне операций
1.1. Программы со сложным управлением
1.2. Программы с обработкой в циклах
1.3. Результаты распараллеливания на уровне операций
1.3.1. Целочисленные задачи
1.3.2. Вещественные задачи
2. Автоматическая векторизация
2.1. Базовые средства векторизации
2.2. Вспомогательные преобразования
2.3. Векторизация рекуррентных выражений
2.4. Векторизация циклов с разветвлениями управления
2.5. Экспериментальные результаты
3. Автоматическое распараллеливание на потоки управления
3.1. Общее описание функциональности
3.2. Техника автоматического распараллеливания
3.3. Библиотека поддержки для автоматического распараллеливания
3.4. Результаты применения автоматического распараллеливания