Опубликован: 10.10.2011 | Уровень: для всех | Доступ: платный
Лекция 5:

Оптимизации для параллельных вычислений

Многоядерные/многопроцессорные Intel архитектуры

Intel® Pentium® Processor Extreme Edition (2005-2007)

  • Представил технологию двойного ядра (dual core)

Intel® Xeon® Processor 5100, 5300 Series

Intel® Core™2 Processor Family (2006-)

  • Появились двухпроцессорные архитектуры. Процессоры поддерживают четыре ядра(quad-core)

Intel® Xeon® Processor 5200, 5400, 7400 Series

Intel® Core™2 Processor Family (2007-)

  • Есть семейства в которых количество ядер на процессоре доведено до 6. 2-4 процессора.

Intel® Atom™ Processor Family (2008-)

  • Процессор с высокой энергоэффективностью.

Intel® Core™i7 Processor Family (2008-)

  • Hyperthreading технология. Системы с неоднородным доступом к памяти.

На рынок персональных компьютеров многопроцессорные/многоядерные архитектуры пришли сравнительно недавно. Ядром процессора называется та его часть, которая извлекает из памяти и исполняет инструкции. Изначально процессор содержал одно ядро. Сейчас широко распространены dual-core, quad-core и даже hexa-core процессоры. Большинство продаваемых сейчас вычислительных систем многопроцессорные.

Одной из главных особенностей многоядерной архитектуры является то, что ядра совместно используют часть подсистемы памяти и шину данных


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

Приведенная схема описывает примерную организацию памяти в двухядерных процессорах.

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

Начиная с процессора Nehalem ядра получили свой кэш второго уровня, но совместно пользуются кэшем третьего уровня.

Классификация многопроцессорных систем по использованию памяти

  1. Массивно-параллельные компьютеры или системы с распределенной памятью. (MPP системы).

    Каждый процессор полностью автономен.

    Существует некоторая коммуникационная среда.

    Достоинства: хорошая масштабируемость
    Недостатки: медленное межпроцедурное взаимодействие
  2. Системы с общей памятью (SMP системы)

    Все процессоры равноудалены от памяти. Связь с памятью осуществляется через общую шину данных.

    Достоинства: хорошее межпроцессорное взаимодействие
    Недостатки: плохая масштабируемость
    большие затраты на синхронизацию подсистем кэшей
  3. Системы с неоднородным доступом к памяти (NUMA)

    Память физически распределена между процессорами. Единое адресное пространство поддерживается на аппаратном уровне.

    Достоинства: хорошее межпроцессорное взаимодействие и масштабируемость
    Недостатки: разное время доступа к разным сегментам памяти.

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

Если различные процессоры не имеют разделяемой памяти, то это существенно усложняет организацию параллельных вычислений.

Если процессоры могут работать с общей памятью, то возникает вопрос синхронизации подсистем памяти этих процессоров. Как уже упоминалось для ускорения работы с памятью процессоры имеют систему кэшей. Что будет происходить если некоторый адрес памяти будет одновременно присутствовать в нескольких подсистемах памяти и будет модифицирован одним в одном из них? В этом случае необходимо привести в соответствие значения во всех остальных подсистемах. В случае с SMP системами процессор передает информацию об измененном адресе на шину данных. Шина данных в свою очередь передает эту информацию другим процессорам.

NUMA архитектура представляет некий гибрид между SMP и MPP. Память физически распределена, но общедоступна. Единое адресное пространство поддерживается на аппаратном уровне, но разная скорость доступа к различным сегментам памяти.

Intel QuickPath Architecture

Приведенные соображения о работе памяти верны и в случае многопроцессорной архитектуры. Типичным представителем многопроцесорных архитектур является архитектура i7. Эта архитектураархитектура с неоднородной памятью. Каждый процессор имеет свою память, доступ к которой наиболее быстрый. Доступ к памяти другого процессора медленнее. Используется QPI соединение между процессорами. Логическая память программы привязывается к физической памяти в момент первого обращения к памяти. Память выделяется из памяти того процессора на котором выполняется поток, запрашивающий память.

Нестабильность работы приложений на многопроцессорных машинах с неоднородным доступом к памяти

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

