openmp嵌套循环处理性能

时间:2019-10-17 11:30:03

标签: c++ performance openmp

请考虑以下代码:

void process(int N, int K, const vector<int>& data)
{
    #pragma omp parallel for
    for(int i = 0; i < data.size(); ++i)
    {
        //perform some processing based on data, N and K
    }
}

void solve(int N, int K, const vector<int>& data)
{
    for(int i = 0; i < N; ++i)
    {
        for(int j = 0; j < K; ++j)
        {
            process(N, K, data);
        }
    }
}

以上代码以每个参数的不同大小执行。 N和K在1-1000的范围内(大多数情况下)。通常两者都是1。 data.size()也相差很大,介于100到30万之间。

上面的代码在大多数情况下都很好用。问题是如果N或K大于〜100。例如K为300,数据不是很大。例如:1000。 在这种情况下,大多数时候我的程序都在等待唤醒omp线程。而且,如果我禁用了omp,则在这种情况下,程序的速度要快2-3倍。

我的问题是-在solve函数内部执行循环时,是否有可能以某种方式使omp保持自旋锁? 我已经尝试过OMP_WAIT_POLICY Active并解决了问题,但是由于其他原因(这是大应用程序的一小部分),到目前为止,我必须保持被动模式。还有其他选择可以使线程保持一段时间活动(或任何其他解决此问题的方法)吗?

编辑: 根据@Gilles,这是我的完整测试程序:

#include <atomic>
#include <iostream>
#include <vector>
#include <chrono>
#include <omp.h>

std::atomic<int> cnt;

void process(int a, int b, std::vector<int>& d)
{
    #pragma omp parallel for 
    for (int i = 0; i < d.size(); ++i)
    {
        //sample operation
        if (d[i] > a + b)
            ++cnt;
    }
}

void solve(int N, int K, std::vector<int>& d)
{
    for (int i = 0; i < N; ++i)
    {
        for (int j = 0; j < K; ++j)
        {
            process(i, j, d);
        }
    }
}

void RunTest(int numOfThreads, int N, int K, int arrSize)
{
    std::vector<int> s(arrSize);
    s[0] = s[10] = 1000;

    omp_set_num_threads(numOfThreads);
    cnt = 0;

    std::chrono::duration<double> minDiff = std::chrono::duration<double>{ 99999999 };
    for (int iters = 0; iters < 20; ++iters)
    {
        auto start = std::chrono::high_resolution_clock::now();
        solve(N, K, s);
        auto end = std::chrono::high_resolution_clock::now();

        std::chrono::duration<double> diff = end - start;
        if (diff < minDiff)
            minDiff = diff;
    }
    std::cout << "Time: " << minDiff.count() * 1000 << " ms \t\t" << "Threads: " << numOfThreads << " N: " << N << " K: " << K << std::endl;
}

int main()
{
    std::cout << "Large N*K" << std::endl;
    RunTest(6, 100, 100, 10000);
    RunTest(1, 100, 100, 10000);

    std::cout << std::endl;
    std::cout << "Small N*K" << std::endl;
    RunTest(6, 1, 1, 1000000);
    RunTest(1, 1, 1, 1000000);
}

根据ACTIVE / PASSIVE等待策略的结果(在MSVC 2019上测试):

PASSIVE:
Large N*K
Time: 126.358 ms                Threads: 6 N: 100 K: 100
Time: 83.0023 ms                Threads: 1 N: 100 K: 100

Small N*K
Time: 0.194 ms                  Threads: 6 N: 1 K: 1
Time: 0.6687 ms                 Threads: 1 N: 1 K: 1

ACTIVE
Large N*K
Time: 20.8449 ms                Threads: 6 N: 100 K: 100
Time: 82.4809 ms                Threads: 1 N: 100 K: 100

Small N*K
Time: 0.1404 ms                 Threads: 6 N: 1 K: 1
Time: 0.6845 ms                 Threads: 1 N: 1 K: 1

