OpenMP с "collapse()" для вложенных for-loop выполняет хуже, когда без

Это мой код:

double res1[NNN];  
#pragma omp parallel for collapse(3) schedule(dynamic) 
for (int i=0; i<NNN; i++)
{
    for (int j=0;j<NNN;j++)
    {
        for (int k=0;k<NNN;k++)
        {
            res1[i] = log(fabs(i*j*k));
        }
    }
}
std::cout<< res1[10] << std::endl;

Когда я использую collapse(3), требуется ~ 50 секунд; без collapse(3) всего ~ 6-7 секунд. Я очень озадачен этим поведением, так как я ожидал бы лучшей производительности с "крахом", чем без.

Я что-то пропустил?


Я провел несколько экспериментов и играл с разными конфигурациями:

(NNN = 2500 и 24 ядра)

  • schedule(STATIC) && & collapse(3) → ~ 54 сек
  • schedule(STATIC) && & collapse(2) → ~ 8 сек
  • schedule(STATIC) && & collapse(1) → ~ 8 сек

Я также пробовал с расписанием DYNAMIC, но это занимает огромное время (несколько минут).


В моей исходной проблеме у меня есть 4 DIM-for-loops (4D-массив): 51x23x51x23.

Каков наилучший способ использования OpenMP/MPI для минимизации времени работы? Я имею в общей сложности ~ 300 процессорных ядер. Каков наилучший способ распространения моего массива по этим ядрам? Длина массива является гибкой (я могу каким-то образом сопоставить ее с количеством процессоров).

Любые предложения?

Ответ 1

Вам не хватает понятия о том, каково влияние использования динамического планирования на служебные данные OpenMP.

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

Наиболее важным различием между статическим и динамическим планированием является размер итерационного куска (т.е. количество последовательных итераций цикла, которые каждый поток выполняет, прежде чем искать работу в другой части итерационного пространства). Если этот параметр опущен, размер блока с статическим планированием по умолчанию равен #_of_iterations/#_of_threads, а размер по умолчанию для динамического планирования - 1, то есть каждый поток должен запрашивать время выполнения OpenMP для каждой итерации распределенного цикла.

Что происходит в вашем случае, так это то, что без collapse(3) у вас есть NNN итерационные фрагменты внешнего цикла, и каждый поток выполняет итерации NNN*NNN (внутренних циклов), прежде чем запрашивать время выполнения OpenMP для другой итерации. Когда вы свертываете петли, количество итерационных кусков увеличивается до NNN*NNN*NNN, т.е. Есть много больше кусков, и каждый поток будет запрашивать время выполнения OpenMP для куска после каждой итерации.

Это создает еще одну проблему, когда внутренние контуры рушится с самой внешней: произойдет, что многие потоки получат итерации, которые имеют одинаковое значение i, что нарушит вычисление, так как порядок выполнения не гарантируется, и это может произойти, что последний поток, который записывается в res1[i], не тот, который выполняет последнюю итерацию обоих внутренних циклов.