为什么omp_set_dynamic(1)从不调整线程数(在Visual C ++中)?

时间:2019-05-09 09:07:49

标签: c++ multithreading visual-c++ openmp

如果我们查看omp_set_dynamic的{​​{3}},它实际上是从Visual C++ documentation复制粘贴而成的(第39页的3.1.7节):

  

如果[功能参数]计算为非零值,则运行时环境可以自动调整用于执行即将到来的并行区域的线程数,以最佳利用系统资源。结果,用户指定的线程数就是最大线程数。在并行区域内,执行并行区域的团队中的线程数保持不变,并由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;
}

编译器版本:用于x64的Microsoft(R)C / C ++优化编译器版本19.16.27024.1

例如gcc,此程序将为averageThreadNum打印的omp_set_dynamic(1)omp_set_dynamic(0)打印的omp_set_dynamic(1)低得多。但是在MSVC上,尽管性能相差30%(170s与230s),但在两种情况下都显示了相同的值。

这怎么解释?

1 个答案:

答案 0 :(得分:1)

在Visual C ++中,在本示例中,执行循环的线程数减少了omp_get_num_threads,这说明了性能差异。

但是,与对标准(和Visual C ++文档)的任何善意解释相反, omp_get_thread_num并未报告这种减少。

弄清楚MSVC实际上每个并行区域使用多少线程的唯一方法是检查每个循环迭代(或并行任务)上的// 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()与Visual C ++上与omp_set_dynamic(1)的并行区域内的omp_get_num_threads产生明显不同的值。


有人可能会说技术上

  • 团队中执行并行区域的线程数”和
  • 用于执行即将到来的并行区域的线程数

字面上并不相同。

在我看来,这是对该标准的荒谬解释,因为其意图非常明确,并且该标准没有理由说“ 执行并行区域的团队中的线程数保持”固定(在该并行区域的持续时间内),并且由此部分的omp_set_dynamic ”报告(如果该数字与{{1 }}。

但是,可能是MSVC决定不影响团队中的线程数,而只是将{strong>不分配循环迭代供执行分配给{ {1}},以简化实施。

无论哪种情况:在Visual C ++中不要信任omp_set_dynamic(1)