Эту проблему можно решить с помощью привязки приложения к определенным ядрам. Установить affinity для приложения.

Плюсы и минусы использования многопоточных приложений

++: Вычислительные ресурсы увеличиваются пропорционально кол-ву используемых реальных ядер.
--:
Усложнение разработки
Необходимость синхронизировать потоки
Потоки конкурируют за ресурсы
Создание потоков имеет свою цену

Вывод: В случае разработки бизнес-приложений четко осознавайте цели и цену распараллеливания вашей программы.

В связи с появлением на рынке различных многопроцессорных архитектур существует мода на "параллелизацию" приложений. Однако параллелизация имеет свои плюсы и минусы. Понятно, что существуют приложения, которые обзательно должны быть многопоточными (Вэб сервисы и т.д.). В случае с вычислительными приложениями необходимо решать вопрос о выгодности многопоточности, учитывая следующие плюсы и минусы многопоточных приложений. Плюсы: Вычислительные ресурсы увеличиваются пропорционально количеству используемых реальных ядер (на Nehaleme из-за гиперсрединга на одном реальном ядре находятся два логических). Особенно это важно из-за увеличивающегося размера кэшей разных уровней. Минусы: Усложнение разработки; Проблемы синхронизации; Потоки конкурируют за ресурсы; Создание потоков имеет свою цену, которая может свести на нет весь выигрыш от использования дополнительных ресурсов.

Автоматическое распараллеливание

Процесс автоматического преобразования последовательного программного кода в многопоточный (multi-threaded), использующий несколько ядер одновременно. Цель автоматического распараллеливания – освободить программистов от тяжелой и нудной ручной работы по разделению вычислений на различные потоки.

/Qparallel
          enable the auto-parallelizer to generate multi-threaded code for
          loops that can be safely executed in parallel
    

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

Выгодность автоматического распараллеливания на простом примере

REAL :: a(1000,1000),b(1000,1000),c(1000,1000)
integer i,j,rep_factor
                                                                                
DO I=1,1000
 DO J=1,1000
  A(J,I) = I
  B(J,I) = I+J
  C(J,I) = 0
 END DO
END DO
                                                                                
DO rep_factor=1,1000
 C=B/A+rep_factor
END DO
                                                                                
END
    

Хорошо и плохо масштабируемые алгоритмы

void matrix_mul_matrix(int n,
float C[n][n], float A[n][n],
float B[n][n]) {
	int i,j,k;
	for (i=0; i<n; i++) 
    	  for (j=0; j<n; j++) {
	    C[i][j]=0;
	    for(k=0;k<n;k++)
	      C[i][j]+=A[i][k]*B[k][j];
	  }
}
        

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

Плохо масштабируемые алгоритмы

void matrix_add(int n, float Res[n][n],float A1[n][n], float A2[n][n],
float A3[n][n],float A4[n][n], float A5[n][n], float A6[n][n],
float A7[n][n], float A8[n][n]) {
	int i,j;
	for (i=0; i<n; i++) 
  	  for (j=1; j<n-1; j++) 
	    Res[i][j]=A1[i][j]+A2[i][j]+A3[i][j]+A4[i][j]+
                  A5[i][j]+A6[i][j]+A7[i][j]+A8[i][j]+
	              A1[i][j+1]+A2[i][j+1]+A3[i][j+1]+A4[i][j+1]+
                  A5[i][j+1]+A6[i][j+1]+A7[i][j+1]+A8[i][j+1];
}
        

Плохо масштабируемый алгоритм. В данном случае показан пример теста в котором одновременно идет обращение к большому количеству различных объектов в памяти, а сами вычисления достаточно просты. Т.е. "узким" местом данного метода является работа с подсистемой памяти. При увеличении кол-ва ядер увеличивается количество пересылок между процессорами.

Допустимость автоматического распараллеливания

Это преобразование – перестановочная оптимизация циклической конструкции.

Упорядоченное выполнение итераций => неопределенный порядок выполнения итераций.

Необходимое условие – отсутствие любых зависимостей внутри цикла.

/Qpar-report{0|1|2|3}
          control the auto-parallelizer diagnostic level
    

/Qpar-report3 сообщает причины, по которым компилятор не распараллеливает тот или иной цикл, в том числе сообщает какие зависимости препятствуют этому.

