Автоматическая параллелизация с помощью компиляторов Intel®

Automatic Parallelization with Intel® Compilers [Eng., PDF 242KB]

Введение

Для параллелизации приложения с целью повышения производительности порой может требоваться длительное время. Для приложений, в которых большинство вычислений выполняется в простых циклах, компиляторы Intel® могут автоматически генерировать многопоточную версию.

В дополнение к оптимизации кода высокого уровня, компиляторы Intel® позволяют проводить автоматическую параллелизацию с поддержкой OpenMP. При автоматической параллелизации компилятор находит циклы, которые могут безопасно и эффективно выполняться параллельно, и генерирует многопоточный код. OpenMP позволяет программистам выражать параллелизм с помощью директив компилятора и прагм С/С++.

Вводные данные

Компиляторы Intel® C++ и Fortran могут анализировать данные в циклах и определять, какие циклы имеет смысл выполнять параллельно. Автоматическая параллелизация избавляет программиста от необходимости:

  • Отыскивать циклы, пригодные для параллельного выполнения
  • Выполнять анализ потока данных для проверки корректности параллельного выполнения
  • Вручную добавлять параллельные директивы компилятора.
Добавление опций -Qparallel (Windows*) или -parallel (Linux* или Mac OS* X) в команду компилятора – единственное, что требуется от программиста. Однако успешная параллелизация может зависеть еще от некоторых условий, которые описываются в следующей главе.

Следующая программа на Фортране содержит цикл с большим количеством итераций:
PROGRAM TEST
PARAMETER (N=10000000)
REAL A, C(N)
DO I = 1, N
A = 2 * I - 1
C(I) = SQRT(A)
ENDDO
PRINT*, N, C(1), C(N)
END
Анализ данных подтверждает, что цикл не содержит зависимостей по данным. Компилятор будет генерировать код, распределяющий итерации равномерно по потокам. Количество потоков по умолчанию равно общему количеству процессорных ядер (что может быть больше, чем количество физических ядер, если включена технология Intel® Hyper Threading), но может устанавливаться независимо через переменную окружения OMP_NUM_THREADS. Ускорение при параллельном выполнении данного цикла зависит от количества работы, баланса нагрузки между потоками, величины издержек на создание и синхронизацию потоков и т.д., но в общем плане ускорение будет ниже, чем линейное по отношению к количеству используемых потоков. Для программы в целом ускорение зависит от отношения количества параллельных вычислений к количеству последовательных.

Рекомендации

Чтобы компилятор мог заниматься параллелизацией цикла, должны быть выполнены три условия. Во-первых, количество итераций должно быть известно до начала работы, чтобы её можно было разделить заранее. Цикл while, например, обычно для распараллеливания не подходит. Во-вторых, не должно быть переходов внутрь или наружу цикла. В-третьих, самое важное, итерации цикла должны быть независимыми. Другими словами, корректность результата не должна логически зависеть от порядка выполнения итераций. Здесь, однако, могут быть некоторые вариации при накоплении ошибки округления, как, например, когда некие величины складываются в разном порядке. В ряде случаев, например, при суммировании массива или другом использовании временных скалярных значений, компилятор может убирать очевидную зависимость с помощью простого преобразования.