正如您在被动模式下看到的那样,当N * K很大时,时间会更长。

2 个答案:

答案 0 :(得分:3)

  

或任何其他想法如何解决此问题?

在将计算分配给线程时,您希望拥有尽可能大的块,并希望尽可能少的同步。在您的示例中,您应该并行化最外面的循环。在您的示例中,尚不清楚process是否修改了data。它作为非const传递,但是假设它没有被修改,这是我期望更好的表现:

void solve(int N, int K, vector<int>& data)
{
    #pragma omp parallel for
    for(int i = 0; i < N; ++i)
    {
        for(int j = 0; j < K; ++j)
        {
            process(N, K, data);
        }
    } // <-- threads have to wait here until all are finished
}

(简单化)基本原理:产生和收集线程会花费时间并带来开销。在您的代码中,您有N*K次的开销。如果并行化最外层的循环,则开销只有一次。

答案 1 :(得分:1)

基于您的MCVE,我进行了一些测试,我相信您的代码编写方式中会出现一些问题。

  1. 您正在使用std::atomic<int>作为计数器的类型,该计数器用于累积结果,并只是增加它(从原子上讲)。尽管从功能的角度来看这是正确的,但这不是一种非常有效的方法。将计数器更改为简单的int,然后在reduction(+:cnt)区域中将其声明为parallel会更好。
  2. 您正在parallel x N循环内创建K区域。向上移动parallel指令可能会有所帮助。然后您以前的#pragma omp parallel for指令就变成了孤儿#pragma omp for了。

因此,我对这些想法进行了一些实验,这就是我现在所拥有的(使用4个线程,因为我的计算机上有4个内核):

-您的带有被动策略的版本:

Large N*K
Time: 74.4741 ms        Threads: 4 N: 100 K: 100
Time: 40.2336 ms        Threads: 1 N: 100 K: 100

Small N*K
Time: 0.151747 ms       Threads: 4 N: 1 K: 1
Time: 0.395791 ms       Threads: 1 N: 1 K: 1

-我的带有被动策略的版本:

Large N*K
Time: 35.1184 ms        Threads: 4 N: 100 K: 100
Time: 7.932 ms      Threads: 1 N: 100 K: 100

Small N*K
Time: 0.040216 ms       Threads: 4 N: 1 K: 1
Time: 0.082633 ms       Threads: 1 N: 1 K: 1

-具有有效策略的版本

Large N*K
Time: 16.3105 ms        Threads: 4 N: 100 K: 100
Time: 44.4862 ms        Threads: 1 N: 100 K: 100

Small N*K
Time: 0.110355 ms       Threads: 4 N: 1 K: 1
Time: 0.427118 ms       Threads: 1 N: 1 K: 1

-具有有效策略的我的版本:

Large N*K
Time: 5.30402 ms        Threads: 4 N: 100 K: 100
Time: 9.57645 ms        Threads: 1 N: 100 K: 100

Small N*K
Time: 0.028136 ms       Threads: 4 N: 1 K: 1
Time: 0.094375 ms       Threads: 1 N: 1 K: 1

从那我想说:

  1. 取而代之的是删除std:atomic并使用reduction(+)会产生重大影响
  2. 如果等待策略必须是被动的,那么采用多线程路由就没有意义,因为在该配置中,单线程版本始终比多线程版本快。

记录下来,修改后的部分如下所示:

int cnt;

void process(int a, int b, std::vector<int>& d)
{
    #pragma omp for reduction(+:cnt)
    for (int i = 0; i < d.size(); ++i)
    {
        //sample operation
        if (d[i] > a + b)
            ++cnt;
    }
}

void solve(int N, int K, std::vector<int>& d)
{
    #pragma omp parallel
    for (int i = 0; i < N; ++i)
    {
        for (int j = 0; j < K; ++j)
        {
            process(i, j, d);
        }
    }
}