Поскольку автопараллелизация работает с циклами, то это цикловая перестановочная оптимизация. Вместо упорядоченного выполнения итераций цикла мы преобразуем цикл так, что порядок выполнения итераций становится неопределенным. Такую оптимизацию можно сделать только при отсутствии зависимостей внутри цикла. Используйте –Qpar-report, чтобы установить причины по которым ваши циклы не параллелизуются. Иногда легко переписать код и удалить зависимость, или если зависимость возникает из-за того, что компилятор не может разрешить проблему разделения памяти, то иногда можно "помочь" компилятору.

Выгодность распараллеливания

/Qpar_report3 информирует, если распараллеливание невыгодно

C:\test_par.c(27) (col. 1): remark: loop was not parallelized: insufficient computational work.
    

Точное определение выгодности таких преобразований во время компиляции достаточно тяжелая задача.

Существуют эффекты производительности, которые сложно оценить, например "эффект первого прикосновения".

В большинстве случаев компилятор может не иметь представления о количестве итераций в цикле.

Используйте директивы распараллеливания при экспериментах с производительностью.

Запуск потоков имеет свою цену, поэтому невыгодно параллелизовать циклы, если вычислительная работа внутри цикла мала.

С другой стороны есть некоторые эффекты, которые очень сложно просчитать во время компиляции. Например для архитектур с неоднородным доступом к памяти имеет место так называемый First Touch Effect. Т.е. память выделяется на определенном процессоре при первом обращении к памяти, поэтому если инициализирующий цикл не параллелится, то вся память выделяется на одном процессоре. Если затем следует цикл с интенсивными вычислениями, который будет параллелиться, то его быстродействие будет страдать из-за того что часть потоков будет работать с "медленной" памятью.

#pragma concurrent – игнорировать предполагаемые зависимости в следующем цикле
#pragma concurrent call – вызов функции в следующем цикле безопасен для параллельного выполнения
#pragma concurrentize – параллелизовать следующий цикл
#pragma no concurrentize - не параллелизовать следующий цикл
#pragma prefer concurrent - параллелизовать следующий цикл, если это безопасно
#pragma prefer serial – предложить компилятору не параллелизовать следующий цикл
#pragma serial – заставить компилятор параллелизовать следующий цикл

Автоматическое распараллеливание осуществляется использованием интерфейса OpenMP.

OpenMP (Open Multi-Processing) – это программный интерфейс, который поддерживает многоплатформенное многопроцессорное программирование с общей памятью на C/C++ и Фортране на многих архитектурах.

Количество потоков, используемых вашим приложением, может изменяться с помощью установки переменной окружения

OMP_NUM_THREADS
    

(по умолчанию будут использоваться все доступные ядра)


8 Threads

16 Threads

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

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

/Qpar-runtime-control[n]
          Control parallelizer to generate runtime check code for effective 
          automatic parallelization.
            n=0    no runtime check based auto-parallelization 
            n=1    generate runtime check code under conservative mode 
                   (DEFAULT when enabled)
            n=2    generate runtime check code under heuristic mode
            n=3    generate runtime check code under aggressive mode
    

Поскольку определение выгодности автопараллелизации достаточно трудная задача, которая зависит от многих факторов, часть которых не может быть известна во время компиляции, то существует возможность создавать многоверсионные приложения, которые содержат проверки времени выполнения и запускают несколько потоков для обработки циклов только при выполнении определенных условий. Опция /Qpar-runtime-control используется для определения уровня использования таких проверок.

Взаимодействие с другими оптимизациями циклических конструкций

  • Объединение циклов и создание больших циклов
  • Автоматическое распараллеливание
  • Оптимизация цикла в поточной функции в соответствии с обычными соображениями. (развертка, векторизация, разбиение на несколько циклов и т.п.)

Эти соображения можно использовать при написании программы. Стремитесь создавать большие циклы без зависимостей, т.е. такие чтобы итерации могли выполняться в произвольном порядке.

Дмитрий Маликов
Дмитрий Маликов
Россия, г. Казань
Татьяна Троеглазова
Татьяна Троеглазова
Россия, Новосибирск, Новосибирский Государственный Универсистет