Почему omp_set_dynamic (1) никогда не регулирует количество потоков (в Visual C ++)? - PullRequest
1 голос
/ 09 мая 2019

Если мы посмотрим на документацию Visual C ++ из omp_set_dynamic, она буквально копируется из стандарта OMP 2.0 (раздел 3.1.7 на стр. 39):

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

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

(обе документации также показывают подпись как void omp_set_dynamic(int dynamic_threads);. Похоже, что «числопотоков, указанных пользователем "не относится к dynamic_threads, а вместо этого означает" все, что пользователь указал с помощью оставшегося интерфейса OpenMP ").

Однако, независимо от того, насколько высоко я увеличиваю нагрузку на системупри omp_set_dynamic(1) возвращаемое значение omp_get_num_threads (запрашиваемое внутри параллельных областей) никогда не меняется от максимума в моей тестовой программе.Тем не менее, я все еще могу наблюдать явные различия в производительности между omp_set_dynamic(1) и omp_set_dynamic(0).

Вот пример программы для воспроизведения проблемы:

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
#include <cstdlib>
#include <cmath>

#include <omp.h>

#define UNDER_LOAD true

const int SET_DYNAMIC_TO = 1;

const int REPEATS = 3000;
const unsigned MAXCOUNT = 1000000;

std::size_t threadNumSum = 0;
std::size_t threadNumCount = 0;

void oneRegion(int i)
{
  // Pesudo-randomize the number of iterations.
  unsigned ui = static_cast<unsigned>(i);
  int count = static_cast<int>(((MAXCOUNT + 37) * (ui + 7) * ui) % MAXCOUNT);

#pragma omp parallel for schedule(guided, 512)
  for (int j = 0; j < count; ++j)
  {
    if (j == 0)
    {
      threadNumSum += omp_get_num_threads();
      threadNumCount++;
    }

    if ((j + i + count) % 16 != 0)
      continue;

    // Do some floating point math.
    double a = j + i;
    for (int k = 0; k < 10; ++k)
      a = std::sin(i * (std::cos(a) * j + std::log(std::abs(a + count) + 1)));

    volatile double out = a;
  }
}


int main()
{
  omp_set_dynamic(SET_DYNAMIC_TO);


#if UNDER_LOAD
  for (int i = 0; i < 10; ++i)
  {
    std::thread([]()
    {
      unsigned x = 0;
      float y = static_cast<float>(std::sqrt(2));
      while (true)
      {
//#pragma omp parallel for
        for (int i = 0; i < 100000; ++i)
        {
          x = x * 7 + 13;
          y = 4 * y * (1 - y);
        }
        volatile unsigned xx = x;
        volatile float yy = y;
      }
    }).detach();
  }
#endif


  std::chrono::high_resolution_clock clk;
  auto start = clk.now();

  for (int i = 0; i < REPEATS; ++i)
    oneRegion(i);

  std::cout << (clk.now() - start).count() / 1000ull / 1000ull << " ms for " << REPEATS << " iterations" << std::endl;

  double averageThreadNum = double(threadNumSum) / threadNumCount;
  std::cout << "Entered " << threadNumCount << " parallel regions with " << averageThreadNum << " threads each on average." << std::endl;

  std::getchar();

  return 0;
}

Версия компилятора: Microsoft (R) CОптимизирующий компилятор / C ++ версии 19.16.27024.1 для x64

Например, на gcc эта программа напечатает averageThreadNum значительно ниже omp_set_dynamic(1), чем для omp_set_dynamic(0).Но на MSVC в обоих случаях отображается одно и то же значение, несмотря на разницу в производительности на 30% (170 с против 230 с).

Чем это можно объяснить?

1 Ответ

1 голос
/ 09 мая 2019

В Visual C ++ число потоков, выполняющих цикл , в этом примере уменьшается на с omp_set_dynamic(1), что объясняет разницу в производительности.

Однако, вопрекиВера толкования стандарта (и документов Visual C ++) omp_get_num_threads не сообщает об этом сокращении .

Единственный способ выяснить, сколько потоков MSVC на самом деле для каждой параллельной области используется проверка omp_get_thread_num на на каждой итерации цикла (или параллельной задаче).Ниже приведен один из способов сделать это с небольшим повышением производительности в цикле:

// std::hardware_destructive_interference_size is not available in gcc or clang, also see comments by Peter Cordes:
// https://stackoverflow.com/questions/39680206/understanding-stdhardware-destructive-interference-size-and-stdhardware-cons
struct alignas(2 * std::hardware_destructive_interference_size) NoFalseSharing
{
    int flagValue = 0;
};

void foo()
{
  std::vector<NoFalseSharing> flags(omp_get_max_threads());

#pragma omp parallel for
  for (int j = 0; j < count; ++j)
  {
    flags[omp_get_thread_num()].flagValue = 1;

    // Your real loop body
  }

  int realOmpNumThreads = 0;
  for (auto flag : flags)
    realOmpNumThreads += flag.flagValue;
}

Действительно, вы обнаружите, что realOmpNumThreads даст значительно отличающиеся значения от omp_get_num_threads() внутри параллельной области с omp_set_dynamic(1) в Visual C ++.


Можно утверждать, что технически

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

буквально не одинаковы.

На мой взгляд, это бессмысленная интерпретация стандарта, потому что цель очень ясна, и у стандарта нет оснований говорить " Количество потоков в команде, выполняющей параллельный регион , остается неизменным для продолжительности этой параллельной области и сообщается функцией omp_get_num_threads"в этом разделе, если это число не связано с функциональностью omp_set_dynamic.

Однако яМожет случиться так, что MSVC решил оставить число потоков в команде без изменений и просто назначить не повторения цикла для выполнения подмножеству из них в omp_set_dynamic(1) для простоты реализации.

В любом случае: Не доверяйте omp_get_num_threads в Visual C ++.

...