如果我们查看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),但在两种情况下都显示了相同的值。
这怎么解释?
答案 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)
。