Частично параллельные циклы с использованием задач openmp
требования:
- параллельный двигатель: OpenMP 3.1+ (может быть OpenMP 4.0 при необходимости)
- параллельные конструкции: задачи OpenMP
- компилятор: gcc 4.9.x (поддерживает OpenMP 4.0)
вход:
- C код с петлями
- цикл имеет зависимость данных перекрестных итераций (ies): итерация" i+1 "требует данных от итерации" i " (только такая зависимость, ничего else)
- тело петли может быть частично зависимым
- петля не может быть разделена на две петли; тело петли должно оставаться твердым
- все разумное можно добавить к петле или определению функции тела петли
код:
(здесь переменные conf/config/configData используются только для иллюстрации, основной интерес находится в переменных value/valueData.)
void loopFunc(const char* config, int* value)
{
int conf;
conf = prepare(config); // independent, does not change “config”
*value = process(conf, *value); // dependent, takes prev., produce next
return;
}
int main()
{
int N = 100;
char* configData; // never changes
int valueData = 0; // initial value
…
for (int i = 0; i < N; i++)
{
loopFunc(configData, &valueData);
}
…
}
нужно кому:
- параллелизируйте цикл с помощью задач omp (разделы omp для / omp нельзя использовать)
- функции"подготовка "должны выполняться параллельно с другими функциями" подготовка "или" процесс"
- функции"процесс" должны быть упорядочены в соответствии с зависимостью данных
что было предложено и реализовано:
- определить целочисленный флаг
- назначьте ему номер первого итерация
- каждая итерация, когда ей нужны данные, ждет, когда флаг будет равен итерации
- обновить значение флага, когда данные для следующей итерации готовы
такой:
(я напоминаю, что переменные conf/config/configData используются только для иллюстрации, основной интерес заключается в переменных value/valueData.)
void loopFunc(const char* config, int* value, volatile int *parSync, int iteration)
{
int conf;
conf = prepare(config); // independent, do not change “config”
while (*parSync != iteration) // wait for previous to be ready
{
#pragma omp taskyield
}
*value = process(conf, *value); // dependent, takes prev., produce next
*parSync = iteration + 1; // inform next about readiness
return;
}
int main()
{
int N = 100;
char* configData; // never changes
int valueData = 0; // initial value
volatile int parallelSync = 0;
…
omp_set_num_threads(5);
#pragma omp parallel
#pragma omp single
for (int i = 0; i < N; i++)
{
#pragma omp task shared(configData, valueData, parallelSync) firstprivate(i)
loopFunc(configData, &valueData, ¶llelSync, i);
}
#pragma omp taskwait
…
}
что получилось:
Это не удается. :)
причина была ли задача openmp занята потоком openmp. Например, если мы определяем 5 потоков openmp (как в коде выше).
- цикл"For" генерирует 100 задач.
- среда выполнения OpenMP назначает 5 произвольных задач 5 потокам и запускает эти задачи.
Если среди запущенных задач не будет задачи с i=0 (это случается время от времени), выполнение задач ждет вечно, занимает потоки навсегда и задача с i=0 никогда начинается.
что дальше?
у меня нет других идей, как реализовать нужный режим вычислений.
текущее решение
Спасибо за идею @parallelgeek ниже
int main()
{
int N = 10;
char* configData; // never changes
int valueData = 0; // initial value
volatile int parallelSync = 0;
int workers;
volatile int workingTasks = 0;
...
omp_set_num_threads(5);
#pragma omp parallel
#pragma omp single
{
workers = omp_get_num_threads()-1; // reserve 1 thread for task generation
for (int i = 0; i < N; i++)
{
while (workingTasks >= workers)
{
#pragma omp taskyield
}
#pragma omp atomic update
workingTasks++;
#pragma omp task shared(configData, valueData, parallelSync, workingTasks) firstprivate(i)
{
loopFunc(configData, &valueData, ¶llelSync, i);
#pragma omp atomic update
workingTasks--;
}
}
#pragma omp taskwait
}
}
1 ответов
- летучие вещества AFAIK не предотвращают переупорядочивание оборудования, поэтому вы
может закончиться беспорядком в памяти, потому что данные еще не записаны,
хотя флаг уже виден потребляющим потоком как
true
. - вот почему небольшая часть советует: вместо этого используйте C11 atomics, чтобы обеспечить видимость данных. Как я вижу, gcc 4.9 поддерживает c11 C11Status в GCC
- вы можете попытаться разделить сгенерированные задачи на группы по k задачам, где
K == ThreadNum
и начните генерировать последующую задачу (после создания задач в первой группе) только после завершения любой из запущенных задач. Таким образом, у вас есть инвариант это каждый раз, когда у вас есть только K задач, запущенных и запланированных на K потоков. - Межзадачных зависимостей также могут быть выполнены с помощью атомно флаги из С11.