Частично параллельные циклы с использованием задач 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, &parallelSync, 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, &parallelSync, i);

                #pragma omp atomic update
                    workingTasks--;
            }
        }
        #pragma omp taskwait
    }
}

1 ответов


  1. летучие вещества AFAIK не предотвращают переупорядочивание оборудования, поэтому вы может закончиться беспорядком в памяти, потому что данные еще не записаны, хотя флаг уже виден потребляющим потоком как true.
  2. вот почему небольшая часть советует: вместо этого используйте C11 atomics, чтобы обеспечить видимость данных. Как я вижу, gcc 4.9 поддерживает c11 C11Status в GCC
  3. вы можете попытаться разделить сгенерированные задачи на группы по k задачам, где K == ThreadNum и начните генерировать последующую задачу (после создания задач в первой группе) только после завершения любой из запущенных задач. Таким образом, у вас есть инвариант это каждый раз, когда у вас есть только K задач, запущенных и запланированных на K потоков.
  4. Межзадачных зависимостей также могут быть выполнены с помощью атомно флаги из С11.