Множественность указателей (aliasing) или элементов массивов – ещё одно часто встречающееся препятствие для безопасной параллелизации. Два указателя являются алиасными, если оба указывают на один и тот же участок памяти. Компилятор может не иметь возможности определить, указывают ли два указателя или элемента массива на один и тот же адрес памяти, если они зависят от аргументов функции, динамических данных или результатов сложных вычислений. Если компилятор не может доказать, что указатели или ссылки массива являются защищенными и что итерации независимы, то он не станет параллелить цикл, за исключением тех редких случаев, когда он вынужден генерировать альтернативные пути кода для явного тестирования алиасинга во время запуска. Если программист знает, что параллелизация конкретного цикла является безопасной и что потенциальными дублированными ссылками можно пренебречь, то этот факт можно сообщить компилятору с помощью прагмы С (#pragma parallel) или директивы Фортрана (!DIR$ PARALLEL).В С альтернативным способом указать, что указатель нигде не дублируется, является использование оператора restrict при объявлении указателя наряду с использованием опции командной строки -Qrestrict (Windows) или -restrict (Linux или Mac OS* X) . Тем не менее, компилятор никогда не станет параллелить цикл, который доказано является небезопасным.

Компилятор может эффективно анализировать циклы только с относительно простой структурой. Например, он не может определить потокобезопасность цикла, содержащего вызовы внешних функций, поскольку он не знает, имеет ли вызов функции какие-нибудь побочные эффекты, которые могут вносить зависимости. Программисты, использующие Fortran 90, могут использовать атрибут PURE для обозначения того, что подпрограммы и функции не имеют побочных эффектов. В С или Fortran также можно включать межпроцедурную оптимизацию с помощью опции компилятора -Qipo (Windows) или -ipo (Linux или Mac OS X). Это даёт компилятору возможность ставить функции инлайн или анализировать вызываемые функции на предмет наличия побочных эффектов.

Когда компилятор не может автоматически распараллелить сложные циклы, про которые программист знает, что их можно безопасно выполнять параллельно, можно с успехом использовать OpenMP. Обычно программист понимает код лучше, чем компилятор, и может выразить параллелизм с большей гранулярностью. С другой стороны, автоматическая параллелизация может быть эффективной для вложенных циклов, применяемых, например, при умножении матриц. Среднезернистый параллелизм возникает при параллелизации внешнего цикла, что позволяет оптимизировать внутренние циклы при более мелкозернистом параллелизме с использованием векторизации или программных конвейеров.

Тот факт, что цикл может быть распараллелен, не означает, что он должен быть распараллелен. Компилятор использует оценочную модель с пороговым параметром для решения, нужно ли распараллелить цикл. Опции компилятора -Qpar-threshold[n] (Windows) и -par-threshold[n] (Linux) управляют этим параметром. Значение n может меняться от 0 до 100, где 0 означает, что нужно всегда параллелить безопасные циклы, независимо от модели оценки, а 100 говорит компилятору параллелизовать только те циклы, для которых выигрыш в производительности будет достаточной высок. Значение n по умолчанию установлено в консервативные 100. иногда уменьшение порогового значения до 99 может привести к существенному увеличению количества распараллеленных циклов. Прагма #parallel always (!DIR$ PARALLEL ALWAYS в Fortran) может использоваться для отключения оценочной модели для отдельного цикла.

Опции -Qpar-report[n] (Windows) и -par-report[n] (Linux), где n варьируется от 1 до 3, позволяют показывать сообщения о том, какие циклы были распараллелены. Сообщения выглядят примерно так:

test.f90(6) : (col. 0) remark: LOOP WAS AUTO-PARALLELIZED
Компилятор также сообщит, какие циклы не могли быть распараллелены и почему:
serial loop: line 6
flow data dependence from line 7 to line 8, due to "c"
Подобный эффект наблюдается например здесь:
void add (int k, float *a, float *b)
{
for (int i = 1; i < 10000; i++)
a[i] = a[i+k] + b[i];
}
Команда компиляции 'icl -c -Qparallel -Qpar-report3 add.cpp' выводит примерно такие сообщения:
procedure: add
test.c(7): (col. 1) remark: parallel dependence: assumed ANTI dependence between a line 7 and a line 7. flow data dependence assumed
...
test.c(7): (col. 1) remark: parallel dependence: assumed FLOW dependence between a line 7 and b line 7.
Поскольку компилятор не знает значение k, он должен предположить, что итерации зависят друг от друга, как, например, если k равняется -1. Однако программист может знать об обратном, так как хорошо знаком с программой (например, что k всегда больше 10000), и может преодолеть мнение компилятора с помощью прагмы:
void add (int k, float *a, float *b)
{
#pragma parallel
for (int i = 1; i < 10000; i++)
a[i] = a[i+k] + b[i];
}
Теперь сообщение говорит, что цикл был распараллелен:
 procedure: add
test.c(6): (col. 1) remark: LOOP WAS AUTO-PARALLELIZED. 
Однако программист должен следить за тем, чтобы функция не вызывалась со значением, меньшим 10000.

Указания к применению

Попробуйте собрать ядро приложения, содержащее интенсивные вычисления, применяя опции компилятора -parallel (Linux или Mac OS X) или -Qparallel (Windows). Вывод отчета разрешается с помощью опции -par-report3 (Linux) или -Qpar-report3 (Windows). Из него можно узнать, какие циклы были распараллелены, а какие нет. В последних можно попробовать убрать зависимости по данным и/или помочь компилятору устранить потенциальные повторные ссылки на переменные. Компиляция с опцией -O3 разрешает дополнительную высокоуровневую оптимизацию циклов (такую, как слияние циклов), что иногда может помочь автопараллелизации. Такая дополнительная оптимизация выводится в оптимизационном отчете компилятора при наличии опции -opt-report-phase hlo. . Необходимо всегда измерять производительность с и без параллелизации для проверки, что необходимое ускорение достигнуто.

Если в командной строке указаны обе опции -openmp и -parallel, то компилятор будет пытаться распараллелить только те циклы, в которых не содержится директив OpenMP. Для сборки с разделенными стадиями компилирования и компоновки (link), при использовании автоматической параллелизации необходимо подключить динамическую библиотеку OpenMP. Самый простой способ сделать это – использовать для компоновки драйвер компилятора с помощью, например, опций icl -Qparallel (Windows) или ifort -parallel (Linux или Mac OS X). В системе Mac OS X может понадобиться установить переменную окружения DYLD_LIBRARY_PATH в Xcode, чтобы динамическая библиотека OpenMP могла быть найдена при старте.

Для получения подробной информации о возможностях оптимизации компилятора обратитесь к нашему Уведомлению об оптимизации.